Merge branch 'master' into limit-match-settings-textbox

This commit is contained in:
Bartłomiej Dach
2021-08-18 21:17:16 +02:00
committed by GitHub
468 changed files with 10003 additions and 4588 deletions

View File

@ -9,7 +9,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
{
public abstract class DisableableTabControl<T> : TabControl<T>
{
public readonly BindableBool Enabled = new BindableBool();
public readonly BindableBool Enabled = new BindableBool(true);
protected override void AddTabItem(TabItem<T> tab, bool addToDropdown = true)
{

View File

@ -2,24 +2,28 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Online.Rooms;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Components
{
public class DrawableGameType : CircularContainer, IHasTooltip
{
private readonly GameType type;
private readonly MatchType type;
public LocalisableString TooltipText => type.Name;
public LocalisableString TooltipText => type.GetLocalisableDescription();
public DrawableGameType(GameType type)
public DrawableGameType(MatchType type)
{
this.type = type;
Masking = true;
@ -34,10 +38,138 @@ namespace osu.Game.Screens.OnlinePlay.Components
};
}
[Resolved]
private OsuColour colours { get; set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
private void load()
{
Add(type.GetIcon(colours, Height / 2));
Add(getIconFor(type));
}
private Drawable getIconFor(MatchType matchType)
{
float size = Height / 2;
switch (matchType)
{
default:
case MatchType.Playlists:
return new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(size),
Icon = FontAwesome.Regular.Clock,
Colour = colours.Blue,
Shadow = false
};
case MatchType.HeadToHead:
return new VersusRow(colours.Blue, colours.Blue, size * 0.6f)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
case MatchType.TeamVersus:
return new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(2f),
Children = new[]
{
new VersusRow(colours.Blue, colours.Pink, size * 0.5f),
new VersusRow(colours.Blue, colours.Pink, size * 0.5f),
},
};
// case MatchType.TagCoop:
// return new SpriteIcon
// {
// Anchor = Anchor.Centre,
// Origin = Anchor.Centre,
// Size = new Vector2(size),
// Icon = FontAwesome.Solid.Sync,
// Colour = colours.Blue,
//
// Shadow = false
// };
// case MatchType.TagTeamCoop:
// return new FillFlowContainer
// {
// Anchor = Anchor.Centre,
// Origin = Anchor.Centre,
// AutoSizeAxes = Axes.Both,
// Direction = FillDirection.Horizontal,
// Spacing = new Vector2(2f),
// Children = new[]
// {
// new SpriteIcon
// {
// Icon = FontAwesome.Solid.Sync,
// Size = new Vector2(size * 0.75f),
// Colour = colours.Blue,
// Shadow = false,
// },
// new SpriteIcon
// {
// Icon = FontAwesome.Solid.Sync,
// Size = new Vector2(size * 0.75f),
// Colour = colours.Pink,
// Shadow = false,
// },
// },
// };
}
}
private class VersusRow : FillFlowContainer
{
public VersusRow(Color4 first, Color4 second, float size)
{
var triangleSize = new Vector2(size);
AutoSizeAxes = Axes.Both;
Spacing = new Vector2(2f, 0f);
Children = new[]
{
new Container
{
Size = triangleSize,
Colour = first,
Children = new[]
{
new EquilateralTriangle
{
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Rotation = 90,
EdgeSmoothness = new Vector2(1f),
},
},
},
new Container
{
Size = triangleSize,
Colour = second,
Children = new[]
{
new EquilateralTriangle
{
Anchor = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Rotation = -90,
EdgeSmoothness = new Vector2(1f),
},
},
},
};
}
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -14,8 +15,10 @@ namespace osu.Game.Screens.OnlinePlay.Components
/// </summary>
public class ListingPollingComponent : RoomPollingComponent
{
[Resolved]
private Bindable<FilterCriteria> currentFilter { get; set; }
public IBindable<bool> InitialRoomsReceived => initialRoomsReceived;
private readonly Bindable<bool> initialRoomsReceived = new Bindable<bool>();
public readonly Bindable<FilterCriteria> Filter = new Bindable<FilterCriteria>();
[Resolved]
private Bindable<Room> selectedRoom { get; set; }
@ -23,9 +26,11 @@ namespace osu.Game.Screens.OnlinePlay.Components
[BackgroundDependencyLoader]
private void load()
{
currentFilter.BindValueChanged(_ =>
Filter.BindValueChanged(_ =>
{
NotifyRoomsReceived(null);
RoomManager.ClearRooms();
initialRoomsReceived.Value = false;
if (IsLoaded)
PollImmediately();
});
@ -38,24 +43,26 @@ namespace osu.Game.Screens.OnlinePlay.Components
if (!API.IsLoggedIn)
return base.Poll();
if (Filter.Value == null)
return base.Poll();
var tcs = new TaskCompletionSource<bool>();
pollReq?.Cancel();
pollReq = new GetRoomsRequest(currentFilter.Value.Status, currentFilter.Value.Category);
pollReq = new GetRoomsRequest(Filter.Value.Status, Filter.Value.Category);
pollReq.Success += result =>
{
for (int i = 0; i < result.Count; i++)
foreach (var existing in RoomManager.Rooms.ToArray())
{
if (result[i].RoomID.Value == selectedRoom.Value?.RoomID.Value)
{
// The listing request always has less information than the opened room, so don't include it.
result[i] = selectedRoom.Value;
break;
}
if (result.All(r => r.RoomID.Value != existing.RoomID.Value))
RoomManager.RemoveRoom(existing);
}
NotifyRoomsReceived(result);
foreach (var incoming in result)
RoomManager.AddOrUpdateRoom(incoming);
initialRoomsReceived.Value = true;
tcs.SetResult(true);
};

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

@ -8,7 +8,6 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
@ -17,15 +16,12 @@ using osu.Game.Rulesets;
namespace osu.Game.Screens.OnlinePlay.Components
{
public abstract class RoomManager : CompositeDrawable, IRoomManager
public class RoomManager : Component, IRoomManager
{
public event Action RoomsUpdated;
private readonly BindableList<Room> rooms = new BindableList<Room>();
public IBindable<bool> InitialRoomsReceived => initialRoomsReceived;
private readonly Bindable<bool> initialRoomsReceived = new Bindable<bool>();
public IBindableList<Room> Rooms => rooms;
protected IBindable<Room> JoinedRoom => joinedRoom;
@ -40,15 +36,9 @@ namespace osu.Game.Screens.OnlinePlay.Components
[Resolved]
private IAPIProvider api { get; set; }
protected RoomManager()
public RoomManager()
{
RelativeSizeAxes = Axes.Both;
InternalChildren = CreatePollingComponents().Select(p =>
{
p.RoomsReceived = onRoomsReceived;
return p;
}).ToList();
}
protected override void Dispose(bool isDisposing)
@ -67,10 +57,10 @@ namespace osu.Game.Screens.OnlinePlay.Components
{
joinedRoom.Value = room;
update(room, result);
addRoom(room);
AddOrUpdateRoom(result);
room.CopyFrom(result); // Also copy back to the source model, since this is likely to have been stored elsewhere.
RoomsUpdated?.Invoke();
// The server may not contain all properties (such as password), so invoke success with the given room.
onSuccess?.Invoke(room);
};
@ -118,84 +108,49 @@ namespace osu.Game.Screens.OnlinePlay.Components
private readonly HashSet<long> ignoredRooms = new HashSet<long>();
private void onRoomsReceived(List<Room> received)
public void AddOrUpdateRoom(Room room)
{
if (received == null)
{
ClearRooms();
Debug.Assert(room.RoomID.Value != null);
if (ignoredRooms.Contains(room.RoomID.Value.Value))
return;
}
// Remove past matches
foreach (var r in rooms.ToList())
room.Position.Value = -room.RoomID.Value.Value;
try
{
if (received.All(e => e.RoomID.Value != r.RoomID.Value))
rooms.Remove(r);
}
foreach (var pi in room.Playlist)
pi.MapObjects(beatmaps, rulesets);
for (int i = 0; i < received.Count; i++)
var existing = rooms.FirstOrDefault(e => e.RoomID.Value == room.RoomID.Value);
if (existing == null)
rooms.Add(room);
else
existing.CopyFrom(room);
}
catch (Exception ex)
{
var room = received[i];
Logger.Error(ex, $"Failed to update room: {room.Name.Value}.");
Debug.Assert(room.RoomID.Value != null);
if (ignoredRooms.Contains(room.RoomID.Value.Value))
continue;
room.Position.Value = i;
try
{
update(room, room);
addRoom(room);
}
catch (Exception ex)
{
Logger.Error(ex, $"Failed to update room: {room.Name.Value}.");
ignoredRooms.Add(room.RoomID.Value.Value);
rooms.Remove(room);
}
ignoredRooms.Add(room.RoomID.Value.Value);
rooms.Remove(room);
}
RoomsUpdated?.Invoke();
initialRoomsReceived.Value = true;
notifyRoomsUpdated();
}
protected void RemoveRoom(Room room) => rooms.Remove(room);
public void RemoveRoom(Room room)
{
rooms.Remove(room);
notifyRoomsUpdated();
}
protected void ClearRooms()
public void ClearRooms()
{
rooms.Clear();
initialRoomsReceived.Value = false;
notifyRoomsUpdated();
}
/// <summary>
/// Updates a local <see cref="Room"/> with a remote copy.
/// </summary>
/// <param name="local">The local <see cref="Room"/> to update.</param>
/// <param name="remote">The remote <see cref="Room"/> to update with.</param>
private void update(Room local, Room remote)
{
foreach (var pi in remote.Playlist)
pi.MapObjects(beatmaps, rulesets);
local.CopyFrom(remote);
}
/// <summary>
/// Adds a <see cref="Room"/> to the list of available rooms.
/// </summary>
/// <param name="room">The <see cref="Room"/> to add.</param>
private void addRoom(Room room)
{
var existing = rooms.FirstOrDefault(e => e.RoomID.Value == room.RoomID.Value);
if (existing == null)
rooms.Add(room);
else
existing.CopyFrom(room);
}
protected abstract IEnumerable<RoomPollingComponent> CreatePollingComponents();
private void notifyRoomsUpdated() => Scheduler.AddOnce(() => RoomsUpdated?.Invoke());
}
}

View File

@ -1,29 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
namespace osu.Game.Screens.OnlinePlay.Components
{
public abstract class RoomPollingComponent : PollingComponent
{
/// <summary>
/// Invoked when any <see cref="Room"/>s have been received from the API.
/// <para>
/// Any <see cref="Room"/>s present locally but not returned by this event are to be removed from display.
/// If null, the display of local rooms is reset to an initial state.
/// </para>
/// </summary>
public Action<List<Room>> RoomsReceived;
[Resolved]
protected IAPIProvider API { get; private set; }
protected void NotifyRoomsReceived(List<Room> rooms) => RoomsReceived?.Invoke(rooms);
[Resolved]
protected IRoomManager RoomManager { get; private set; }
}
}

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,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -48,17 +46,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
pollReq.Success += result =>
{
// existing rooms need to be ordered by their position because the received of NotifyRoomsReceives expects to be able to sort them based on this order.
var rooms = new List<Room>(roomManager.Rooms.OrderBy(r => r.Position.Value));
int index = rooms.FindIndex(r => r.RoomID.Value == result.RoomID.Value);
if (index < 0)
return;
rooms[index] = result;
NotifyRoomsReceived(rooms);
RoomManager.AddOrUpdateRoom(result);
tcs.SetResult(true);
};

View File

@ -1,15 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using 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.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Screens.Ranking.Expanded;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Components
@ -62,8 +64,8 @@ namespace osu.Game.Screens.OnlinePlay.Components
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
minDisplay = new StarRatingDisplay(default),
maxDisplay = new StarRatingDisplay(default)
minDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Range),
maxDisplay = new StarRatingDisplay(default, StarRatingDisplaySize.Range)
}
}
};
@ -85,9 +87,10 @@ 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.ForDifficultyRating(minDifficulty.DifficultyRating, true);
maxBackground.Colour = colours.ForDifficultyRating(maxDifficulty.DifficultyRating, true);
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

@ -18,16 +18,29 @@ namespace osu.Game.Screens.OnlinePlay
/// </summary>
event Action RoomsUpdated;
/// <summary>
/// Whether an initial listing of rooms has been received.
/// </summary>
IBindable<bool> InitialRoomsReceived { get; }
/// <summary>
/// All the active <see cref="Room"/>s.
/// </summary>
IBindableList<Room> Rooms { get; }
/// <summary>
/// Adds a <see cref="Room"/> to this <see cref="IRoomManager"/>.
/// If already existing, the local room will be updated with the given one.
/// </summary>
/// <param name="room">The incoming <see cref="Room"/>.</param>
void AddOrUpdateRoom(Room room);
/// <summary>
/// Removes a <see cref="Room"/> from this <see cref="IRoomManager"/>.
/// </summary>
/// <param name="room">The <see cref="Room"/> to remove.</param>
void RemoveRoom(Room room);
/// <summary>
/// Removes all <see cref="Room"/>s from this <see cref="IRoomManager"/>.
/// </summary>
void ClearRooms();
/// <summary>
/// Creates a new <see cref="Room"/>.
/// </summary>

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());
})
};
@ -262,7 +381,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
switch (action)
{
case GlobalAction.Select:
Click();
TriggerClick();
return true;
}
@ -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

