mirror of
https://github.com/osukey/osukey.git
synced 2025-08-04 23:24:04 +09:00
Merge branch 'master' into consume-bindable-current-factory
This commit is contained in:
@ -32,10 +32,10 @@ namespace osu.Game.Graphics
|
||||
return Pink;
|
||||
|
||||
case DifficultyRating.Expert:
|
||||
return useLighterColour ? PurpleLight : Purple;
|
||||
return PurpleLight;
|
||||
|
||||
case DifficultyRating.ExpertPlus:
|
||||
return useLighterColour ? Gray9 : Gray0;
|
||||
return useLighterColour ? Gray9 : Color4Extensions.FromHex("#121415");
|
||||
}
|
||||
}
|
||||
|
||||
|
19
osu.Game/Graphics/UserInterfaceV2/OsuColourPicker.cs
Normal file
19
osu.Game/Graphics/UserInterfaceV2/OsuColourPicker.cs
Normal file
@ -0,0 +1,19 @@
|
||||
// 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.UserInterface;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
public class OsuColourPicker : ColourPicker
|
||||
{
|
||||
public OsuColourPicker()
|
||||
{
|
||||
CornerRadius = 10;
|
||||
Masking = true;
|
||||
}
|
||||
|
||||
protected override HSVColourPicker CreateHSVColourPicker() => new OsuHSVColourPicker();
|
||||
protected override HexColourPicker CreateHexColourPicker() => new OsuHexColourPicker();
|
||||
}
|
||||
}
|
129
osu.Game/Graphics/UserInterfaceV2/OsuHSVColourPicker.cs
Normal file
129
osu.Game/Graphics/UserInterfaceV2/OsuHSVColourPicker.cs
Normal file
@ -0,0 +1,129 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
public class OsuHSVColourPicker : HSVColourPicker
|
||||
{
|
||||
private const float spacing = 10;
|
||||
private const float corner_radius = 10;
|
||||
private const float control_border_thickness = 3;
|
||||
|
||||
protected override HueSelector CreateHueSelector() => new OsuHueSelector();
|
||||
protected override SaturationValueSelector CreateSaturationValueSelector() => new OsuSaturationValueSelector();
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load([CanBeNull] OverlayColourProvider colourProvider, OsuColour osuColour)
|
||||
{
|
||||
Background.Colour = colourProvider?.Dark5 ?? osuColour.GreySeafoamDark;
|
||||
|
||||
Content.Padding = new MarginPadding(spacing);
|
||||
Content.Spacing = new Vector2(0, spacing);
|
||||
}
|
||||
|
||||
private static EdgeEffectParameters createShadowParameters() => new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Offset = new Vector2(0, 1),
|
||||
Radius = 3,
|
||||
Colour = Colour4.Black.Opacity(0.3f)
|
||||
};
|
||||
|
||||
private class OsuHueSelector : HueSelector
|
||||
{
|
||||
public OsuHueSelector()
|
||||
{
|
||||
SliderBar.CornerRadius = corner_radius;
|
||||
SliderBar.Masking = true;
|
||||
}
|
||||
|
||||
protected override Drawable CreateSliderNub() => new SliderNub(this);
|
||||
|
||||
private class SliderNub : CompositeDrawable
|
||||
{
|
||||
private readonly Bindable<float> hue;
|
||||
private readonly Box fill;
|
||||
|
||||
public SliderNub(OsuHueSelector osuHueSelector)
|
||||
{
|
||||
hue = osuHueSelector.Hue.GetBoundCopy();
|
||||
|
||||
InternalChild = new CircularContainer
|
||||
{
|
||||
Height = 35,
|
||||
Width = 10,
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Masking = true,
|
||||
BorderColour = Colour4.White,
|
||||
BorderThickness = control_border_thickness,
|
||||
EdgeEffect = createShadowParameters(),
|
||||
Child = fill = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
hue.BindValueChanged(h => fill.Colour = Colour4.FromHSV(h.NewValue, 1, 1), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class OsuSaturationValueSelector : SaturationValueSelector
|
||||
{
|
||||
public OsuSaturationValueSelector()
|
||||
{
|
||||
SelectionArea.CornerRadius = corner_radius;
|
||||
SelectionArea.Masking = true;
|
||||
// purposefully use hard non-AA'd masking to avoid edge artifacts.
|
||||
SelectionArea.MaskingSmoothness = 0;
|
||||
}
|
||||
|
||||
protected override Marker CreateMarker() => new OsuMarker();
|
||||
|
||||
private class OsuMarker : Marker
|
||||
{
|
||||
private readonly Box previewBox;
|
||||
|
||||
public OsuMarker()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = new CircularContainer
|
||||
{
|
||||
Size = new Vector2(20),
|
||||
Masking = true,
|
||||
BorderColour = Colour4.White,
|
||||
BorderThickness = control_border_thickness,
|
||||
EdgeEffect = createShadowParameters(),
|
||||
Child = previewBox = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Current.BindValueChanged(colour => previewBox.Colour = colour.NewValue, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
57
osu.Game/Graphics/UserInterfaceV2/OsuHexColourPicker.cs
Normal file
57
osu.Game/Graphics/UserInterfaceV2/OsuHexColourPicker.cs
Normal file
@ -0,0 +1,57 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
public class OsuHexColourPicker : HexColourPicker
|
||||
{
|
||||
public OsuHexColourPicker()
|
||||
{
|
||||
Padding = new MarginPadding(20);
|
||||
Spacing = 20;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load([CanBeNull] OverlayColourProvider overlayColourProvider, OsuColour osuColour)
|
||||
{
|
||||
Background.Colour = overlayColourProvider?.Dark6 ?? osuColour.GreySeafoamDarker;
|
||||
}
|
||||
|
||||
protected override TextBox CreateHexCodeTextBox() => new OsuTextBox();
|
||||
protected override ColourPreview CreateColourPreview() => new OsuColourPreview();
|
||||
|
||||
private class OsuColourPreview : ColourPreview
|
||||
{
|
||||
private readonly Box preview;
|
||||
|
||||
public OsuColourPreview()
|
||||
{
|
||||
InternalChild = new CircularContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Child = preview = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Current.BindValueChanged(colour => preview.Colour = colour.NewValue, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -223,7 +223,20 @@ namespace osu.Game
|
||||
|
||||
// bind config int to database RulesetInfo
|
||||
configRuleset = LocalConfig.GetBindable<int>(OsuSetting.Ruleset);
|
||||
Ruleset.Value = RulesetStore.GetRuleset(configRuleset.Value) ?? RulesetStore.AvailableRulesets.First();
|
||||
|
||||
var preferredRuleset = RulesetStore.GetRuleset(configRuleset.Value);
|
||||
|
||||
try
|
||||
{
|
||||
Ruleset.Value = preferredRuleset ?? RulesetStore.AvailableRulesets.First();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// on startup, a ruleset may be selected which has compatibility issues.
|
||||
Logger.Error(e, $@"Failed to switch to preferred ruleset {preferredRuleset}.");
|
||||
Ruleset.Value = RulesetStore.AvailableRulesets.First();
|
||||
}
|
||||
|
||||
Ruleset.ValueChanged += r => configRuleset.Value = r.NewValue.ID ?? 0;
|
||||
|
||||
// bind config int to database SkinInfo
|
||||
@ -478,6 +491,10 @@ namespace osu.Game
|
||||
public override Task Import(params ImportTask[] imports)
|
||||
{
|
||||
// encapsulate task as we don't want to begin the import process until in a ready state.
|
||||
|
||||
// ReSharper disable once AsyncVoidLambda
|
||||
// TODO: This is bad because `new Task` doesn't have a Func<Task?> override.
|
||||
// Only used for android imports and a bit of a mess. Probably needs rethinking overall.
|
||||
var importTask = new Task(async () => await base.Import(imports).ConfigureAwait(false));
|
||||
|
||||
waitForReady(() => this, _ => importTask.Start());
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using Humanizer;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osuTK.Graphics;
|
||||
@ -17,11 +18,11 @@ namespace osu.Game.Overlays.Changelog
|
||||
Width *= 2;
|
||||
}
|
||||
|
||||
protected override string MainText => Value.DisplayName;
|
||||
protected override LocalisableString MainText => Value.DisplayName;
|
||||
|
||||
protected override string AdditionalText => Value.LatestBuild.DisplayVersion;
|
||||
protected override LocalisableString AdditionalText => Value.LatestBuild.DisplayVersion;
|
||||
|
||||
protected override string InfoText => Value.LatestBuild.Users > 0 ? $"{"user".ToQuantity(Value.LatestBuild.Users, "N0")} online" : null;
|
||||
protected override LocalisableString InfoText => Value.LatestBuild.Users > 0 ? $"{"user".ToQuantity(Value.LatestBuild.Users, "N0")} online" : null;
|
||||
|
||||
protected override Color4 GetBarColour(OsuColour colours) => Value.Colour;
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.Dashboard
|
||||
{
|
||||
@ -13,13 +16,14 @@ namespace osu.Game.Overlays.Dashboard
|
||||
{
|
||||
public DashboardTitle()
|
||||
{
|
||||
Title = "dashboard";
|
||||
Title = HomeStrings.UserTitle;
|
||||
Description = "view your friends and other information";
|
||||
IconTexture = "Icons/Hexacons/social";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[LocalisableEnum(typeof(DashboardOverlayTabsEnumLocalisationMapper))]
|
||||
public enum DashboardOverlayTabs
|
||||
{
|
||||
Friends,
|
||||
@ -27,4 +31,22 @@ namespace osu.Game.Overlays.Dashboard
|
||||
[Description("Currently Playing")]
|
||||
CurrentlyPlaying
|
||||
}
|
||||
|
||||
public class DashboardOverlayTabsEnumLocalisationMapper : EnumLocalisationMapper<DashboardOverlayTabs>
|
||||
{
|
||||
public override LocalisableString Map(DashboardOverlayTabs value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case DashboardOverlayTabs.Friends:
|
||||
return FriendsStrings.TitleCompact;
|
||||
|
||||
case DashboardOverlayTabs.CurrentlyPlaying:
|
||||
return @"Currently Playing";
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -14,9 +16,9 @@ namespace osu.Game.Overlays.Dashboard.Friends
|
||||
{
|
||||
}
|
||||
|
||||
protected override string MainText => Value.Status.ToString();
|
||||
protected override LocalisableString MainText => Value.Status.GetLocalisableDescription();
|
||||
|
||||
protected override string AdditionalText => Value.Count.ToString();
|
||||
protected override LocalisableString AdditionalText => Value.Count.ToString();
|
||||
|
||||
protected override Color4 GetBarColour(OsuColour colours)
|
||||
{
|
||||
|
@ -1,12 +1,38 @@
|
||||
// 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.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.Dashboard.Friends
|
||||
{
|
||||
[LocalisableEnum(typeof(OnlineStatusEnumLocalisationMapper))]
|
||||
public enum OnlineStatus
|
||||
{
|
||||
All,
|
||||
Online,
|
||||
Offline
|
||||
}
|
||||
|
||||
public class OnlineStatusEnumLocalisationMapper : EnumLocalisationMapper<OnlineStatus>
|
||||
{
|
||||
public override LocalisableString Map(OnlineStatus value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case OnlineStatus.All:
|
||||
return SortStrings.All;
|
||||
|
||||
case OnlineStatus.Online:
|
||||
return UsersStrings.StatusOnline;
|
||||
|
||||
case OnlineStatus.Offline:
|
||||
return UsersStrings.StatusOffline;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.Dashboard.Friends
|
||||
{
|
||||
@ -9,6 +12,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
|
||||
{
|
||||
}
|
||||
|
||||
[LocalisableEnum(typeof(UserSortCriteriaEnumLocalisationMappper))]
|
||||
public enum UserSortCriteria
|
||||
{
|
||||
[Description(@"Recently Active")]
|
||||
@ -16,4 +20,25 @@ namespace osu.Game.Overlays.Dashboard.Friends
|
||||
Rank,
|
||||
Username
|
||||
}
|
||||
|
||||
public class UserSortCriteriaEnumLocalisationMappper : EnumLocalisationMapper<UserSortCriteria>
|
||||
{
|
||||
public override LocalisableString Map(UserSortCriteria value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case UserSortCriteria.LastVisit:
|
||||
return SortStrings.LastVisit;
|
||||
|
||||
case UserSortCriteria.Rank:
|
||||
return SortStrings.Rank;
|
||||
|
||||
case UserSortCriteria.Username:
|
||||
return SortStrings.Username;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,9 @@ using osu.Framework.Allocation;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Localisation;
|
||||
using System;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Framework.Extensions;
|
||||
|
||||
namespace osu.Game.Overlays
|
||||
{
|
||||
@ -57,7 +60,7 @@ namespace osu.Game.Overlays
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; }
|
||||
|
||||
public LocalisableString TooltipText => $@"{Value} view";
|
||||
public LocalisableString TooltipText => Value.GetLocalisableDescription();
|
||||
|
||||
private readonly SpriteIcon icon;
|
||||
|
||||
@ -98,10 +101,32 @@ namespace osu.Game.Overlays
|
||||
}
|
||||
}
|
||||
|
||||
[LocalisableEnum(typeof(OverlayPanelDisplayStyleEnumLocalisationMapper))]
|
||||
public enum OverlayPanelDisplayStyle
|
||||
{
|
||||
Card,
|
||||
List,
|
||||
Brick
|
||||
}
|
||||
|
||||
public class OverlayPanelDisplayStyleEnumLocalisationMapper : EnumLocalisationMapper<OverlayPanelDisplayStyle>
|
||||
{
|
||||
public override LocalisableString Map(OverlayPanelDisplayStyle value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case OverlayPanelDisplayStyle.Card:
|
||||
return UsersStrings.ViewModeCard;
|
||||
|
||||
case OverlayPanelDisplayStyle.List:
|
||||
return UsersStrings.ViewModeList;
|
||||
|
||||
case OverlayPanelDisplayStyle.Brick:
|
||||
return UsersStrings.ViewModeBrick;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value), value, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -118,7 +119,7 @@ namespace osu.Game.Overlays
|
||||
}
|
||||
});
|
||||
|
||||
TooltipText = "Scroll to top";
|
||||
TooltipText = CommonStrings.ButtonsBackToTop;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Overlays
|
||||
{
|
||||
@ -88,11 +89,11 @@ namespace osu.Game.Overlays
|
||||
SelectedItem.BindValueChanged(_ => updateState(), true);
|
||||
}
|
||||
|
||||
protected abstract string MainText { get; }
|
||||
protected abstract LocalisableString MainText { get; }
|
||||
|
||||
protected abstract string AdditionalText { get; }
|
||||
protected abstract LocalisableString AdditionalText { get; }
|
||||
|
||||
protected virtual string InfoText => string.Empty;
|
||||
protected virtual LocalisableString InfoText => string.Empty;
|
||||
|
||||
protected abstract Color4 GetBarColour(OsuColour colours);
|
||||
|
||||
|
@ -6,6 +6,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Containers.Markdown;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Wiki.Markdown
|
||||
@ -32,11 +33,7 @@ namespace osu.Game.Overlays.Wiki.Markdown
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new WikiMarkdownImage(linkInline)
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
},
|
||||
new BlockMarkdownImage(linkInline),
|
||||
parentTextComponent.CreateSpriteText().With(t =>
|
||||
{
|
||||
t.Text = linkInline.Title;
|
||||
@ -45,5 +42,50 @@ namespace osu.Game.Overlays.Wiki.Markdown
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private class BlockMarkdownImage : WikiMarkdownImage
|
||||
{
|
||||
public BlockMarkdownImage(LinkInline linkInline)
|
||||
: base(linkInline)
|
||||
{
|
||||
AutoSizeAxes = Axes.Y;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
}
|
||||
|
||||
protected override ImageContainer CreateImageContainer(string url) => new BlockImageContainer(url);
|
||||
|
||||
private class BlockImageContainer : ImageContainer
|
||||
{
|
||||
public BlockImageContainer(string url)
|
||||
: base(url)
|
||||
{
|
||||
AutoSizeAxes = Axes.Y;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
}
|
||||
|
||||
protected override Sprite CreateImageSprite() => new ImageSprite();
|
||||
|
||||
private class ImageSprite : Sprite
|
||||
{
|
||||
public ImageSprite()
|
||||
{
|
||||
Anchor = Anchor.TopCentre;
|
||||
Origin = Anchor.TopCentre;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (Width > Parent.DrawWidth)
|
||||
{
|
||||
float ratio = Height / Width;
|
||||
Width = Parent.DrawWidth;
|
||||
Height = ratio * Width;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -114,8 +114,8 @@ namespace osu.Game.Rulesets.Mods
|
||||
[JsonIgnore]
|
||||
public virtual bool UserPlayable => true;
|
||||
|
||||
[Obsolete("Going forward, the concept of \"ranked\" doesn't exist. The only exceptions are automation mods, which should now override and set UserPlayable to true.")] // Can be removed 20211009
|
||||
public virtual bool IsRanked => false;
|
||||
[Obsolete("Going forward, the concept of \"ranked\" doesn't exist. The only exceptions are automation mods, which should now override and set UserPlayable to false.")] // Can be removed 20211009
|
||||
public virtual bool Ranked => false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this mod requires configuration to apply changes to the game.
|
||||
|
@ -0,0 +1,93 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Screens.Ranking.Expanded;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Components
|
||||
{
|
||||
public class StarRatingRangeDisplay : OnlinePlayComposite
|
||||
{
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
private StarRatingDisplay minDisplay;
|
||||
private Drawable minBackground;
|
||||
private StarRatingDisplay maxDisplay;
|
||||
private Drawable maxBackground;
|
||||
|
||||
public StarRatingRangeDisplay()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = 1,
|
||||
Children = new[]
|
||||
{
|
||||
minBackground = new Box
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(0.5f),
|
||||
},
|
||||
maxBackground = new Box
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(0.5f),
|
||||
},
|
||||
}
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
minDisplay = new StarRatingDisplay(default),
|
||||
maxDisplay = new StarRatingDisplay(default)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Playlist.BindCollectionChanged(updateRange, true);
|
||||
}
|
||||
|
||||
private void updateRange(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
var orderedDifficulties = Playlist.Select(p => p.Beatmap.Value).OrderBy(b => b.StarDifficulty).ToArray();
|
||||
|
||||
StarDifficulty minDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[0].StarDifficulty : 0, 0);
|
||||
StarDifficulty maxDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[^1].StarDifficulty : 0, 0);
|
||||
|
||||
minDisplay.Current.Value = minDifficulty;
|
||||
maxDisplay.Current.Value = maxDifficulty;
|
||||
|
||||
minBackground.Colour = colours.ForDifficultyRating(minDifficulty.DifficultyRating, true);
|
||||
maxBackground.Colour = colours.ForDifficultyRating(maxDifficulty.DifficultyRating, true);
|
||||
}
|
||||
}
|
||||
}
|
@ -93,7 +93,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
|
||||
{
|
||||
bool matchingFilter = true;
|
||||
|
||||
matchingFilter &= r.Room.Playlist.Count == 0 || r.Room.Playlist.Any(i => i.Ruleset.Value.Equals(criteria.Ruleset));
|
||||
matchingFilter &= r.Room.Playlist.Count == 0 || criteria.Ruleset == null || r.Room.Playlist.Any(i => i.Ruleset.Value.Equals(criteria.Ruleset));
|
||||
|
||||
if (!string.IsNullOrEmpty(criteria.SearchString))
|
||||
matchingFilter &= r.FilterTerms.Any(term => term.Contains(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
@ -34,7 +34,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
|
||||
public void Stop() => IsRunning = false;
|
||||
|
||||
public bool Seek(double position) => true;
|
||||
public bool Seek(double position)
|
||||
{
|
||||
CurrentTime = position;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ResetSpeedAdjustments()
|
||||
{
|
||||
|
@ -1,8 +1,11 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Timing;
|
||||
|
||||
@ -28,16 +31,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
/// </summary>
|
||||
public const double MAXIMUM_START_DELAY = 15000;
|
||||
|
||||
public event Action ReadyToStart;
|
||||
|
||||
/// <summary>
|
||||
/// The master clock which is used to control the timing of all player clocks clocks.
|
||||
/// </summary>
|
||||
public IAdjustableClock MasterClock { get; }
|
||||
|
||||
public IBindable<MasterClockState> MasterState => masterState;
|
||||
|
||||
/// <summary>
|
||||
/// The player clocks.
|
||||
/// </summary>
|
||||
private readonly List<ISpectatorPlayerClock> playerClocks = new List<ISpectatorPlayerClock>();
|
||||
|
||||
private readonly Bindable<MasterClockState> masterState = new Bindable<MasterClockState>();
|
||||
|
||||
private bool hasStarted;
|
||||
private double? firstStartAttemptTime;
|
||||
|
||||
@ -46,7 +55,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
MasterClock = master;
|
||||
}
|
||||
|
||||
public void AddPlayerClock(ISpectatorPlayerClock clock) => playerClocks.Add(clock);
|
||||
public void AddPlayerClock(ISpectatorPlayerClock clock)
|
||||
{
|
||||
Debug.Assert(!playerClocks.Contains(clock));
|
||||
playerClocks.Add(clock);
|
||||
}
|
||||
|
||||
public void RemovePlayerClock(ISpectatorPlayerClock clock) => playerClocks.Remove(clock);
|
||||
|
||||
@ -62,8 +75,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
return;
|
||||
}
|
||||
|
||||
updateCatchup();
|
||||
updateMasterClock();
|
||||
updatePlayerCatchup();
|
||||
updateMasterState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -81,14 +94,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
int readyCount = playerClocks.Count(s => !s.WaitingOnFrames.Value);
|
||||
|
||||
if (readyCount == playerClocks.Count)
|
||||
return hasStarted = true;
|
||||
return performStart();
|
||||
|
||||
if (readyCount > 0)
|
||||
{
|
||||
firstStartAttemptTime ??= Time.Current;
|
||||
|
||||
if (Time.Current - firstStartAttemptTime > MAXIMUM_START_DELAY)
|
||||
return hasStarted = true;
|
||||
return performStart();
|
||||
}
|
||||
|
||||
bool performStart()
|
||||
{
|
||||
ReadyToStart?.Invoke();
|
||||
return hasStarted = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -97,7 +116,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
/// <summary>
|
||||
/// Updates the catchup states of all player clocks clocks.
|
||||
/// </summary>
|
||||
private void updateCatchup()
|
||||
private void updatePlayerCatchup()
|
||||
{
|
||||
for (int i = 0; i < playerClocks.Count; i++)
|
||||
{
|
||||
@ -135,19 +154,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the master clock's running state.
|
||||
/// Updates the state of the master clock.
|
||||
/// </summary>
|
||||
private void updateMasterClock()
|
||||
private void updateMasterState()
|
||||
{
|
||||
bool anyInSync = playerClocks.Any(s => !s.IsCatchingUp);
|
||||
|
||||
if (MasterClock.IsRunning != anyInSync)
|
||||
{
|
||||
if (anyInSync)
|
||||
MasterClock.Start();
|
||||
else
|
||||
MasterClock.Stop();
|
||||
}
|
||||
masterState.Value = anyInSync ? MasterClockState.Synchronised : MasterClockState.TooFarAhead;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Timing;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
@ -10,11 +12,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
/// </summary>
|
||||
public interface ISyncManager
|
||||
{
|
||||
/// <summary>
|
||||
/// An event which is invoked when gameplay is ready to start.
|
||||
/// </summary>
|
||||
event Action ReadyToStart;
|
||||
|
||||
/// <summary>
|
||||
/// The master clock which player clocks should synchronise to.
|
||||
/// </summary>
|
||||
IAdjustableClock MasterClock { get; }
|
||||
|
||||
/// <summary>
|
||||
/// An event which is invoked when the state of <see cref="MasterClock"/> is changed.
|
||||
/// </summary>
|
||||
IBindable<MasterClockState> MasterState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds an <see cref="ISpectatorPlayerClock"/> to manage.
|
||||
/// </summary>
|
||||
|
@ -0,0 +1,18 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
public enum MasterClockState
|
||||
{
|
||||
/// <summary>
|
||||
/// The master clock is synchronised with at least one player clock.
|
||||
/// </summary>
|
||||
Synchronised,
|
||||
|
||||
/// <summary>
|
||||
/// The master clock is too far ahead of any player clock and needs to slow down.
|
||||
/// </summary>
|
||||
TooFarAhead
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
@ -42,6 +43,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
private PlayerGrid grid;
|
||||
private MultiSpectatorLeaderboard leaderboard;
|
||||
private PlayerArea currentAudioSource;
|
||||
private bool canStartMasterClock;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="MultiSpectatorScreen"/>.
|
||||
@ -100,15 +102,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
Expanded = { Value = true },
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
}, leaderboardContainer.Add);
|
||||
}, l =>
|
||||
{
|
||||
foreach (var instance in instances)
|
||||
leaderboard.AddClock(instance.UserId, instance.GameplayClock);
|
||||
|
||||
leaderboardContainer.Add(leaderboard);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
masterClockContainer.Stop();
|
||||
masterClockContainer.Reset();
|
||||
masterClockContainer.Stop();
|
||||
|
||||
syncManager.ReadyToStart += onReadyToStart;
|
||||
syncManager.MasterState.BindValueChanged(onMasterStateChanged, true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
@ -129,19 +140,45 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
private bool isCandidateAudioSource([CanBeNull] ISpectatorPlayerClock clock)
|
||||
=> clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames.Value;
|
||||
|
||||
private void onReadyToStart()
|
||||
{
|
||||
// Seek the master clock to the gameplay time.
|
||||
// This is chosen as the first available frame in the players' replays, which matches the seek by each individual SpectatorPlayer.
|
||||
var startTime = instances.Where(i => i.Score != null)
|
||||
.SelectMany(i => i.Score.Replay.Frames)
|
||||
.Select(f => f.Time)
|
||||
.DefaultIfEmpty(0)
|
||||
.Min();
|
||||
|
||||
masterClockContainer.Seek(startTime);
|
||||
masterClockContainer.Start();
|
||||
|
||||
// Although the clock has been started, this flag is set to allow for later synchronisation state changes to also be able to start it.
|
||||
canStartMasterClock = true;
|
||||
}
|
||||
|
||||
private void onMasterStateChanged(ValueChangedEvent<MasterClockState> state)
|
||||
{
|
||||
switch (state.NewValue)
|
||||
{
|
||||
case MasterClockState.Synchronised:
|
||||
if (canStartMasterClock)
|
||||
masterClockContainer.Start();
|
||||
|
||||
break;
|
||||
|
||||
case MasterClockState.TooFarAhead:
|
||||
masterClockContainer.Stop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnUserStateChanged(int userId, SpectatorState spectatorState)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void StartGameplay(int userId, GameplayState gameplayState)
|
||||
{
|
||||
var instance = instances.Single(i => i.UserId == userId);
|
||||
|
||||
instance.LoadScore(gameplayState.Score);
|
||||
|
||||
syncManager.AddPlayerClock(instance.GameplayClock);
|
||||
leaderboard.AddClock(instance.UserId, instance.GameplayClock);
|
||||
}
|
||||
=> instances.Single(i => i.UserId == userId).LoadScore(gameplayState.Score);
|
||||
|
||||
protected override void EndGameplay(int userId)
|
||||
{
|
||||
|
@ -27,7 +27,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
/// <summary>
|
||||
/// Whether a <see cref="Player"/> is loaded in the area.
|
||||
/// </summary>
|
||||
public bool PlayerLoaded => stack?.CurrentScreen is Player;
|
||||
public bool PlayerLoaded => (stack?.CurrentScreen as Player)?.IsLoaded == true;
|
||||
|
||||
/// <summary>
|
||||
/// The user id this <see cref="PlayerArea"/> corresponds to.
|
||||
|
@ -768,6 +768,7 @@ namespace osu.Game.Screens.Play
|
||||
return false;
|
||||
|
||||
HasFailed = true;
|
||||
Score.ScoreInfo.Passed = false;
|
||||
|
||||
// There is a chance that we could be in a paused state as the ruleset's internal clock (see FrameStabilityContainer)
|
||||
// could process an extra frame after the GameplayClock is stopped.
|
||||
@ -950,6 +951,10 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
screenSuspension?.Expire();
|
||||
|
||||
// if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap.
|
||||
if (prepareScoreForDisplayTask == null)
|
||||
Score.ScoreInfo.Passed = false;
|
||||
|
||||
// EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous.
|
||||
// To resolve test failures, forcefully end playing synchronously when this screen exits.
|
||||
// Todo: Replace this with a more permanent solution once osu-framework has a synchronous cleanup method.
|
||||
|
@ -12,6 +12,16 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
public class SoloPlayer : SubmittingPlayer
|
||||
{
|
||||
public SoloPlayer()
|
||||
: this(null)
|
||||
{
|
||||
}
|
||||
|
||||
protected SoloPlayer(PlayerConfiguration configuration = null)
|
||||
: base(configuration)
|
||||
{
|
||||
}
|
||||
|
||||
protected override APIRequest<APIScoreToken> CreateTokenRequest()
|
||||
{
|
||||
if (!(Beatmap.Value.BeatmapInfo.OnlineBeatmapID is int beatmapId))
|
||||
@ -27,9 +37,11 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
protected override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token)
|
||||
{
|
||||
Debug.Assert(Beatmap.Value.BeatmapInfo.OnlineBeatmapID != null);
|
||||
var beatmap = score.ScoreInfo.Beatmap;
|
||||
|
||||
int beatmapId = Beatmap.Value.BeatmapInfo.OnlineBeatmapID.Value;
|
||||
Debug.Assert(beatmap.OnlineBeatmapID != null);
|
||||
|
||||
int beatmapId = beatmap.OnlineBeatmapID.Value;
|
||||
|
||||
return new SubmitSoloScoreRequest(beatmapId, token, score.ScoreInfo);
|
||||
}
|
||||
|
@ -47,8 +47,9 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
base.StartGameplay();
|
||||
|
||||
// Start gameplay along with the very first arrival frame (the latest one).
|
||||
score.Replay.Frames.Clear();
|
||||
spectatorClient.OnNewFrames += userSentFrames;
|
||||
seekToGameplay();
|
||||
}
|
||||
|
||||
private void userSentFrames(int userId, FrameDataBundle bundle)
|
||||
@ -62,6 +63,8 @@ namespace osu.Game.Screens.Play
|
||||
if (!this.IsCurrentScreen())
|
||||
return;
|
||||
|
||||
bool isFirstBundle = score.Replay.Frames.Count == 0;
|
||||
|
||||
foreach (var frame in bundle.Frames)
|
||||
{
|
||||
IConvertibleReplayFrame convertibleFrame = GameplayRuleset.CreateConvertibleReplayFrame();
|
||||
@ -73,19 +76,8 @@ namespace osu.Game.Screens.Play
|
||||
score.Replay.Frames.Add(convertedFrame);
|
||||
}
|
||||
|
||||
seekToGameplay();
|
||||
}
|
||||
|
||||
private bool seekedToGameplay;
|
||||
|
||||
private void seekToGameplay()
|
||||
{
|
||||
if (seekedToGameplay || score.Replay.Frames.Count == 0)
|
||||
return;
|
||||
|
||||
NonFrameStableSeek(score.Replay.Frames[0].Time);
|
||||
|
||||
seekedToGameplay = true;
|
||||
if (isFirstBundle && score.Replay.Frames.Count > 0)
|
||||
NonFrameStableSeek(score.Replay.Frames[0].Time);
|
||||
}
|
||||
|
||||
protected override Score CreateScore() => score;
|
||||
|
@ -27,6 +27,8 @@ namespace osu.Game.Screens.Play
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
private TaskCompletionSource<bool> scoreSubmissionSource;
|
||||
|
||||
protected SubmittingPlayer(PlayerConfiguration configuration = null)
|
||||
: base(configuration)
|
||||
{
|
||||
@ -106,27 +108,16 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);
|
||||
|
||||
// token may be null if the request failed but gameplay was still allowed (see HandleTokenRetrievalFailure).
|
||||
if (token == null)
|
||||
return;
|
||||
await submitScore(score).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
var request = CreateSubmissionRequest(score, token.Value);
|
||||
public override bool OnExiting(IScreen next)
|
||||
{
|
||||
var exiting = base.OnExiting(next);
|
||||
|
||||
request.Success += s =>
|
||||
{
|
||||
score.ScoreInfo.OnlineScoreID = s.ID;
|
||||
tcs.SetResult(true);
|
||||
};
|
||||
submitScore(Score);
|
||||
|
||||
request.Failure += e =>
|
||||
{
|
||||
Logger.Error(e, "Failed to submit score");
|
||||
tcs.SetResult(false);
|
||||
};
|
||||
|
||||
api.Queue(request);
|
||||
await tcs.Task.ConfigureAwait(false);
|
||||
return exiting;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -143,5 +134,33 @@ namespace osu.Game.Screens.Play
|
||||
/// <param name="score">The score to be submitted.</param>
|
||||
/// <param name="token">The submission token.</param>
|
||||
protected abstract APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token);
|
||||
|
||||
private Task submitScore(Score score)
|
||||
{
|
||||
// token may be null if the request failed but gameplay was still allowed (see HandleTokenRetrievalFailure).
|
||||
if (token == null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (scoreSubmissionSource != null)
|
||||
return scoreSubmissionSource.Task;
|
||||
|
||||
scoreSubmissionSource = new TaskCompletionSource<bool>();
|
||||
var request = CreateSubmissionRequest(score, token.Value);
|
||||
|
||||
request.Success += s =>
|
||||
{
|
||||
score.ScoreInfo.OnlineScoreID = s.ID;
|
||||
scoreSubmissionSource.SetResult(true);
|
||||
};
|
||||
|
||||
request.Failure += e =>
|
||||
{
|
||||
Logger.Error(e, "Failed to submit score");
|
||||
scoreSubmissionSource.SetResult(false);
|
||||
};
|
||||
|
||||
api.Queue(request);
|
||||
return scoreSubmissionSource.Task;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,7 @@
|
||||
using System.Globalization;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
@ -111,12 +109,9 @@ namespace osu.Game.Screens.Ranking.Expanded
|
||||
|
||||
var rating = Current.Value.DifficultyRating;
|
||||
|
||||
background.Colour = rating == DifficultyRating.ExpertPlus
|
||||
? ColourInfo.GradientVertical(Color4Extensions.FromHex("#C1C1C1"), Color4Extensions.FromHex("#595959"))
|
||||
: (ColourInfo)colours.ForDifficultyRating(rating);
|
||||
background.Colour = colours.ForDifficultyRating(rating, true);
|
||||
|
||||
textFlow.Clear();
|
||||
|
||||
textFlow.AddText($"{wholePart}", s =>
|
||||
{
|
||||
s.Colour = Color4.Black;
|
||||
|
@ -127,6 +127,7 @@ namespace osu.Game.Skinning.Editor
|
||||
public override bool HandleFlip(Direction direction)
|
||||
{
|
||||
var selectionQuad = getSelectionQuad();
|
||||
Vector2 scaleFactor = direction == Direction.Horizontal ? new Vector2(-1, 1) : new Vector2(1, -1);
|
||||
|
||||
foreach (var b in SelectedBlueprints)
|
||||
{
|
||||
@ -136,10 +137,8 @@ namespace osu.Game.Skinning.Editor
|
||||
|
||||
updateDrawablePosition(drawableItem, flippedPosition);
|
||||
|
||||
drawableItem.Scale *= new Vector2(
|
||||
direction == Direction.Horizontal ? -1 : 1,
|
||||
direction == Direction.Vertical ? -1 : 1
|
||||
);
|
||||
drawableItem.Scale *= scaleFactor;
|
||||
drawableItem.Rotation -= drawableItem.Rotation % 180 * 2;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
57
osu.Game/Skinning/ResourceStoreBackedSkin.cs
Normal file
57
osu.Game/Skinning/ResourceStoreBackedSkin.cs
Normal file
@ -0,0 +1,57 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.OpenGL.Textures;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Audio;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// An <see cref="ISkin"/> that uses an underlying <see cref="IResourceStore{T}"/> with namespaces for resources retrieval.
|
||||
/// </summary>
|
||||
public class ResourceStoreBackedSkin : ISkin, IDisposable
|
||||
{
|
||||
private readonly TextureStore textures;
|
||||
private readonly ISampleStore samples;
|
||||
|
||||
public ResourceStoreBackedSkin(IResourceStore<byte[]> resources, GameHost host, AudioManager audio)
|
||||
{
|
||||
textures = new TextureStore(host.CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(resources, @"Textures")));
|
||||
samples = audio.GetSampleStore(new NamespacedResourceStore<byte[]>(resources, @"Samples"));
|
||||
}
|
||||
|
||||
public Drawable? GetDrawableComponent(ISkinComponent component) => null;
|
||||
|
||||
public Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => textures.Get(componentName, wrapModeS, wrapModeT);
|
||||
|
||||
public ISample? GetSample(ISampleInfo sampleInfo)
|
||||
{
|
||||
foreach (var lookup in sampleInfo.LookupNames)
|
||||
{
|
||||
ISample? sample = samples.Get(lookup);
|
||||
if (sample != null)
|
||||
return sample;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup) => null;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
textures.Dispose();
|
||||
samples.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,14 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@ -44,11 +48,16 @@ namespace osu.Game.Skinning
|
||||
|
||||
private ISkinSource parentSource;
|
||||
|
||||
private ResourceStoreBackedSkin rulesetResourcesSkin;
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
parentSource = parent.Get<ISkinSource>();
|
||||
parentSource.SourceChanged += OnSourceChanged;
|
||||
|
||||
if (Ruleset.CreateResourceStore() is IResourceStore<byte[]> resources)
|
||||
rulesetResourcesSkin = new ResourceStoreBackedSkin(resources, parent.Get<GameHost>(), parent.Get<AudioManager>());
|
||||
|
||||
// ensure sources are populated and ready for use before childrens' asynchronous load flow.
|
||||
UpdateSkinSources();
|
||||
|
||||
@ -78,6 +87,16 @@ namespace osu.Game.Skinning
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int lastDefaultSkinIndex = SkinSources.IndexOf(SkinSources.OfType<DefaultSkin>().LastOrDefault());
|
||||
|
||||
// Ruleset resources should be given the ability to override game-wide defaults
|
||||
// This is achieved by placing them before the last instance of DefaultSkin.
|
||||
// Note that DefaultSkin may not be present in some test scenes.
|
||||
if (lastDefaultSkinIndex >= 0)
|
||||
SkinSources.Insert(lastDefaultSkinIndex, rulesetResourcesSkin);
|
||||
else
|
||||
SkinSources.Add(rulesetResourcesSkin);
|
||||
}
|
||||
|
||||
protected ISkin GetLegacyRulesetTransformedSkin(ISkin legacySkin)
|
||||
@ -98,6 +117,8 @@ namespace osu.Game.Skinning
|
||||
|
||||
if (parentSource != null)
|
||||
parentSource.SourceChanged -= OnSourceChanged;
|
||||
|
||||
rulesetResourcesSkin?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,37 @@
|
||||
// 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.Database;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Tests.Visual.OnlinePlay;
|
||||
using osu.Game.Tests.Visual.Spectator;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface that defines the dependencies required for multiplayer test scenes.
|
||||
/// </summary>
|
||||
public interface IMultiplayerTestSceneDependencies : IOnlinePlayTestSceneDependencies
|
||||
{
|
||||
/// <summary>
|
||||
/// The cached <see cref="MultiplayerClient"/>.
|
||||
/// </summary>
|
||||
TestMultiplayerClient Client { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The cached <see cref="IRoomManager"/>.
|
||||
/// </summary>
|
||||
new TestMultiplayerRoomManager RoomManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The cached <see cref="UserLookupCache"/>.
|
||||
/// </summary>
|
||||
TestUserLookupCache LookupCache { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The cached <see cref="osu.Game.Online.Spectator.SpectatorClient"/>.
|
||||
/// </summary>
|
||||
TestSpectatorClient SpectatorClient { get; }
|
||||
}
|
||||
}
|
@ -2,66 +2,55 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge.Components;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Visual.OnlinePlay;
|
||||
using osu.Game.Tests.Visual.Spectator;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public abstract class MultiplayerTestScene : RoomTestScene
|
||||
/// <summary>
|
||||
/// The base test scene for all multiplayer components and screens.
|
||||
/// </summary>
|
||||
public abstract class MultiplayerTestScene : OnlinePlayTestScene, IMultiplayerTestSceneDependencies
|
||||
{
|
||||
public const int PLAYER_1_ID = 55;
|
||||
public const int PLAYER_2_ID = 56;
|
||||
|
||||
[Cached(typeof(MultiplayerClient))]
|
||||
public TestMultiplayerClient Client { get; }
|
||||
public TestMultiplayerClient Client => OnlinePlayDependencies.Client;
|
||||
public new TestMultiplayerRoomManager RoomManager => OnlinePlayDependencies.RoomManager;
|
||||
public TestUserLookupCache LookupCache => OnlinePlayDependencies?.LookupCache;
|
||||
public TestSpectatorClient SpectatorClient => OnlinePlayDependencies?.SpectatorClient;
|
||||
|
||||
[Cached(typeof(IRoomManager))]
|
||||
public TestMultiplayerRoomManager RoomManager { get; }
|
||||
|
||||
[Cached]
|
||||
public Bindable<FilterCriteria> Filter { get; }
|
||||
|
||||
[Cached]
|
||||
public OngoingOperationTracker OngoingOperationTracker { get; }
|
||||
|
||||
protected override Container<Drawable> Content => content;
|
||||
private readonly TestMultiplayerRoomContainer content;
|
||||
protected new MultiplayerTestSceneDependencies OnlinePlayDependencies => (MultiplayerTestSceneDependencies)base.OnlinePlayDependencies;
|
||||
|
||||
private readonly bool joinRoom;
|
||||
|
||||
protected MultiplayerTestScene(bool joinRoom = true)
|
||||
{
|
||||
this.joinRoom = joinRoom;
|
||||
base.Content.Add(content = new TestMultiplayerRoomContainer { RelativeSizeAxes = Axes.Both });
|
||||
|
||||
Client = content.Client;
|
||||
RoomManager = content.RoomManager;
|
||||
Filter = content.Filter;
|
||||
OngoingOperationTracker = content.OngoingOperationTracker;
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public new void Setup() => Schedule(() =>
|
||||
{
|
||||
RoomManager.Schedule(() => RoomManager.PartRoom());
|
||||
|
||||
if (joinRoom)
|
||||
{
|
||||
Room.Name.Value = "test name";
|
||||
Room.Playlist.Add(new PlaylistItem
|
||||
var room = new Room
|
||||
{
|
||||
Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo },
|
||||
Ruleset = { Value = Ruleset.Value }
|
||||
});
|
||||
Name = { Value = "test name" },
|
||||
Playlist =
|
||||
{
|
||||
new PlaylistItem
|
||||
{
|
||||
Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo },
|
||||
Ruleset = { Value = Ruleset.Value }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
RoomManager.Schedule(() => RoomManager.CreateRoom(Room));
|
||||
RoomManager.CreateRoom(room);
|
||||
SelectedRoom.Value = room;
|
||||
}
|
||||
});
|
||||
|
||||
@ -72,5 +61,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
if (joinRoom)
|
||||
AddUntilStep("wait for room join", () => Client.Room != null);
|
||||
}
|
||||
|
||||
protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,38 @@
|
||||
// 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.Database;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Spectator;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Tests.Visual.OnlinePlay;
|
||||
using osu.Game.Tests.Visual.Spectator;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains the basic dependencies of multiplayer test scenes.
|
||||
/// </summary>
|
||||
public class MultiplayerTestSceneDependencies : OnlinePlayTestSceneDependencies, IMultiplayerTestSceneDependencies
|
||||
{
|
||||
public TestMultiplayerClient Client { get; }
|
||||
public TestUserLookupCache LookupCache { get; }
|
||||
public TestSpectatorClient SpectatorClient { get; }
|
||||
public new TestMultiplayerRoomManager RoomManager => (TestMultiplayerRoomManager)base.RoomManager;
|
||||
|
||||
public MultiplayerTestSceneDependencies()
|
||||
{
|
||||
Client = new TestMultiplayerClient(RoomManager);
|
||||
LookupCache = new TestUserLookupCache();
|
||||
SpectatorClient = CreateSpectatorClient();
|
||||
|
||||
CacheAs<MultiplayerClient>(Client);
|
||||
CacheAs<UserLookupCache>(LookupCache);
|
||||
CacheAs<SpectatorClient>(SpectatorClient);
|
||||
}
|
||||
|
||||
protected override IRoomManager CreateRoomManager() => new TestMultiplayerRoomManager();
|
||||
|
||||
protected virtual TestSpectatorClient CreateSpectatorClient() => new TestSpectatorClient();
|
||||
}
|
||||
}
|
@ -20,6 +20,9 @@ using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="MultiplayerClient"/> for use in multiplayer test scenes. Should generally not be used by itself outside of a <see cref="MultiplayerTestScene"/>.
|
||||
/// </summary>
|
||||
public class TestMultiplayerClient : MultiplayerClient
|
||||
{
|
||||
public override IBindable<bool> IsConnected => isConnected;
|
||||
|
@ -1,48 +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.Game.Online.Multiplayer;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge.Components;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public class TestMultiplayerRoomContainer : Container
|
||||
{
|
||||
protected override Container<Drawable> Content => content;
|
||||
private readonly Container content;
|
||||
|
||||
[Cached(typeof(MultiplayerClient))]
|
||||
public readonly TestMultiplayerClient Client;
|
||||
|
||||
[Cached(typeof(IRoomManager))]
|
||||
public readonly TestMultiplayerRoomManager RoomManager;
|
||||
|
||||
[Cached]
|
||||
public readonly Bindable<FilterCriteria> Filter = new Bindable<FilterCriteria>(new FilterCriteria());
|
||||
|
||||
[Cached]
|
||||
public readonly OngoingOperationTracker OngoingOperationTracker;
|
||||
|
||||
public TestMultiplayerRoomContainer()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
RoomManager = new TestMultiplayerRoomManager();
|
||||
Client = new TestMultiplayerClient(RoomManager);
|
||||
OngoingOperationTracker = new OngoingOperationTracker();
|
||||
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
Client,
|
||||
RoomManager,
|
||||
OngoingOperationTracker,
|
||||
content = new Container { RelativeSizeAxes = Axes.Both }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -11,11 +11,15 @@ using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.OnlinePlay.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="RoomManager"/> for use in multiplayer test scenes. Should generally not be used by itself outside of a <see cref="MultiplayerTestScene"/>.
|
||||
/// </summary>
|
||||
public class TestMultiplayerRoomManager : MultiplayerRoomManager
|
||||
{
|
||||
[Resolved]
|
||||
@ -29,10 +33,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
public new readonly List<Room> Rooms = new List<Room>();
|
||||
|
||||
protected override void LoadComplete()
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
int currentScoreId = 0;
|
||||
int currentRoomId = 0;
|
||||
int currentPlaylistItemId = 0;
|
||||
|
78
osu.Game/Tests/Visual/OnlinePlay/BasicTestRoomManager.cs
Normal file
78
osu.Game/Tests/Visual/OnlinePlay/BasicTestRoomManager.cs
Normal file
@ -0,0 +1,78 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Screens.OnlinePlay.Components;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Tests.Visual.OnlinePlay
|
||||
{
|
||||
/// <summary>
|
||||
/// A very simple <see cref="RoomManager"/> for use in online play test scenes.
|
||||
/// </summary>
|
||||
public class BasicTestRoomManager : IRoomManager
|
||||
{
|
||||
public event Action RoomsUpdated
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public readonly BindableList<Room> Rooms = new BindableList<Room>();
|
||||
|
||||
public IBindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>(true);
|
||||
|
||||
IBindableList<Room> IRoomManager.Rooms => Rooms;
|
||||
|
||||
public void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
|
||||
{
|
||||
room.RoomID.Value ??= Rooms.Select(r => r.RoomID.Value).Where(id => id != null).Select(id => id.Value).DefaultIfEmpty().Max() + 1;
|
||||
Rooms.Add(room);
|
||||
onSuccess?.Invoke(room);
|
||||
}
|
||||
|
||||
public void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null) => onSuccess?.Invoke(room);
|
||||
|
||||
public void PartRoom()
|
||||
{
|
||||
}
|
||||
|
||||
public void AddRooms(int count, RulesetInfo ruleset = null)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var room = new Room
|
||||
{
|
||||
RoomID = { Value = i },
|
||||
Name = { Value = $"Room {i}" },
|
||||
Host = { Value = new User { Username = "Host" } },
|
||||
EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) },
|
||||
Category = { Value = i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal }
|
||||
};
|
||||
|
||||
if (ruleset != null)
|
||||
{
|
||||
room.Playlist.Add(new PlaylistItem
|
||||
{
|
||||
Ruleset = { Value = ruleset },
|
||||
Beatmap =
|
||||
{
|
||||
Value = new BeatmapInfo
|
||||
{
|
||||
Metadata = new BeatmapMetadata()
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
CreateRoom(room);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge.Components;
|
||||
|
||||
namespace osu.Game.Tests.Visual.OnlinePlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface that defines the dependencies required for online play test scenes.
|
||||
/// </summary>
|
||||
public interface IOnlinePlayTestSceneDependencies
|
||||
{
|
||||
/// <summary>
|
||||
/// The cached <see cref="Room"/>.
|
||||
/// </summary>
|
||||
Bindable<Room> SelectedRoom { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The cached <see cref="IRoomManager"/>
|
||||
/// </summary>
|
||||
IRoomManager RoomManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The cached <see cref="FilterCriteria"/>.
|
||||
/// </summary>
|
||||
Bindable<FilterCriteria> Filter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The cached <see cref="OngoingOperationTracker"/>.
|
||||
/// </summary>
|
||||
OngoingOperationTracker OngoingOperationTracker { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The cached <see cref="OnlinePlayBeatmapAvailabilityTracker"/>.
|
||||
/// </summary>
|
||||
OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; }
|
||||
}
|
||||
}
|
104
osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs
Normal file
104
osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs
Normal file
@ -0,0 +1,104 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge.Components;
|
||||
|
||||
namespace osu.Game.Tests.Visual.OnlinePlay
|
||||
{
|
||||
/// <summary>
|
||||
/// A base test scene for all online play components and screens.
|
||||
/// </summary>
|
||||
public abstract class OnlinePlayTestScene : ScreenTestScene, IOnlinePlayTestSceneDependencies
|
||||
{
|
||||
public Bindable<Room> SelectedRoom => OnlinePlayDependencies?.SelectedRoom;
|
||||
public IRoomManager RoomManager => OnlinePlayDependencies?.RoomManager;
|
||||
public Bindable<FilterCriteria> Filter => OnlinePlayDependencies?.Filter;
|
||||
public OngoingOperationTracker OngoingOperationTracker => OnlinePlayDependencies?.OngoingOperationTracker;
|
||||
public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker => OnlinePlayDependencies?.AvailabilityTracker;
|
||||
|
||||
/// <summary>
|
||||
/// All dependencies required for online play components and screens.
|
||||
/// </summary>
|
||||
protected OnlinePlayTestSceneDependencies OnlinePlayDependencies => dependencies?.OnlinePlayDependencies;
|
||||
|
||||
private DelegatedDependencyContainer dependencies;
|
||||
|
||||
protected override Container<Drawable> Content => content;
|
||||
private readonly Container content;
|
||||
private readonly Container drawableDependenciesContainer;
|
||||
|
||||
protected OnlinePlayTestScene()
|
||||
{
|
||||
base.Content.AddRange(new Drawable[]
|
||||
{
|
||||
drawableDependenciesContainer = new Container { RelativeSizeAxes = Axes.Both },
|
||||
content = new Container { RelativeSizeAxes = Axes.Both },
|
||||
});
|
||||
}
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
dependencies = new DelegatedDependencyContainer(base.CreateChildDependencies(parent));
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
// Reset the room dependencies to a fresh state.
|
||||
drawableDependenciesContainer.Clear();
|
||||
dependencies.OnlinePlayDependencies = CreateOnlinePlayDependencies();
|
||||
drawableDependenciesContainer.AddRange(OnlinePlayDependencies.DrawableComponents);
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Creates the room dependencies. Called every <see cref="Setup"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Any custom dependencies required for online play sub-classes should be added here.
|
||||
/// </remarks>
|
||||
protected virtual OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new OnlinePlayTestSceneDependencies();
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="IReadOnlyDependencyContainer"/> providing a mutable lookup source for online play dependencies.
|
||||
/// </summary>
|
||||
private class DelegatedDependencyContainer : IReadOnlyDependencyContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// The online play dependencies.
|
||||
/// </summary>
|
||||
public OnlinePlayTestSceneDependencies OnlinePlayDependencies { get; set; }
|
||||
|
||||
private readonly IReadOnlyDependencyContainer parent;
|
||||
private readonly DependencyContainer injectableDependencies;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="DelegatedDependencyContainer"/>.
|
||||
/// </summary>
|
||||
/// <param name="parent">The fallback <see cref="IReadOnlyDependencyContainer"/> to use when <see cref="OnlinePlayDependencies"/> cannot satisfy a dependency.</param>
|
||||
public DelegatedDependencyContainer(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
this.parent = parent;
|
||||
injectableDependencies = new DependencyContainer(this);
|
||||
}
|
||||
|
||||
public object Get(Type type)
|
||||
=> OnlinePlayDependencies?.Get(type) ?? parent.Get(type);
|
||||
|
||||
public object Get(Type type, CacheInfo info)
|
||||
=> OnlinePlayDependencies?.Get(type, info) ?? parent.Get(type, info);
|
||||
|
||||
public void Inject<T>(T instance)
|
||||
where T : class
|
||||
=> injectableDependencies.Inject(instance);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge.Components;
|
||||
|
||||
namespace osu.Game.Tests.Visual.OnlinePlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains the basic dependencies of online play test scenes.
|
||||
/// </summary>
|
||||
public class OnlinePlayTestSceneDependencies : IReadOnlyDependencyContainer, IOnlinePlayTestSceneDependencies
|
||||
{
|
||||
public Bindable<Room> SelectedRoom { get; }
|
||||
public IRoomManager RoomManager { get; }
|
||||
public Bindable<FilterCriteria> Filter { get; }
|
||||
public OngoingOperationTracker OngoingOperationTracker { get; }
|
||||
public OnlinePlayBeatmapAvailabilityTracker AvailabilityTracker { get; }
|
||||
|
||||
/// <summary>
|
||||
/// All cached dependencies which are also <see cref="Drawable"/> components.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Drawable> DrawableComponents => drawableComponents;
|
||||
|
||||
private readonly List<Drawable> drawableComponents = new List<Drawable>();
|
||||
private readonly DependencyContainer dependencies;
|
||||
|
||||
public OnlinePlayTestSceneDependencies()
|
||||
{
|
||||
SelectedRoom = new Bindable<Room>();
|
||||
RoomManager = CreateRoomManager();
|
||||
Filter = new Bindable<FilterCriteria>(new FilterCriteria());
|
||||
OngoingOperationTracker = new OngoingOperationTracker();
|
||||
AvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker();
|
||||
|
||||
dependencies = new DependencyContainer(new CachedModelDependencyContainer<Room>(null) { Model = { BindTarget = SelectedRoom } });
|
||||
|
||||
CacheAs(SelectedRoom);
|
||||
CacheAs(RoomManager);
|
||||
CacheAs(Filter);
|
||||
CacheAs(OngoingOperationTracker);
|
||||
CacheAs(AvailabilityTracker);
|
||||
}
|
||||
|
||||
public object Get(Type type)
|
||||
=> dependencies.Get(type);
|
||||
|
||||
public object Get(Type type, CacheInfo info)
|
||||
=> dependencies.Get(type, info);
|
||||
|
||||
public void Inject<T>(T instance)
|
||||
where T : class
|
||||
=> dependencies.Inject(instance);
|
||||
|
||||
protected void Cache(object instance)
|
||||
{
|
||||
dependencies.Cache(instance);
|
||||
if (instance is Drawable drawable)
|
||||
drawableComponents.Add(drawable);
|
||||
}
|
||||
|
||||
protected void CacheAs<T>(T instance)
|
||||
where T : class
|
||||
{
|
||||
dependencies.CacheAs(instance);
|
||||
if (instance is Drawable drawable)
|
||||
drawableComponents.Add(drawable);
|
||||
}
|
||||
|
||||
protected virtual IRoomManager CreateRoomManager() => new BasicTestRoomManager();
|
||||
}
|
||||
}
|
@ -1,33 +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 NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Online.Rooms;
|
||||
|
||||
namespace osu.Game.Tests.Visual
|
||||
{
|
||||
public abstract class RoomTestScene : ScreenTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly Bindable<Room> currentRoom = new Bindable<Room>();
|
||||
|
||||
protected Room Room => currentRoom.Value;
|
||||
|
||||
private CachedModelDependencyContainer<Room> dependencies;
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
dependencies = new CachedModelDependencyContainer<Room>(base.CreateChildDependencies(parent));
|
||||
dependencies.Model.BindTo(currentRoom);
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
currentRoom.Value = new Room();
|
||||
});
|
||||
}
|
||||
}
|
@ -20,9 +20,15 @@ namespace osu.Game.Tests.Visual.Spectator
|
||||
{
|
||||
public class TestSpectatorClient : SpectatorClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Maximum number of frames sent per bundle via <see cref="SendFrames"/>.
|
||||
/// </summary>
|
||||
public const int FRAME_BUNDLE_SIZE = 10;
|
||||
|
||||
public override IBindable<bool> IsConnected { get; } = new Bindable<bool>(true);
|
||||
|
||||
private readonly Dictionary<int, int> userBeatmapDictionary = new Dictionary<int, int>();
|
||||
private readonly Dictionary<int, int> userNextFrameDictionary = new Dictionary<int, int>();
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
@ -35,6 +41,7 @@ namespace osu.Game.Tests.Visual.Spectator
|
||||
public void StartPlay(int userId, int beatmapId)
|
||||
{
|
||||
userBeatmapDictionary[userId] = beatmapId;
|
||||
userNextFrameDictionary[userId] = 0;
|
||||
sendPlayingState(userId);
|
||||
}
|
||||
|
||||
@ -57,24 +64,41 @@ namespace osu.Game.Tests.Visual.Spectator
|
||||
public new void Schedule(Action action) => base.Schedule(action);
|
||||
|
||||
/// <summary>
|
||||
/// Sends frames for an arbitrary user.
|
||||
/// Sends frames for an arbitrary user, in bundles containing 10 frames each.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user to send frames for.</param>
|
||||
/// <param name="index">The frame index.</param>
|
||||
/// <param name="count">The number of frames to send.</param>
|
||||
public void SendFrames(int userId, int index, int count)
|
||||
/// <param name="count">The total number of frames to send.</param>
|
||||
public void SendFrames(int userId, int count)
|
||||
{
|
||||
var frames = new List<LegacyReplayFrame>();
|
||||
|
||||
for (int i = index; i < index + count; i++)
|
||||
{
|
||||
var buttonState = i == index + count - 1 ? ReplayButtonState.None : ReplayButtonState.Left1;
|
||||
int currentFrameIndex = userNextFrameDictionary[userId];
|
||||
int lastFrameIndex = currentFrameIndex + count - 1;
|
||||
|
||||
frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState));
|
||||
for (; currentFrameIndex <= lastFrameIndex; currentFrameIndex++)
|
||||
{
|
||||
// This is done in the next frame so that currentFrameIndex is updated to the correct value.
|
||||
if (frames.Count == FRAME_BUNDLE_SIZE)
|
||||
flush();
|
||||
|
||||
var buttonState = currentFrameIndex == lastFrameIndex ? ReplayButtonState.None : ReplayButtonState.Left1;
|
||||
frames.Add(new LegacyReplayFrame(currentFrameIndex * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState));
|
||||
}
|
||||
|
||||
var bundle = new FrameDataBundle(new ScoreInfo { Combo = index + count }, frames);
|
||||
((ISpectatorClient)this).UserSentFrames(userId, bundle);
|
||||
flush();
|
||||
|
||||
userNextFrameDictionary[userId] = currentFrameIndex;
|
||||
|
||||
void flush()
|
||||
{
|
||||
if (frames.Count == 0)
|
||||
return;
|
||||
|
||||
var bundle = new FrameDataBundle(new ScoreInfo { Combo = currentFrameIndex }, frames.ToArray());
|
||||
((ISpectatorClient)this).UserSentFrames(userId, bundle);
|
||||
|
||||
frames.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
protected override Task BeginPlayingInternal(SpectatorState state)
|
||||
|
@ -1,14 +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 System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Tests.Visual
|
||||
@ -16,7 +20,7 @@ namespace osu.Game.Tests.Visual
|
||||
/// <summary>
|
||||
/// A player that exposes many components that would otherwise not be available, for testing purposes.
|
||||
/// </summary>
|
||||
public class TestPlayer : Player
|
||||
public class TestPlayer : SoloPlayer
|
||||
{
|
||||
protected override bool PauseOnFocusLost { get; }
|
||||
|
||||
@ -35,6 +39,10 @@ namespace osu.Game.Tests.Visual
|
||||
|
||||
public new HealthProcessor HealthProcessor => base.HealthProcessor;
|
||||
|
||||
public bool TokenCreationRequested { get; private set; }
|
||||
|
||||
public Score SubmittedScore { get; private set; }
|
||||
|
||||
public new bool PauseCooldownActive => base.PauseCooldownActive;
|
||||
|
||||
public readonly List<JudgementResult> Results = new List<JudgementResult>();
|
||||
@ -49,6 +57,20 @@ namespace osu.Game.Tests.Visual
|
||||
PauseOnFocusLost = pauseOnFocusLost;
|
||||
}
|
||||
|
||||
protected override bool HandleTokenRetrievalFailure(Exception exception) => false;
|
||||
|
||||
protected override APIRequest<APIScoreToken> CreateTokenRequest()
|
||||
{
|
||||
TokenCreationRequested = true;
|
||||
return base.CreateTokenRequest();
|
||||
}
|
||||
|
||||
protected override APIRequest<MultiplayerScore> CreateSubmissionRequest(Score score, long token)
|
||||
{
|
||||
SubmittedScore = score;
|
||||
return base.CreateSubmissionRequest(score, token);
|
||||
}
|
||||
|
||||
protected override void PrepareReplay()
|
||||
{
|
||||
// Generally, replay generation is handled by whatever is constructing the player.
|
||||
|
19
osu.Game/Tests/Visual/TestUserLookupCache.cs
Normal file
19
osu.Game/Tests/Visual/TestUserLookupCache.cs
Normal file
@ -0,0 +1,19 @@
|
||||
// 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;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Tests.Visual
|
||||
{
|
||||
public class TestUserLookupCache : UserLookupCache
|
||||
{
|
||||
protected override Task<User> ComputeValueAsync(int lookup, CancellationToken token = default) => Task.FromResult(new User
|
||||
{
|
||||
Id = lookup,
|
||||
Username = $"User {lookup}"
|
||||
});
|
||||
}
|
||||
}
|
@ -20,26 +20,26 @@
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="AutoMapper" Version="10.1.1" />
|
||||
<PackageReference Include="DiffPlex" Version="1.7.0" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.33" />
|
||||
<PackageReference Include="Humanizer" Version="2.10.1" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.34" />
|
||||
<PackageReference Include="Humanizer" Version="2.11.10" />
|
||||
<PackageReference Include="MessagePack" Version="2.2.85" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.6" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.6" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="5.0.6" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="5.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="ppy.LocalisationAnalyser" Version="2021.608.0">
|
||||
<PackageReference Include="ppy.LocalisationAnalyser" Version="2021.614.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="10.2.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2021.628.0" />
|
||||
<PackageReference Include="Realm" Version="10.2.1" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2021.702.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.618.0" />
|
||||
<PackageReference Include="Sentry" Version="3.4.0" />
|
||||
<PackageReference Include="SharpCompress" Version="0.28.2" />
|
||||
<PackageReference Include="Sentry" Version="3.6.0" />
|
||||
<PackageReference Include="SharpCompress" Version="0.28.3" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
Reference in New Issue
Block a user