@ -30,8 +30,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
public IReadOnlyList<DrawableRoom> Rooms => roomFlow.FlowingChildren.Cast<DrawableRoom>().ToArray();
[Resolved(CanBeNull = true)]
private Bindable<FilterCriteria> filter { get; set; }
public readonly Bindable<FilterCriteria> Filter = new Bindable<FilterCriteria>();
[Resolved]
private Bindable<Room> selectedRoom { get; set; }
@ -50,6 +49,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 +61,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),
}
};
}
@ -71,7 +73,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
rooms.BindTo(roomManager.Rooms);
filter?.BindValueChanged(criteria => Filter(criteria.NewValue));
Filter?.BindValueChanged(criteria => applyFilterCriteria(criteria.NewValue), true);
selectedRoom.BindValueChanged(selection =>
{
@ -82,7 +84,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
private void updateSelection() =>
roomFlow.Children.ForEach(r => r.State = r.Room == selectedRoom.Value ? SelectionState.Selected : SelectionState.NotSelected);
public void Filter(FilterCriteria criteria)
private void applyFilterCriteria(FilterCriteria criteria)
{
roomFlow.Children.ForEach(r =>
{
@ -123,7 +125,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
roomFlow.Add(new DrawableRoom(room));
}
Filter(filter?.Value);
applyFilterCriteria(Filter?.Value);
updateSelection();
}
@ -137,7 +139,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 +153,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 +216,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,23 @@ 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.Logging;
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.Input;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay.Components;
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,12 +38,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby();
private readonly IBindable<bool> initialRoomsReceived = new Bindable<bool>();
private readonly IBindable<bool> operationInProgress = new Bindable<bool>();
protected Container<OsuButton> Buttons { get; } = new Container<OsuButton>
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
AutoSizeAxes = Axes.Both
};
private FilterControl filter;
private Container content;
private LoadingLayer loadingLayer;
protected ListingPollingComponent ListingPollingComponent { get; private set; }
[Resolved]
private Bindable<Room> selectedRoom { get; set; }
@ -44,53 +56,118 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
[Resolved(CanBeNull = true)]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; }
[CanBeNull]
private IDisposable joiningRoomOperation { get; set; }
private RoomsContainer roomsContainer;
[CanBeNull]
private LeasedBindable<Room> selectionLease;
[BackgroundDependencyLoader]
private void load()
private readonly Bindable<FilterCriteria> filter = new Bindable<FilterCriteria>(new FilterCriteria());
private readonly IBindable<bool> operationInProgress = new Bindable<bool>();
private readonly IBindable<bool> isIdle = new BindableBool();
private LoadingLayer loadingLayer;
private RoomsContainer roomsContainer;
private SearchTextBox searchTextBox;
private Dropdown<RoomStatusFilter> statusDropdown;
[BackgroundDependencyLoader(true)]
private void load([CanBeNull] IdleTracker idleTracker)
{
if (idleTracker != null)
isIdle.BindTo(idleTracker.IsIdle);
OsuScrollContainer scrollContainer;
InternalChildren = new Drawable[]
{
content = new Container
ListingPollingComponent = CreatePollingComponent().With(c => c.Filter.BindTarget = filter),
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
{
Filter = { BindTarget = filter }
}
},
}
},
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,37 +183,65 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
{
base.LoadComplete();
initialRoomsReceived.BindTo(RoomManager.InitialRoomsReceived);
initialRoomsReceived.BindValueChanged(_ => updateLoadingLayer());
searchTextBox.Current.BindValueChanged(_ => updateFilterDebounced());
ruleset.BindValueChanged(_ => UpdateFilter());
isIdle.BindValueChanged(_ => updatePollingRate(this.IsCurrentScreen()), true);
if (ongoingOperationTracker != null)
{
operationInProgress.BindTo(ongoingOperationTracker.InProgress);
operationInProgress.BindValueChanged(_ => updateLoadingLayer(), true);
operationInProgress.BindValueChanged(_ => updateLoadingLayer());
}
ListingPollingComponent.InitialRoomsReceived.BindValueChanged(_ => updateLoadingLayer(), true);
updateFilter();
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
#region Filtering
content.Padding = new MarginPadding
public 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)
{
base.OnEntering(last);
onReturning();
}
@ -144,6 +249,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 +274,21 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
base.OnSuspending(next);
}
protected override void OnFocus(FocusEvent e)
{
searchTextBox.TakeFocus();
}
private void onReturning()
{
filter.HoldFocus = true;
updatePollingRate(true);
searchTextBox.HoldFocus = true;
}
private void onLeaving()
{
filter.HoldFocus = false;
updatePollingRate(false);
searchTextBox.HoldFocus = false;
// ensure any password prompt is dismissed.
this.HidePopover();
@ -199,32 +316,63 @@ 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 RoomSubScreen CreateRoomSubScreen(Room room);
private void updateLoadingLayer()
{
if (operationInProgress.Value || !initialRoomsReceived.Value)
if (operationInProgress.Value || !ListingPollingComponent.InitialRoomsReceived.Value)
loadingLayer.Show();
else
loadingLayer.Hide();
}
private void updatePollingRate(bool isCurrentScreen)
{
if (!isCurrentScreen)
ListingPollingComponent.TimeBetweenPolls.Value = 0;
else
ListingPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 120000 : 15000;
Logger.Log($"Polling adjusted (listing: {ListingPollingComponent.TimeBetweenPolls.Value})");
}
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);
protected abstract ListingPollingComponent CreatePollingComponent();
private class LoungeSearchTextBox : SearchTextBox
{
[BackgroundDependencyLoader]
private void load()
{
BackgroundUnfocused = OsuColour.Gray(0.06f);
BackgroundFocused = OsuColour.Gray(0.12f);
}
}
}
}

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.Allocation;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
namespace osu.Game.Screens.OnlinePlay.Match.Components
{
public abstract class CreateRoomButton : PurpleTriangleButton, IKeyBindingHandler<PlatformAction>
{
[BackgroundDependencyLoader]
private void load()
{
SpriteText.Font = SpriteText.Font.With(size: 14);
Triangles.TriangleScale = 1.5f;
}
public bool OnPressed(PlatformAction action)
{
if (!Enabled.Value)
return false;
switch (action)
{
case PlatformAction.DocumentNew:
// might as well also handle new tab. it's a bit of an undefined flow on this screen.
case PlatformAction.TabNew:
TriggerClick();
return true;
}
return false;
}
public void OnReleased(PlatformAction action)
{
}
}
}

View File

@ -4,15 +4,17 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Match.Components
{
public abstract class MatchSettingsOverlay : FocusedOverlayContainer
public abstract class MatchSettingsOverlay : FocusedOverlayContainer, IKeyBindingHandler<GlobalAction>
{
protected const float TRANSITION_DURATION = 350;
protected const float FIELD_PADDING = 45;
@ -21,6 +23,10 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
protected override bool BlockScrollInput => false;
protected abstract OsuButton SubmitButton { get; }
protected abstract bool IsLoading { get; }
[BackgroundDependencyLoader]
private void load()
{
@ -29,18 +35,49 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
Add(Settings = CreateSettings());
}
protected abstract void SelectBeatmap();
protected abstract OnlinePlayComposite CreateSettings();
protected override void PopIn()
{
base.PopIn();
Settings.MoveToY(0, TRANSITION_DURATION, Easing.OutQuint);
}
protected override void PopOut()
{
base.PopOut();
Settings.MoveToY(-1, TRANSITION_DURATION, Easing.InSine);
}
public bool OnPressed(GlobalAction action)
{
switch (action)
{
case GlobalAction.Select:
if (IsLoading)
return true;
if (SubmitButton.Enabled.Value)
{
SubmitButton.TriggerClick();
return true;
}
else
{
SelectBeatmap();
return true;
}
}
return false;
}
public void OnReleased(GlobalAction action)
{
}
protected class SettingsTextBox : OsuTextBox
{
[BackgroundDependencyLoader]

View File

@ -9,31 +9,27 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.GameTypes;
using osu.Game.Screens.OnlinePlay.Components;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Match.Components
{
public class GameTypePicker : DisableableTabControl<GameType>
public class MatchTypePicker : DisableableTabControl<MatchType>
{
private const float height = 40;
private const float selection_width = 3;
protected override TabItem<GameType> CreateTabItem(GameType value) => new GameTypePickerItem(value);
protected override TabItem<MatchType> CreateTabItem(MatchType value) => new GameTypePickerItem(value);
protected override Dropdown<GameType> CreateDropdown() => null;
protected override Dropdown<MatchType> CreateDropdown() => null;
public GameTypePicker()
public MatchTypePicker()
{
Height = height + selection_width * 2;
TabContainer.Spacing = new Vector2(10 - selection_width * 2);
AddItem(new GameTypeTag());
AddItem(new GameTypeVersus());
AddItem(new GameTypeTagTeam());
AddItem(new GameTypeTeamVersus());
AddItem(new GameTypePlaylists());
AddItem(MatchType.HeadToHead);
AddItem(MatchType.TeamVersus);
}
private class GameTypePickerItem : DisableableTabItem
@ -42,7 +38,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
private readonly CircularContainer hover, selection;
public GameTypePickerItem(GameType value)
public GameTypePickerItem(MatchType value)
: base(value)
{
AutoSizeAxes = Axes.Both;

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

@ -8,7 +8,7 @@ using osu.Game.Screens.OnlinePlay.Match.Components;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
public class CreateMultiplayerMatchButton : PurpleTriangleButton
public class CreateMultiplayerMatchButton : CreateRoomButton
{
private IBindable<bool> isConnected;
private IBindable<bool> operationInProgress;
@ -22,8 +22,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[BackgroundDependencyLoader]
private void load()
{
Triangles.TriangleScale = 1.5f;
Text = "Create room";
isConnected = multiplayerClient.IsConnected.GetBoundCopy();

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

@ -47,7 +47,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
RelativeSizeAxes = Axes.X,
Height = 40,
Text = "Select beatmap",
Action = () => matchSubScreen.Push(new MultiplayerMatchSongSelect()),
Action = () =>
{
if (matchSubScreen.IsCurrentScreen())
matchSubScreen.Push(new MultiplayerMatchSongSelect());
},
Alpha = 0
}
}
@ -68,6 +72,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
}, true);
}
public void BeginSelection() => selectButton.TriggerClick();
private void updateBeatmap()
{
if (SelectedItem.Value == null)

View File

@ -6,6 +6,7 @@ using System.Diagnostics;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ExceptionExtensions;
using osu.Framework.Graphics;
@ -27,8 +28,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
public class MultiplayerMatchSettingsOverlay : MatchSettingsOverlay
{
private MatchSettings settings;
protected override OsuButton SubmitButton => settings.ApplyButton;
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
protected override bool IsLoading => ongoingOperationTracker.InProgress.Value;
protected override void SelectBeatmap() => settings.SelectBeatmap();
protected override OnlinePlayComposite CreateSettings()
=> new MatchSettings
=> settings = new MatchSettings
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Y,
@ -43,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
public OsuTextBox NameField, MaxParticipantsField;
public RoomAvailabilityPicker AvailabilityPicker;
public GameTypePicker TypePicker;
public MatchTypePicker TypePicker;
public OsuTextBox PasswordTextBox;
public TriangleButton ApplyButton;
@ -53,6 +65,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private LoadingLayer loadingLayer;
private BeatmapSelectionControl initialBeatmapControl;
public void SelectBeatmap() => initialBeatmapControl.BeginSelection();
[Resolved]
private IRoomManager manager { get; set; }
@ -149,7 +163,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
},
new Section("Game type")
{
Alpha = disabled_alpha,
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
@ -158,10 +171,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
Spacing = new Vector2(7),
Children = new Drawable[]
{
TypePicker = new GameTypePicker
TypePicker = new MatchTypePicker
{
RelativeSizeAxes = Axes.X,
Enabled = { Value = false }
},
typeLabel = new OsuSpriteText
{
@ -267,7 +279,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
loadingLayer = new LoadingLayer(true)
};
TypePicker.Current.BindValueChanged(type => typeLabel.Text = type.NewValue?.Name ?? string.Empty, true);
TypePicker.Current.BindValueChanged(type => typeLabel.Text = type.NewValue.GetLocalisableDescription(), true);
RoomName.BindValueChanged(name => NameField.Text = name.NewValue, true);
Availability.BindValueChanged(availability => AvailabilityPicker.Current.Value = availability.NewValue, true);
Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true);
@ -306,7 +318,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
// Otherwise, update the room directly in preparation for it to be submitted to the API on match creation.
if (client.Room != null)
{
client.ChangeSettings(name: NameField.Text, password: PasswordTextBox.Text).ContinueWith(t => Schedule(() =>
client.ChangeSettings(name: NameField.Text, password: PasswordTextBox.Text, matchType: TypePicker.Current.Value).ContinueWith(t => Schedule(() =>
{
if (t.IsCompletedSuccessfully)
onSuccess(currentRoom.Value);

View File

@ -99,7 +99,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
break;
}
bool enableButton = Client.Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value;
bool enableButton = Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value;
// When the local user is the host and spectating the match, the "start match" state should be enabled if any users are ready.
if (localUser?.State == MultiplayerUserState.Spectating)

View File

@ -2,11 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
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;
@ -25,48 +22,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
client.ChangeState(MultiplayerUserState.Idle);
}
protected override void UpdatePollingRate(bool isIdle)
{
var multiplayerRoomManager = (MultiplayerRoomManager)RoomManager;
if (!this.IsCurrentScreen())
{
multiplayerRoomManager.TimeBetweenListingPolls.Value = 0;
multiplayerRoomManager.TimeBetweenSelectionPolls.Value = 0;
}
else
{
switch (CurrentSubScreen)
{
case LoungeSubScreen _:
multiplayerRoomManager.TimeBetweenListingPolls.Value = isIdle ? 120000 : 15000;
multiplayerRoomManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000;
break;
// Don't poll inside the match or anywhere else.
default:
multiplayerRoomManager.TimeBetweenListingPolls.Value = 0;
multiplayerRoomManager.TimeBetweenSelectionPolls.Value = 0;
break;
}
}
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 }
};
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

@ -1,10 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Framework.Screens;
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.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match;
@ -13,13 +19,45 @@ 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; }
public override void OnResuming(IScreen last)
{
base.OnResuming(last);
// Upon having left a room, we don't know whether we were the only participant, and whether the room is now closed as a result of leaving it.
// To work around this, temporarily remove the room and trigger an immediate listing poll.
if (last is MultiplayerMatchSubScreen match)
{
RoomManager.RemoveRoom(match.Room);
ListingPollingComponent.PollImmediately();
}
}
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 ListingPollingComponent CreatePollingComponent() => new MultiplayerListingPollingComponent();
protected override void OpenNewRoom(Room room)
{
if (client?.IsConnected.Value != true)
@ -30,5 +68,32 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
base.OpenNewRoom(room);
}
private class MultiplayerListingPollingComponent : ListingPollingComponent
{
[Resolved]
private MultiplayerClient client { get; set; }
private readonly IBindable<bool> isConnected = new Bindable<bool>();
[BackgroundDependencyLoader]
private void load()
{
isConnected.BindTo(client.IsConnected);
isConnected.BindValueChanged(c => Scheduler.AddOnce(() =>
{
if (isConnected.Value && IsLoaded)
PollImmediately();
}), true);
}
protected override Task Poll()
{
if (!isConnected.Value)
return Task.CompletedTask;
return base.Poll();
}
}
}
}

View File

@ -42,6 +42,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
public override string ShortTitle => "room";
public readonly Room Room;
[Resolved]
private MultiplayerClient client { get; set; }
@ -49,9 +51,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private OngoingOperationTracker ongoingOperationTracker { get; set; }
[Resolved]
private Bindable<Room> currentRoom { get; set; }
private MultiplayerMatchSettingsOverlay settingsOverlay;
private Bindable<Room> currentRoom { get; set; } // Todo: This should not exist.
private readonly IBindable<bool> isConnected = new Bindable<bool>();
@ -59,9 +59,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private IDisposable readyClickOperation;
private GridContainer mainContent;
private MultiplayerMatchSettingsOverlay settingsOverlay;
public MultiplayerMatchSubScreen(Room room)
{
Room = room;
Title = room.RoomID.Value == null ? "New room" : room.Name.Value;
Activity.Value = new UserActivity.InLobby(room);
}
@ -176,7 +179,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 +277,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
isConnected.BindValueChanged(connected =>
{
if (!connected.NewValue)
Schedule(this.Exit);
handleRoomLost();
}, true);
currentRoom.BindValueChanged(room =>
@ -284,7 +287,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 +451,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 +493,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(IsBreakTime);
}
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

@ -2,11 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ExceptionExtensions;
using osu.Framework.Logging;
using osu.Game.Online.Multiplayer;
@ -21,22 +18,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[Resolved]
private MultiplayerClient multiplayerClient { get; set; }
public readonly Bindable<double> TimeBetweenListingPolls = new Bindable<double>();
public readonly Bindable<double> TimeBetweenSelectionPolls = new Bindable<double>();
private readonly IBindable<bool> isConnected = new Bindable<bool>();
private readonly Bindable<bool> allowPolling = new Bindable<bool>();
private ListingPollingComponent listingPollingComponent;
protected override void LoadComplete()
{
base.LoadComplete();
isConnected.BindTo(multiplayerClient.IsConnected);
isConnected.BindValueChanged(_ => Scheduler.AddOnce(updatePolling));
JoinedRoom.BindValueChanged(_ => Scheduler.AddOnce(updatePolling), true);
}
public override void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
=> base.CreateRoom(room, r => joinMultiplayerRoom(r, r.Password.Value, onSuccess, onError), onError);
@ -64,19 +45,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (JoinedRoom.Value == null)
return;
var joinedRoom = JoinedRoom.Value;
base.PartRoom();
multiplayerClient.LeaveRoom();
// Todo: This is not the way to do this. Basically when we're the only participant and the room closes, there's no way to know if this is actually the case.
// This is delayed one frame because upon exiting the match subscreen, multiplayer updates the polling rate and messes with polling.
Schedule(() =>
{
RemoveRoom(joinedRoom);
listingPollingComponent.PollImmediately();
});
}
private void joinMultiplayerRoom(Room room, string password, Action<Room> onSuccess = null, Action<string> onError = null)
@ -99,70 +69,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}
});
}
private void updatePolling()
{
if (!isConnected.Value)
ClearRooms();
// Don't poll when not connected or when a room has been joined.
allowPolling.Value = isConnected.Value && JoinedRoom.Value == null;
}
protected override IEnumerable<RoomPollingComponent> CreatePollingComponents() => new RoomPollingComponent[]
{
listingPollingComponent = new MultiplayerListingPollingComponent
{
TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls },
AllowPolling = { BindTarget = allowPolling }
},
new MultiplayerSelectionPollingComponent
{
TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls },
AllowPolling = { BindTarget = allowPolling }
}
};
private class MultiplayerListingPollingComponent : ListingPollingComponent
{
public readonly IBindable<bool> AllowPolling = new Bindable<bool>();
protected override void LoadComplete()
{
base.LoadComplete();
AllowPolling.BindValueChanged(allowPolling =>
{
if (!allowPolling.NewValue)
return;
if (IsLoaded)
PollImmediately();
});
}
protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll();
}
private class MultiplayerSelectionPollingComponent : SelectionPollingComponent
{
public readonly IBindable<bool> AllowPolling = new Bindable<bool>();
protected override void LoadComplete()
{
base.LoadComplete();
AllowPolling.BindValueChanged(allowPolling =>
{
if (!allowPolling.NewValue)
return;
if (IsLoaded)
PollImmediately();
});
}
protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll();
}
}
}

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

@ -37,10 +37,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
private RulesetStore rulesets { get; set; }
private SpriteIcon crown;
private OsuSpriteText userRankText;
private ModDisplay userModsDisplay;
private StateDisplay userStateDisplay;
private IconButton kickButton;
public ParticipantPanel(MultiplayerRoomUser user)
{
User = user;
@ -56,99 +59,117 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
var backgroundColour = Color4Extensions.FromHex("#33413C");
InternalChildren = new Drawable[]
InternalChild = new GridContainer
{
crown = new SpriteIcon
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = FontAwesome.Solid.Crown,
Size = new Vector2(14),
Colour = Color4Extensions.FromHex("#F7E65D"),
Alpha = 0
new Dimension(GridSizeMode.Absolute, 18),
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
},
new Container
Content = new[]
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = 24 },
Child = new Container
new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 5,
Children = new Drawable[]
crown = new SpriteIcon
{
new Box
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = FontAwesome.Solid.Crown,
Size = new Vector2(14),
Colour = Color4Extensions.FromHex("#F7E65D"),
Alpha = 0
},
new TeamDisplay(User),
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 5,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Colour = backgroundColour
},
new UserCoverBackground
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Both,
Width = 0.75f,
User = user,
Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0), Color4.White.Opacity(0.25f))
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Spacing = new Vector2(10),
Direction = FillDirection.Horizontal,
Children = new Drawable[]
new Box
{
new UpdateableAvatar
RelativeSizeAxes = Axes.Both,
Colour = backgroundColour
},
new UserCoverBackground
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Both,
Width = 0.75f,
User = user,
Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0), Color4.White.Opacity(0.25f))
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Spacing = new Vector2(10),
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
User = user
},
new UpdateableFlag
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(30, 20),
Country = user?.Country
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 18),
Text = user?.Username
},
userRankText = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: 14),
new UpdateableAvatar
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
User = user
},
new UpdateableFlag
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(30, 20),
Country = user?.Country
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 18),
Text = user?.Username
},
userRankText = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: 14),
}
}
}
},
new Container
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Right = 70 },
Child = userModsDisplay = new ModDisplay
},
new Container
{
Scale = new Vector2(0.5f),
ExpansionMode = ExpansionMode.AlwaysContracted,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Right = 70 },
Child = userModsDisplay = new ModDisplay
{
Scale = new Vector2(0.5f),
ExpansionMode = ExpansionMode.AlwaysContracted,
}
},
userStateDisplay = new StateDisplay
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Margin = new MarginPadding { Right = 10 },
}
},
userStateDisplay = new StateDisplay
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
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),
},
},
}
};
}
@ -157,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;
@ -169,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
@ -201,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

@ -0,0 +1,131 @@
// 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.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{
internal class TeamDisplay : MultiplayerRoomComposite
{
private readonly MultiplayerRoomUser user;
private Drawable box;
[Resolved]
private OsuColour colours { get; set; }
public TeamDisplay(MultiplayerRoomUser user)
{
this.user = user;
RelativeSizeAxes = Axes.Y;
Width = 15;
Margin = new MarginPadding { Horizontal = 3 };
Alpha = 0;
Scale = new Vector2(0, 1);
}
[BackgroundDependencyLoader]
private void load()
{
box = new Container
{
RelativeSizeAxes = Axes.Both,
CornerRadius = 5,
Masking = true,
Scale = new Vector2(0, 1),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = new Box
{
Colour = Color4.White,
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
if (Client.LocalUser?.Equals(user) == true)
{
InternalChild = new OsuClickableContainer
{
RelativeSizeAxes = Axes.Both,
TooltipText = "Change team",
Action = changeTeam,
Child = box
};
}
else
{
InternalChild = box;
}
}
private void changeTeam()
{
Client.SendMatchRequest(new ChangeTeamRequest
{
TeamID = ((Client.LocalUser?.MatchState as TeamVersusUserState)?.TeamID + 1) % 2 ?? 0,
});
}
private int? displayedTeam;
protected override void OnRoomUpdated()
{
base.OnRoomUpdated();
// 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.Equals(user))?.MatchState;
const double duration = 400;
int? newTeam = (userRoomState as TeamVersusUserState)?.TeamID;
if (newTeam == displayedTeam)
return;
displayedTeam = newTeam;
if (displayedTeam != null)
{
box.FadeColour(getColourForTeam(displayedTeam.Value), duration, Easing.OutQuint);
box.ScaleTo(new Vector2(box.Scale.X < 0 ? 1 : -1, 1), duration, Easing.OutQuint);
this.ScaleTo(Vector2.One, duration, Easing.OutQuint);
this.FadeIn(duration);
}
else
{
this.ScaleTo(new Vector2(0, 1), duration, Easing.OutQuint);
this.FadeOut(duration);
}
}
private ColourInfo getColourForTeam(int id)
{
switch (id)
{
default:
return colours.Red;
case 1:
return colours.Blue;
}
}
}
}

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

@ -25,7 +25,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// <param name="score">The score containing the player's replay.</param>
/// <param name="spectatorPlayerClock">The clock controlling the gameplay running state.</param>
public MultiSpectatorPlayer([NotNull] Score score, [NotNull] ISpectatorPlayerClock spectatorPlayerClock)
: base(score)
: base(score, new PlayerConfiguration { AllowUserInteraction = false })
{
this.spectatorPlayerClock = spectatorPlayerClock;
}
@ -34,6 +34,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
private void load()
{
spectatorPlayerClock.WaitingOnFrames.BindTo(waitingOnFrames);
HUDOverlay.PlayerSettingsOverlay.Expire();
HUDOverlay.HoldToQuit.Expire();
}
protected override void UpdateAfterChildren()

View File

@ -3,6 +3,7 @@
using System;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Game.Scoring;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
@ -19,6 +20,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
}
[BackgroundDependencyLoader]
private void load()
{
PlayerSettings.Expire();
}
protected override void LogoArriving(OsuLogo logo, bool resuming)
{
}

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
@ -24,7 +25,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
public override bool DisallowExternalBeatmapRulesetChanges => true;
// We are managing our own adjustments. For now, this happens inside the Player instances themselves.
public override bool AllowRateAdjustments => false;
public override bool AllowTrackAdjustments => false;
/// <summary>
/// Whether all spectating players have finished loading.
@ -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

@ -30,11 +30,14 @@ namespace osu.Game.Screens.OnlinePlay
protected Bindable<RoomStatus> Status { get; private set; }
[Resolved(typeof(Room))]
protected Bindable<GameType> Type { get; private set; }
protected Bindable<MatchType> Type { get; private set; }
[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,26 +11,27 @@ 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;
using osu.Game.Overlays;
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,14 +39,8 @@ namespace osu.Game.Screens.OnlinePlay
public override bool DisallowExternalBeatmapRulesetChanges => true;
private MultiplayerWaveContainer waves;
private OsuButton createButton;
private ScreenStack screenStack;
private LoungeSubScreen loungeSubScreen;
private readonly IBindable<bool> isIdle = new BindableBool();
private ScreenStack screenStack;
[Cached(Type = typeof(IRoomManager))]
protected RoomManager RoomManager { get; private set; }
@ -53,9 +48,6 @@ namespace osu.Game.Screens.OnlinePlay
[Cached]
private readonly Bindable<Room> selectedRoom = new Bindable<Room>();
[Cached]
private readonly Bindable<FilterCriteria> currentFilter = new Bindable<FilterCriteria>(new FilterCriteria());
[Cached]
private readonly OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker();
@ -68,15 +60,9 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved]
protected IAPIProvider API { get; private set; }
[Resolved(CanBeNull = true)]
private IdleTracker idleTracker { get; set; }
[Resolved(CanBeNull = true)]
private OsuLogo logo { get; set; }
private Drawable header;
private Drawable headerBackground;
protected OnlinePlayScreen()
{
Anchor = Anchor.Centre;
@ -107,59 +93,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
}
};
}
@ -184,12 +137,6 @@ namespace osu.Game.Screens.OnlinePlay
apiState.BindTo(API.State);
apiState.BindValueChanged(onlineStateChanged, true);
if (idleTracker != null)
{
isIdle.BindTo(idleTracker.IsIdle);
isIdle.BindValueChanged(idle => UpdatePollingRate(idle.NewValue), true);
}
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@ -199,8 +146,6 @@ namespace osu.Game.Screens.OnlinePlay
return dependencies;
}
protected abstract void UpdatePollingRate(bool isIdle);
private void forcefullyExit()
{
// This is temporary since we don't currently have a way to force screens to be exited
@ -236,8 +181,6 @@ namespace osu.Game.Screens.OnlinePlay
screenStack.CurrentScreen.OnResuming(last);
base.OnResuming(last);
UpdatePollingRate(isIdle.Value);
}
public override void OnSuspending(IScreen next)
@ -247,8 +190,6 @@ namespace osu.Game.Screens.OnlinePlay
Debug.Assert(screenStack.CurrentScreen != null);
screenStack.CurrentScreen.OnSuspending(next);
UpdatePollingRate(isIdle.Value);
}
public override bool OnExiting(IScreen next)
@ -292,18 +233,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,39 +248,21 @@ 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);
if (newScreen is IOsuScreen newOsuScreen)
((IBindable<UserActivity>)Activity).BindTo(newOsuScreen.Activity);
UpdatePollingRate(isIdle.Value);
createButton.FadeTo(newScreen is LoungeSubScreen ? 1 : 0, 200);
}
protected IScreen CurrentSubScreen => screenStack.CurrentScreen;
protected abstract string ScreenTitle { get; }
protected abstract RoomManager CreateRoomManager();
protected virtual RoomManager CreateRoomManager() => new RoomManager();
protected abstract LoungeSubScreen CreateLounge();
protected abstract OsuButton CreateNewMultiplayerGameButton();
private class MultiplayerWaveContainer : WaveContainer
{
protected override bool StartHidden => true;
@ -365,13 +276,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

@ -6,13 +6,11 @@ using osu.Game.Screens.OnlinePlay.Match.Components;
namespace osu.Game.Screens.OnlinePlay.Playlists
{
public class CreatePlaylistsRoomButton : PurpleTriangleButton
public class CreatePlaylistsRoomButton : CreateRoomButton
{
[BackgroundDependencyLoader]
private void load()
{
Triangles.TriangleScale = 1.5f;
Text = "Create playlist";
}
}

View File

@ -1,62 +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 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;
namespace osu.Game.Screens.OnlinePlay.Playlists
{
public class Playlists : OnlinePlayScreen
{
protected override void UpdatePollingRate(bool isIdle)
{
var playlistsManager = (PlaylistsRoomManager)RoomManager;
if (!this.IsCurrentScreen())
{
playlistsManager.TimeBetweenListingPolls.Value = 0;
playlistsManager.TimeBetweenSelectionPolls.Value = 0;
}
else
{
switch (CurrentSubScreen)
{
case LoungeSubScreen _:
playlistsManager.TimeBetweenListingPolls.Value = isIdle ? 120000 : 15000;
playlistsManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000;
break;
case RoomSubScreen _:
playlistsManager.TimeBetweenListingPolls.Value = 0;
playlistsManager.TimeBetweenSelectionPolls.Value = isIdle ? 30000 : 5000;
break;
default:
playlistsManager.TimeBetweenListingPolls.Value = 0;
playlistsManager.TimeBetweenSelectionPolls.Value = 0;
break;
}
}
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" } };
}
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,7 +1,15 @@
// 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.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match;
@ -10,8 +18,62 @@ 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);
protected override ListingPollingComponent CreatePollingComponent() => new ListingPollingComponent();
private enum PlaylistsCategory
{
Any,
Normal,
Spotlight
}
}
}

View File

@ -26,8 +26,16 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
public Action EditPlaylist;
private MatchSettings settings;
protected override OsuButton SubmitButton => settings.ApplyButton;
protected override bool IsLoading => settings.IsLoading; // should probably be replaced with an OngoingOperationTracker.
protected override void SelectBeatmap() => settings.SelectBeatmap();
protected override OnlinePlayComposite CreateSettings()
=> new MatchSettings
=> settings = new MatchSettings
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Y,
@ -45,12 +53,16 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
public RoomAvailabilityPicker AvailabilityPicker;
public TriangleButton ApplyButton;
public bool IsLoading => loadingLayer.State.Value == Visibility.Visible;
public OsuSpriteText ErrorText;
private LoadingLayer loadingLayer;
private DrawableRoomPlaylist playlist;
private OsuSpriteText playlistLength;
private PurpleTriangleButton editPlaylistButton;
[Resolved(CanBeNull = true)]
private IRoomManager manager { get; set; }
@ -199,7 +211,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
},
new Drawable[]
{
new PurpleTriangleButton
editPlaylistButton = new PurpleTriangleButton
{
RelativeSizeAxes = Axes.X,
Height = 40,
@ -292,6 +304,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
ApplyButton.Enabled.Value = hasValidSettings;
}
public void SelectBeatmap() => editPlaylistButton.TriggerClick();
private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) =>
playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}";

View File

@ -1,21 +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.Bindables;
using osu.Game.Screens.OnlinePlay.Components;
namespace osu.Game.Screens.OnlinePlay.Playlists
{
public class PlaylistsRoomManager : RoomManager
{
public readonly Bindable<double> TimeBetweenListingPolls = new Bindable<double>();
public readonly Bindable<double> TimeBetweenSelectionPolls = new Bindable<double>();
protected override IEnumerable<RoomPollingComponent> CreatePollingComponents() => new RoomPollingComponent[]
{
new ListingPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls } },
new SelectionPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls } }
};
}
}

View File

@ -3,11 +3,14 @@
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Input;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
@ -33,12 +36,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
[Resolved(typeof(Room), nameof(Room.Playlist))]
private BindableList<PlaylistItem> playlist { get; set; }
private readonly IBindable<bool> isIdle = new BindableBool();
private MatchSettingsOverlay settingsOverlay;
private MatchLeaderboard leaderboard;
private OverlinedHeader participantsHeader;
private GridContainer mainContent;
private SelectionPollingComponent selectionPollingComponent;
public PlaylistsRoomSubScreen(Room room)
{
@ -46,11 +50,15 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
Activity.Value = new UserActivity.InLobby(room);
}
[BackgroundDependencyLoader]
private void load()
[BackgroundDependencyLoader(true)]
private void load([CanBeNull] IdleTracker idleTracker)
{
if (idleTracker != null)
isIdle.BindTo(idleTracker.IsIdle);
AddRangeInternal(new Drawable[]
{
selectionPollingComponent = new SelectionPollingComponent(),
mainContent = new GridContainer
{
RelativeSizeAxes = Axes.Both,
@ -163,7 +171,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,
@ -230,7 +238,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
settingsOverlay = new PlaylistsMatchSettingsOverlay
{
RelativeSizeAxes = Axes.Both,
EditPlaylist = () => this.Push(new PlaylistsSongSelect()),
EditPlaylist = () =>
{
if (this.IsCurrentScreen())
this.Push(new PlaylistsSongSelect());
},
State = { Value = roomId.Value == null ? Visibility.Visible : Visibility.Hidden }
}
});
@ -256,6 +268,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
base.LoadComplete();
isIdle.BindValueChanged(_ => updatePollingRate(), true);
roomId.BindValueChanged(id =>
{
if (id.NewValue == null)
@ -271,6 +285,12 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
}, true);
}
private void updatePollingRate()
{
selectionPollingComponent.TimeBetweenPolls.Value = isIdle.Value ? 30000 : 5000;
Logger.Log($"Polling adjusted (selection: {selectionPollingComponent.TimeBetweenPolls.Value})");
}
protected override Screen CreateGameplayScreen() => new PlayerLoader(() => new PlaylistsPlayer(SelectedItem.Value)
{
Exited = () => leaderboard.RefreshScores()