1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-29 01:22:54 +08:00

Merge branch 'master' into open-tablet-driver-faq

This commit is contained in:
Dan Balasescu 2021-08-16 13:46:43 +09:00 committed by GitHub
commit 4d31d07c51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 1468 additions and 689 deletions

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.810.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.813.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.813.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.813.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">

View File

@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
AddSliderStep<float>("circle size", 0, 8, 5, createCatcher); AddSliderStep<float>("circle size", 0, 8, 5, createCatcher);
AddToggleStep("hyper dash", t => this.ChildrenOfType<TestCatcherArea>().ForEach(area => area.ToggleHyperDash(t))); AddToggleStep("hyper dash", t => this.ChildrenOfType<TestCatcherArea>().ForEach(area => area.ToggleHyperDash(t)));
AddToggleStep("toggle hit lighting", lighting => config.SetValue(OsuSetting.HitLighting, lighting));
AddStep("catch centered fruit", () => attemptCatch(new Fruit())); AddStep("catch centered fruit", () => attemptCatch(new Fruit()));
AddStep("catch many random fruit", () => AddStep("catch many random fruit", () =>

View File

@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Catch
Banana, Banana,
Droplet, Droplet,
Catcher, Catcher,
CatchComboCounter CatchComboCounter,
HitExplosion
} }
} }

View 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 osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Skinning.Default
{
public class DefaultHitExplosion : CompositeDrawable, IHitExplosion
{
private CircularContainer largeFaint;
private CircularContainer smallFaint;
private CircularContainer directionalGlow1;
private CircularContainer directionalGlow2;
[BackgroundDependencyLoader]
private void load()
{
Size = new Vector2(20);
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
// scale roughly in-line with visual appearance of notes
const float initial_height = 10;
InternalChildren = new Drawable[]
{
largeFaint = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
Blending = BlendingParameters.Additive,
},
smallFaint = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
Blending = BlendingParameters.Additive,
},
directionalGlow1 = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
Size = new Vector2(0.01f, initial_height),
Blending = BlendingParameters.Additive,
},
directionalGlow2 = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
Size = new Vector2(0.01f, initial_height),
Blending = BlendingParameters.Additive,
}
};
}
public void Animate(HitExplosionEntry entry)
{
X = entry.Position;
Scale = new Vector2(entry.HitObject.Scale);
setColour(entry.ObjectColour);
using (BeginAbsoluteSequence(entry.LifetimeStart))
applyTransforms(entry.HitObject.RandomSeed);
}
private void applyTransforms(int randomSeed)
{
const double duration = 400;
// we want our size to be very small so the glow dominates it.
largeFaint.Size = new Vector2(0.8f);
largeFaint
.ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint)
.FadeOut(duration * 2);
const float angle_variangle = 15; // should be less than 45
directionalGlow1.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 4);
directionalGlow2.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 5);
this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out);
}
private void setColour(Color4 objectColour)
{
const float roundness = 100;
largeFaint.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f),
Roundness = 160,
Radius = 200,
};
smallFaint.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1),
Roundness = 20,
Radius = 50,
};
directionalGlow1.EdgeEffect = directionalGlow2.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1),
Roundness = roundness,
Radius = 40,
};
}
}
}

View File

@ -70,13 +70,11 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
if (version < 2.3m) if (version < 2.3m)
{ {
if (GetTexture(@"fruit-ryuuta") != null || if (hasOldStyleCatcherSprite())
GetTexture(@"fruit-ryuuta-0") != null)
return new LegacyCatcherOld(); return new LegacyCatcherOld();
} }
if (GetTexture(@"fruit-catcher-idle") != null || if (hasNewStyleCatcherSprite())
GetTexture(@"fruit-catcher-idle-0") != null)
return new LegacyCatcherNew(); return new LegacyCatcherNew();
return null; return null;
@ -86,12 +84,26 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
return new LegacyCatchComboCounter(Skin); return new LegacyCatchComboCounter(Skin);
return null; return null;
case CatchSkinComponents.HitExplosion:
if (hasOldStyleCatcherSprite() || hasNewStyleCatcherSprite())
return new LegacyHitExplosion();
return null;
} }
} }
return base.GetDrawableComponent(component); return base.GetDrawableComponent(component);
} }
private bool hasOldStyleCatcherSprite() =>
GetTexture(@"fruit-ryuuta") != null
|| GetTexture(@"fruit-ryuuta-0") != null;
private bool hasNewStyleCatcherSprite() =>
GetTexture(@"fruit-catcher-idle") != null
|| GetTexture(@"fruit-catcher-idle-0") != null;
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{ {
switch (lookup) switch (lookup)

View File

@ -0,0 +1,94 @@
// 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.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{
public class LegacyHitExplosion : CompositeDrawable, IHitExplosion
{
[Resolved]
private Catcher catcher { get; set; }
private const float catch_margin = (1 - Catcher.ALLOWED_CATCH_RANGE) / 2;
private readonly Sprite explosion1;
private readonly Sprite explosion2;
public LegacyHitExplosion()
{
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
RelativeSizeAxes = Axes.Both;
Scale = new Vector2(0.5f);
InternalChildren = new[]
{
explosion1 = new Sprite
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.CentreLeft,
Alpha = 0,
Blending = BlendingParameters.Additive,
Rotation = -90
},
explosion2 = new Sprite
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.CentreLeft,
Alpha = 0,
Blending = BlendingParameters.Additive,
Rotation = -90
}
};
}
[BackgroundDependencyLoader]
private void load(SkinManager skins)
{
var defaultLegacySkin = skins.DefaultLegacySkin;
// sprite names intentionally swapped to match stable member naming / ease of cross-referencing
explosion1.Texture = defaultLegacySkin.GetTexture("scoreboard-explosion-2");
explosion2.Texture = defaultLegacySkin.GetTexture("scoreboard-explosion-1");
}
public void Animate(HitExplosionEntry entry)
{
Colour = entry.ObjectColour;
using (BeginAbsoluteSequence(entry.LifetimeStart))
{
float halfCatchWidth = catcher.CatchWidth / 2;
float explosionOffset = Math.Clamp(entry.Position, -halfCatchWidth + catch_margin * 3, halfCatchWidth - catch_margin * 3);
if (!(entry.HitObject is Droplet))
{
float scale = Math.Clamp(entry.JudgementResult.ComboAtJudgement / 200f, 0.35f, 1.125f);
explosion1.Scale = new Vector2(1, 0.9f);
explosion1.Position = new Vector2(explosionOffset, 0);
explosion1.FadeOutFromOne(300);
explosion1.ScaleTo(new Vector2(16 * scale, 1.1f), 160, Easing.Out);
}
explosion2.Scale = new Vector2(0.9f, 1);
explosion2.Position = new Vector2(explosionOffset, 0);
explosion2.FadeOutFromOne(700);
explosion2.ScaleTo(new Vector2(0.9f, 1.3f), 500, Easing.Out);
this.Delay(700).FadeOutFromOne();
}
}
}
}

View File

@ -23,6 +23,7 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.UI namespace osu.Game.Rulesets.Catch.UI
{ {
[Cached]
public class Catcher : SkinReloadableDrawable public class Catcher : SkinReloadableDrawable
{ {
/// <summary> /// <summary>
@ -106,7 +107,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// <summary> /// <summary>
/// Width of the area that can be used to attempt catches during gameplay. /// Width of the area that can be used to attempt catches during gameplay.
/// </summary> /// </summary>
private readonly float catchWidth; public readonly float CatchWidth;
private readonly SkinnableCatcher body; private readonly SkinnableCatcher body;
@ -133,7 +134,7 @@ namespace osu.Game.Rulesets.Catch.UI
if (difficulty != null) if (difficulty != null)
Scale = calculateScale(difficulty); Scale = calculateScale(difficulty);
catchWidth = CalculateCatchWidth(Scale); CatchWidth = CalculateCatchWidth(Scale);
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
@ -193,7 +194,7 @@ namespace osu.Game.Rulesets.Catch.UI
if (!(hitObject is PalpableCatchHitObject fruit)) if (!(hitObject is PalpableCatchHitObject fruit))
return false; return false;
float halfCatchWidth = catchWidth * 0.5f; float halfCatchWidth = CatchWidth * 0.5f;
return fruit.EffectiveX >= X - halfCatchWidth && return fruit.EffectiveX >= X - halfCatchWidth &&
fruit.EffectiveX <= X + halfCatchWidth; fruit.EffectiveX <= X + halfCatchWidth;
} }
@ -216,7 +217,7 @@ namespace osu.Game.Rulesets.Catch.UI
placeCaughtObject(palpableObject, positionInStack); placeCaughtObject(palpableObject, positionInStack);
if (hitLighting.Value) if (hitLighting.Value)
addLighting(hitObject, positionInStack.X, drawableObject.AccentColour.Value); addLighting(result, drawableObject.AccentColour.Value, positionInStack.X);
} }
// droplet doesn't affect the catcher state // droplet doesn't affect the catcher state
@ -365,8 +366,8 @@ namespace osu.Game.Rulesets.Catch.UI
return position; return position;
} }
private void addLighting(CatchHitObject hitObject, float x, Color4 colour) => private void addLighting(JudgementResult judgementResult, Color4 colour, float x) =>
hitExplosionContainer.Add(new HitExplosionEntry(Time.Current, x, hitObject.Scale, colour, hitObject.RandomSeed)); hitExplosionContainer.Add(new HitExplosionEntry(Time.Current, judgementResult, colour, x));
private CaughtObject getCaughtObject(PalpableCatchHitObject source) private CaughtObject getCaughtObject(PalpableCatchHitObject source)
{ {

View File

@ -1,129 +1,56 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Catch.Skinning.Default;
using osu.Framework.Graphics.Effects;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Pooling; using osu.Game.Rulesets.Objects.Pooling;
using osu.Game.Utils; using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics; #nullable enable
namespace osu.Game.Rulesets.Catch.UI namespace osu.Game.Rulesets.Catch.UI
{ {
public class HitExplosion : PoolableDrawableWithLifetime<HitExplosionEntry> public class HitExplosion : PoolableDrawableWithLifetime<HitExplosionEntry>
{ {
private readonly CircularContainer largeFaint; private readonly SkinnableDrawable skinnableExplosion;
private readonly CircularContainer smallFaint;
private readonly CircularContainer directionalGlow1;
private readonly CircularContainer directionalGlow2;
public HitExplosion() public HitExplosion()
{ {
Size = new Vector2(20); RelativeSizeAxes = Axes.Both;
Anchor = Anchor.TopCentre; Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre; Origin = Anchor.BottomCentre;
// scale roughly in-line with visual appearance of notes InternalChild = skinnableExplosion = new SkinnableDrawable(new CatchSkinComponent(CatchSkinComponents.HitExplosion), _ => new DefaultHitExplosion())
const float initial_height = 10;
InternalChildren = new Drawable[]
{ {
largeFaint = new CircularContainer CentreComponent = false,
{ Anchor = Anchor.BottomCentre,
Anchor = Anchor.Centre, Origin = Anchor.BottomCentre
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
Blending = BlendingParameters.Additive,
},
smallFaint = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
Blending = BlendingParameters.Additive,
},
directionalGlow1 = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
Size = new Vector2(0.01f, initial_height),
Blending = BlendingParameters.Additive,
},
directionalGlow2 = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
Size = new Vector2(0.01f, initial_height),
Blending = BlendingParameters.Additive,
}
}; };
} }
protected override void OnApply(HitExplosionEntry entry) protected override void OnApply(HitExplosionEntry entry)
{ {
X = entry.Position; base.OnApply(entry);
Scale = new Vector2(entry.Scale); if (IsLoaded)
setColour(entry.ObjectColour); apply(entry);
using (BeginAbsoluteSequence(entry.LifetimeStart))
applyTransforms(entry.RNGSeed);
} }
private void applyTransforms(int randomSeed) protected override void LoadComplete()
{ {
base.LoadComplete();
apply(Entry);
}
private void apply(HitExplosionEntry? entry)
{
if (entry == null)
return;
ApplyTransformsAt(double.MinValue, true);
ClearTransforms(true); ClearTransforms(true);
const double duration = 400; (skinnableExplosion.Drawable as IHitExplosion)?.Animate(entry);
LifetimeEnd = skinnableExplosion.Drawable.LatestTransformEndTime;
// we want our size to be very small so the glow dominates it.
largeFaint.Size = new Vector2(0.8f);
largeFaint
.ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint)
.FadeOut(duration * 2);
const float angle_variangle = 15; // should be less than 45
directionalGlow1.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 4);
directionalGlow2.Rotation = StatelessRNG.NextSingle(-angle_variangle, angle_variangle, randomSeed, 5);
this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out).Expire();
}
private void setColour(Color4 objectColour)
{
const float roundness = 100;
largeFaint.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f),
Roundness = 160,
Radius = 200,
};
smallFaint.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1),
Roundness = 20,
Radius = 50,
};
directionalGlow1.EdgeEffect = directionalGlow2.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1),
Roundness = roundness,
Radius = 40,
};
} }
} }
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects.Pooling; using osu.Game.Rulesets.Objects.Pooling;
@ -14,6 +15,8 @@ namespace osu.Game.Rulesets.Catch.UI
public HitExplosionContainer() public HitExplosionContainer()
{ {
RelativeSizeAxes = Axes.Both;
AddInternal(pool = new DrawablePool<HitExplosion>(10)); AddInternal(pool = new DrawablePool<HitExplosion>(10));
} }

View File

@ -2,24 +2,42 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Performance;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Judgements;
using osuTK.Graphics; using osuTK.Graphics;
#nullable enable
namespace osu.Game.Rulesets.Catch.UI namespace osu.Game.Rulesets.Catch.UI
{ {
public class HitExplosionEntry : LifetimeEntry public class HitExplosionEntry : LifetimeEntry
{ {
public readonly float Position; /// <summary>
public readonly float Scale; /// The judgement result that triggered this explosion.
public readonly Color4 ObjectColour; /// </summary>
public readonly int RNGSeed; public JudgementResult JudgementResult { get; }
public HitExplosionEntry(double startTime, float position, float scale, Color4 objectColour, int rngSeed) /// <summary>
/// The hitobject which triggered this explosion.
/// </summary>
public CatchHitObject HitObject => (CatchHitObject)JudgementResult.HitObject;
/// <summary>
/// The accent colour of the object caught.
/// </summary>
public Color4 ObjectColour { get; }
/// <summary>
/// The position at which the object was caught.
/// </summary>
public float Position { get; }
public HitExplosionEntry(double startTime, JudgementResult judgementResult, Color4 objectColour, float position)
{ {
LifetimeStart = startTime; LifetimeStart = startTime;
Position = position; Position = position;
Scale = scale; JudgementResult = judgementResult;
ObjectColour = objectColour; ObjectColour = objectColour;
RNGSeed = rngSeed;
} }
} }
} }

View File

@ -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.
#nullable enable
namespace osu.Game.Rulesets.Catch.UI
{
/// <summary>
/// Common interface for all hit explosion skinnables.
/// </summary>
public interface IHitExplosion
{
/// <summary>
/// Begins animating this <see cref="IHitExplosion"/>.
/// </summary>
void Animate(HitExplosionEntry entry);
}
}

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -32,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{ {
drawableRuleset.Overlays.Add(blinds = new DrawableOsuBlinds(drawableRuleset.Playfield.HitObjectContainer, drawableRuleset.Beatmap)); drawableRuleset.Overlays.Add(blinds = new DrawableOsuBlinds(drawableRuleset.Playfield, drawableRuleset.Beatmap));
} }
public void ApplyToHealthProcessor(HealthProcessor healthProcessor) public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
@ -128,8 +129,21 @@ namespace osu.Game.Rulesets.Osu.Mods
protected override void Update() protected override void Update()
{ {
float start = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopLeft).X; float start, end;
float end = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopRight).X;
if (Precision.AlmostEquals(restrictTo.Rotation, 0))
{
start = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopLeft).X;
end = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopRight).X;
}
else
{
float center = restrictTo.ToSpaceOfOtherDrawable(restrictTo.OriginPosition, Parent).X;
float halfDiagonal = (restrictTo.DrawSize / 2).LengthFast;
start = center - halfDiagonal;
end = center + halfDiagonal;
}
float rawWidth = end - start; float rawWidth = end - start;

View File

@ -9,6 +9,7 @@ using osu.Framework.Input.Events;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
@ -21,6 +22,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private double lastTrailTime; private double lastTrailTime;
private IBindable<float> cursorSize; private IBindable<float> cursorSize;
private Vector2? currentPosition;
public LegacyCursorTrail(ISkin skin) public LegacyCursorTrail(ISkin skin)
{ {
this.skin = skin; this.skin = skin;
@ -54,22 +57,34 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
} }
protected override double FadeDuration => disjointTrail ? 150 : 500; protected override double FadeDuration => disjointTrail ? 150 : 500;
protected override float FadeExponent => 1;
protected override bool InterpolateMovements => !disjointTrail; protected override bool InterpolateMovements => !disjointTrail;
protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1); protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1);
protected override void Update()
{
base.Update();
if (!disjointTrail || !currentPosition.HasValue)
return;
if (Time.Current - lastTrailTime >= disjoint_trail_time_separation)
{
lastTrailTime = Time.Current;
AddTrail(currentPosition.Value);
}
}
protected override bool OnMouseMove(MouseMoveEvent e) protected override bool OnMouseMove(MouseMoveEvent e)
{ {
if (!disjointTrail) if (!disjointTrail)
return base.OnMouseMove(e); return base.OnMouseMove(e);
if (Time.Current - lastTrailTime >= disjoint_trail_time_separation) currentPosition = e.ScreenSpaceMousePosition;
{
lastTrailTime = Time.Current;
return base.OnMouseMove(e);
}
// Intentionally block the base call as we're adding the trails ourselves.
return false; return false;
} }
} }

View File

@ -26,6 +26,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
{ {
private const int max_sprites = 2048; private const int max_sprites = 2048;
/// <summary>
/// An exponentiating factor to ease the trail fade.
/// </summary>
protected virtual float FadeExponent => 1.7f;
private readonly TrailPart[] parts = new TrailPart[max_sprites]; private readonly TrailPart[] parts = new TrailPart[max_sprites];
private int currentIndex; private int currentIndex;
private IShader shader; private IShader shader;
@ -141,22 +146,25 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
protected override bool OnMouseMove(MouseMoveEvent e) protected override bool OnMouseMove(MouseMoveEvent e)
{ {
Vector2 pos = e.ScreenSpaceMousePosition; AddTrail(e.ScreenSpaceMousePosition);
if (lastPosition == null)
{
lastPosition = pos;
resampler.AddPosition(lastPosition.Value);
return base.OnMouseMove(e); return base.OnMouseMove(e);
} }
foreach (Vector2 pos2 in resampler.AddPosition(pos)) protected void AddTrail(Vector2 position)
{
if (InterpolateMovements)
{
if (!lastPosition.HasValue)
{
lastPosition = position;
resampler.AddPosition(lastPosition.Value);
return;
}
foreach (Vector2 pos2 in resampler.AddPosition(position))
{ {
Trace.Assert(lastPosition.HasValue); Trace.Assert(lastPosition.HasValue);
if (InterpolateMovements)
{
// ReSharper disable once PossibleInvalidOperationException
Vector2 pos1 = lastPosition.Value; Vector2 pos1 = lastPosition.Value;
Vector2 diff = pos2 - pos1; Vector2 diff = pos2 - pos1;
float distance = diff.Length; float distance = diff.Length;
@ -170,16 +178,14 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
addPart(lastPosition.Value); addPart(lastPosition.Value);
} }
} }
}
else else
{ {
lastPosition = pos2; lastPosition = position;
addPart(lastPosition.Value); addPart(lastPosition.Value);
} }
} }
return base.OnMouseMove(e);
}
private void addPart(Vector2 screenSpacePosition) private void addPart(Vector2 screenSpacePosition)
{ {
parts[currentIndex].Position = screenSpacePosition; parts[currentIndex].Position = screenSpacePosition;
@ -206,10 +212,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private Texture texture; private Texture texture;
private float time; private float time;
private float fadeExponent;
private readonly TrailPart[] parts = new TrailPart[max_sprites]; private readonly TrailPart[] parts = new TrailPart[max_sprites];
private Vector2 size; private Vector2 size;
private Vector2 originPosition; private Vector2 originPosition;
private readonly QuadBatch<TexturedTrailVertex> vertexBatch = new QuadBatch<TexturedTrailVertex>(max_sprites, 1); private readonly QuadBatch<TexturedTrailVertex> vertexBatch = new QuadBatch<TexturedTrailVertex>(max_sprites, 1);
@ -227,6 +233,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
texture = Source.texture; texture = Source.texture;
size = Source.partSize; size = Source.partSize;
time = Source.time; time = Source.time;
fadeExponent = Source.FadeExponent;
originPosition = Vector2.Zero; originPosition = Vector2.Zero;
@ -249,6 +256,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
shader.Bind(); shader.Bind();
shader.GetUniform<float>("g_FadeClock").UpdateValue(ref time); shader.GetUniform<float>("g_FadeClock").UpdateValue(ref time);
shader.GetUniform<float>("g_FadeExponent").UpdateValue(ref fadeExponent);
texture.TextureGL.Bind(); texture.TextureGL.Bind();

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -19,6 +20,7 @@ namespace osu.Game.Tests.Chat
{ {
private ChannelManager channelManager; private ChannelManager channelManager;
private int currentMessageId; private int currentMessageId;
private List<Message> sentMessages;
[SetUp] [SetUp]
public void Setup() => Schedule(() => public void Setup() => Schedule(() =>
@ -34,6 +36,7 @@ namespace osu.Game.Tests.Chat
AddStep("register request handling", () => AddStep("register request handling", () =>
{ {
currentMessageId = 0; currentMessageId = 0;
sentMessages = new List<Message>();
((DummyAPIAccess)API).HandleRequest = req => ((DummyAPIAccess)API).HandleRequest = req =>
{ {
@ -44,16 +47,11 @@ namespace osu.Game.Tests.Chat
return true; return true;
case PostMessageRequest postMessage: case PostMessageRequest postMessage:
postMessage.TriggerSuccess(new Message(++currentMessageId) handlePostMessageRequest(postMessage);
{ return true;
IsAction = postMessage.Message.IsAction,
ChannelId = postMessage.Message.ChannelId,
Content = postMessage.Message.Content,
Links = postMessage.Message.Links,
Timestamp = postMessage.Message.Timestamp,
Sender = postMessage.Message.Sender
});
case MarkChannelAsReadRequest markRead:
handleMarkChannelAsReadRequest(markRead);
return true; return true;
} }
@ -83,12 +81,65 @@ namespace osu.Game.Tests.Chat
AddAssert("/np command received by channel 2", () => channel2.Messages.Last().Content.Contains("is listening to")); AddAssert("/np command received by channel 2", () => channel2.Messages.Last().Content.Contains("is listening to"));
} }
[Test]
public void TestMarkAsReadIgnoringLocalMessages()
{
Channel channel = null;
AddStep("join channel and select it", () =>
{
channelManager.JoinChannel(channel = createChannel(1, ChannelType.Public));
channelManager.CurrentChannel.Value = channel;
});
AddStep("post message", () => channelManager.PostMessage("Something interesting"));
AddStep("post /help command", () => channelManager.PostCommand("help", channel));
AddStep("post /me command with no action", () => channelManager.PostCommand("me", channel));
AddStep("post /join command with no channel", () => channelManager.PostCommand("join", channel));
AddStep("post /join command with non-existent channel", () => channelManager.PostCommand("join i-dont-exist", channel));
AddStep("post non-existent command", () => channelManager.PostCommand("non-existent-cmd arg", channel));
AddStep("mark channel as read", () => channelManager.MarkChannelAsRead(channel));
AddAssert("channel's last read ID is set to the latest message", () => channel.LastReadId == sentMessages.Last().Id);
}
private void handlePostMessageRequest(PostMessageRequest request)
{
var message = new Message(++currentMessageId)
{
IsAction = request.Message.IsAction,
ChannelId = request.Message.ChannelId,
Content = request.Message.Content,
Links = request.Message.Links,
Timestamp = request.Message.Timestamp,
Sender = request.Message.Sender
};
sentMessages.Add(message);
request.TriggerSuccess(message);
}
private void handleMarkChannelAsReadRequest(MarkChannelAsReadRequest request)
{
// only accept messages that were sent through the API
if (sentMessages.Contains(request.Message))
{
request.TriggerSuccess();
}
else
{
request.TriggerFailure(new APIException("unknown message!", null));
}
}
private Channel createChannel(int id, ChannelType type) => new Channel(new User()) private Channel createChannel(int id, ChannelType type) => new Channel(new User())
{ {
Id = id, Id = id,
Name = $"Channel {id}", Name = $"Channel {id}",
Topic = $"Topic of channel {id} with type {type}", Topic = $"Topic of channel {id} with type {type}",
Type = type, Type = type,
LastMessageId = 0,
}; };
private class ChannelManagerContainer : CompositeDrawable private class ChannelManagerContainer : CompositeDrawable

View File

@ -40,6 +40,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}); });
AddStep("add local player", () => createLeaderboardScore(playerScore, new User { Username = "You", Id = 3 }, true)); AddStep("add local player", () => createLeaderboardScore(playerScore, new User { Username = "You", Id = 3 }, true));
AddStep("toggle expanded", () => leaderboard.Expanded.Value = !leaderboard.Expanded.Value);
AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v); AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v);
} }
@ -83,19 +84,38 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("add frenzibyte", () => createRandomScore(new User { Username = "frenzibyte", Id = 14210502 })); AddStep("add frenzibyte", () => createRandomScore(new User { Username = "frenzibyte", Id = 14210502 }));
} }
[Test]
public void TestMaxHeight()
{
int playerNumber = 1;
AddRepeatStep("add 3 other players", () => createRandomScore(new User { Username = $"Player {playerNumber++}" }), 3);
checkHeight(4);
AddRepeatStep("add 4 other players", () => createRandomScore(new User { Username = $"Player {playerNumber++}" }), 4);
checkHeight(8);
AddRepeatStep("add 4 other players", () => createRandomScore(new User { Username = $"Player {playerNumber++}" }), 4);
checkHeight(8);
void checkHeight(int panelCount)
=> AddAssert($"leaderboard height is {panelCount} panels high", () => leaderboard.DrawHeight == (GameplayLeaderboardScore.PANEL_HEIGHT + leaderboard.Spacing) * panelCount);
}
private void createRandomScore(User user) => createLeaderboardScore(new BindableDouble(RNG.Next(0, 5_000_000)), user); private void createRandomScore(User user) => createLeaderboardScore(new BindableDouble(RNG.Next(0, 5_000_000)), user);
private void createLeaderboardScore(BindableDouble score, User user, bool isTracked = false) private void createLeaderboardScore(BindableDouble score, User user, bool isTracked = false)
{ {
var leaderboardScore = leaderboard.AddPlayer(user, isTracked); var leaderboardScore = leaderboard.Add(user, isTracked);
leaderboardScore.TotalScore.BindTo(score); leaderboardScore.TotalScore.BindTo(score);
} }
private class TestGameplayLeaderboard : GameplayLeaderboard private class TestGameplayLeaderboard : GameplayLeaderboard
{ {
public float Spacing => Flow.Spacing.Y;
public bool CheckPositionByUsername(string username, int? expectedPosition) public bool CheckPositionByUsername(string username, int? expectedPosition)
{ {
var scoreItem = this.FirstOrDefault(i => i.User?.Username == username); var scoreItem = Flow.FirstOrDefault(i => i.User?.Username == username);
return scoreItem != null && scoreItem.ScorePosition == expectedPosition; return scoreItem != null && scoreItem.ScorePosition == expectedPosition;
} }

View File

@ -12,6 +12,7 @@ using osu.Game.Configuration;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
@ -142,6 +143,22 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("return value", () => config.SetValue(OsuSetting.KeyOverlay, keyCounterVisibleValue)); AddStep("return value", () => config.SetValue(OsuSetting.KeyOverlay, keyCounterVisibleValue));
} }
[Test]
public void TestHiddenHUDDoesntBlockSkinnableComponentsLoad()
{
HUDVisibilityMode originalConfigValue = default;
AddStep("get original config value", () => originalConfigValue = config.Get<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode));
AddStep("set hud to never show", () => config.SetValue(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Never));
createNew();
AddUntilStep("wait for hud load", () => hudOverlay.IsLoaded);
AddUntilStep("skinnable components loaded", () => hudOverlay.ChildrenOfType<SkinnableTargetContainer>().Single().ComponentsLoaded);
AddStep("set original config value", () => config.SetValue(OsuSetting.HUDVisibilityMode, originalConfigValue));
}
private void createNew(Action<HUDOverlay> action = null) private void createNew(Action<HUDOverlay> action = null)
{ {
AddStep("create overlay", () => AddStep("create overlay", () =>

View File

@ -0,0 +1,42 @@
// 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.Testing;
using osu.Game.Overlays;
using osu.Game.Tests.Visual.Navigation;
namespace osu.Game.Tests.Visual.Menus
{
public class TestSceneSideOverlays : OsuGameTestScene
{
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
AddAssert("no screen offset applied", () => Game.ScreenOffsetContainer.X == 0f);
AddUntilStep("wait for overlays", () => Game.Settings.IsLoaded && Game.Notifications.IsLoaded);
}
[Test]
public void TestScreenOffsettingOnSettingsOverlay()
{
AddStep("open settings", () => Game.Settings.Show());
AddUntilStep("right screen offset applied", () => Game.ScreenOffsetContainer.X == SettingsPanel.WIDTH * TestOsuGame.SIDE_OVERLAY_OFFSET_RATIO);
AddStep("hide settings", () => Game.Settings.Hide());
AddUntilStep("screen offset removed", () => Game.ScreenOffsetContainer.X == 0f);
}
[Test]
public void TestScreenOffsettingOnNotificationOverlay()
{
AddStep("open notifications", () => Game.Notifications.Show());
AddUntilStep("right screen offset applied", () => Game.ScreenOffsetContainer.X == -NotificationOverlay.WIDTH * TestOsuGame.SIDE_OVERLAY_OFFSET_RATIO);
AddStep("hide notifications", () => Game.Notifications.Hide());
AddUntilStep("screen offset removed", () => Game.ScreenOffsetContainer.X == 0f);
}
}
}

View File

@ -0,0 +1,55 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Play;
namespace osu.Game.Tests.Visual.Mods
{
public class TestSceneModFailCondition : ModTestScene
{
private bool restartRequested;
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
protected override TestPlayer CreateModPlayer(Ruleset ruleset)
{
var player = base.CreateModPlayer(ruleset);
player.RestartRequested = () => restartRequested = true;
return player;
}
protected override bool AllowFail => true;
[SetUpSteps]
public void SetUp()
{
AddStep("reset flag", () => restartRequested = false);
}
[Test]
public void TestRestartOnFailDisabled() => CreateModTest(new ModTestData
{
Autoplay = false,
Mod = new OsuModSuddenDeath(),
PassCondition = () => !restartRequested && Player.ChildrenOfType<FailOverlay>().Single().State.Value == Visibility.Visible
});
[Test]
public void TestRestartOnFailEnabled() => CreateModTest(new ModTestData
{
Autoplay = false,
Mod = new OsuModSuddenDeath
{
Restart = { Value = true }
},
PassCondition = () => restartRequested && Player.ChildrenOfType<FailOverlay>().Single().State.Value == Visibility.Hidden
});
}
}

View File

@ -87,6 +87,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestEmpty() public void TestEmpty()
{ {
// used to test the flow of multiplayer from visual tests. // used to test the flow of multiplayer from visual tests.
AddStep("empty step", () => { });
} }
[Test] [Test]
@ -408,8 +409,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden); AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden);
testLeave("lounge tab item", () => this.ChildrenOfType<BreadcrumbControl<IScreen>.BreadcrumbTabItem>().First().TriggerClick());
testLeave("back button", () => multiplayerScreen.OnBackButton()); testLeave("back button", () => multiplayerScreen.OnBackButton());
// mimics home button and OS window close // mimics home button and OS window close

View File

@ -0,0 +1,51 @@
// 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.Game.Online.Rooms;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerResults : ScreenTestScene
{
[Test]
public void TestDisplayResults()
{
MultiplayerResultsScreen screen = null;
AddStep("show results screen", () =>
{
var rulesetInfo = new OsuRuleset().RulesetInfo;
var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo;
var score = new ScoreInfo
{
Rank = ScoreRank.B,
TotalScore = 987654,
Accuracy = 0.8,
MaxCombo = 500,
Combo = 250,
Beatmap = beatmapInfo,
User = new User { Username = "Test user" },
Date = DateTimeOffset.Now,
OnlineScoreID = 12345,
Ruleset = rulesetInfo,
};
PlaylistItem playlistItem = new PlaylistItem
{
BeatmapID = beatmapInfo.ID,
};
Stack.Push(screen = new MultiplayerResultsScreen(score, 1, playlistItem));
});
AddUntilStep("wait for loaded", () => screen.IsLoaded);
}
}
}

View File

@ -0,0 +1,61 @@
// 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 NUnit.Framework;
using osu.Framework.Bindables;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerTeamResults : ScreenTestScene
{
[TestCase(7483253, 1048576)]
[TestCase(1048576, 7483253)]
[TestCase(1048576, 1048576)]
public void TestDisplayTeamResults(int team1Score, int team2Score)
{
MultiplayerResultsScreen screen = null;
AddStep("show results screen", () =>
{
var rulesetInfo = new OsuRuleset().RulesetInfo;
var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo;
var score = new ScoreInfo
{
Rank = ScoreRank.B,
TotalScore = 987654,
Accuracy = 0.8,
MaxCombo = 500,
Combo = 250,
Beatmap = beatmapInfo,
User = new User { Username = "Test user" },
Date = DateTimeOffset.Now,
OnlineScoreID = 12345,
Ruleset = rulesetInfo,
};
PlaylistItem playlistItem = new PlaylistItem
{
BeatmapID = beatmapInfo.ID,
};
SortedDictionary<int, BindableInt> teamScores = new SortedDictionary<int, BindableInt>
{
{ 0, new BindableInt(team1Score) },
{ 1, new BindableInt(team2Score) }
};
Stack.Push(screen = new MultiplayerTeamResultsScreen(score, 1, playlistItem, teamScores));
});
AddUntilStep("wait for loaded", () => screen.IsLoaded);
}
}
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Screens; using osu.Framework.Screens;
@ -95,6 +96,8 @@ namespace osu.Game.Tests.Visual.Navigation
public class TestOsuGame : OsuGame public class TestOsuGame : OsuGame
{ {
public new const float SIDE_OVERLAY_OFFSET_RATIO = OsuGame.SIDE_OVERLAY_OFFSET_RATIO;
public new ScreenStack ScreenStack => base.ScreenStack; public new ScreenStack ScreenStack => base.ScreenStack;
public new BackButton BackButton => base.BackButton; public new BackButton BackButton => base.BackButton;
@ -103,7 +106,11 @@ namespace osu.Game.Tests.Visual.Navigation
public new ScoreManager ScoreManager => base.ScoreManager; public new ScoreManager ScoreManager => base.ScoreManager;
public new SettingsPanel Settings => base.Settings; public new Container ScreenOffsetContainer => base.ScreenOffsetContainer;
public new SettingsOverlay Settings => base.Settings;
public new NotificationOverlay Notifications => base.Notifications;
public new MusicController MusicController => base.MusicController; public new MusicController MusicController => base.MusicController;

View File

@ -1,23 +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.Game.Screens.OnlinePlay.Lounge.Components;
namespace osu.Game.Tests.Visual.Playlists
{
public class TestScenePlaylistsFilterControl : OsuTestScene
{
public TestScenePlaylistsFilterControl()
{
Child = new PlaylistsFilterControl
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Width = 0.7f,
Height = 80,
};
}
}
}

View File

@ -62,6 +62,24 @@ namespace osu.Game.Tests.Visual.Playlists
AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.Rooms[^1])); AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.Rooms[^1]));
} }
[Test]
public void TestEnteringRoomTakesLeaseOnSelection()
{
AddStep("add rooms", () => RoomManager.AddRooms(1));
AddAssert("selected room is not disabled", () => !OnlinePlayDependencies.SelectedRoom.Disabled);
AddStep("select room", () => roomsContainer.Rooms[0].TriggerClick());
AddAssert("selected room is non-null", () => OnlinePlayDependencies.SelectedRoom.Value != null);
AddStep("enter room", () => roomsContainer.Rooms[0].TriggerClick());
AddUntilStep("wait for match load", () => Stack.CurrentScreen is PlaylistsRoomSubScreen);
AddAssert("selected room is non-null", () => OnlinePlayDependencies.SelectedRoom.Value != null);
AddAssert("selected room is disabled", () => OnlinePlayDependencies.SelectedRoom.Disabled);
}
private bool checkRoomVisible(DrawableRoom room) => private bool checkRoomVisible(DrawableRoom room) =>
loungeScreen.ChildrenOfType<OsuScrollContainer>().First().ScreenSpaceDrawQuad loungeScreen.ChildrenOfType<OsuScrollContainer>().First().ScreenSpaceDrawQuad
.Contains(room.ScreenSpaceDrawQuad.Centre); .Contains(room.ScreenSpaceDrawQuad.Centre);

View File

@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Settings
new TabletSettings(tabletHandler) new TabletSettings(tabletHandler)
{ {
RelativeSizeAxes = Axes.None, RelativeSizeAxes = Axes.None,
Width = SettingsPanel.WIDTH, Width = SettingsPanel.PANEL_WIDTH,
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
} }

View File

@ -101,7 +101,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.HitLighting, true); SetDefault(OsuSetting.HitLighting, true);
SetDefault(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always); SetDefault(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always);
SetDefault(OsuSetting.ShowProgressGraph, true); SetDefault(OsuSetting.ShowDifficultyGraph, true);
SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true); SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true);
SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true); SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true);
SetDefault(OsuSetting.KeyOverlay, false); SetDefault(OsuSetting.KeyOverlay, false);
@ -217,7 +217,7 @@ namespace osu.Game.Configuration
AlwaysPlayFirstComboBreak, AlwaysPlayFirstComboBreak,
FloatingComments, FloatingComments,
HUDVisibilityMode, HUDVisibilityMode,
ShowProgressGraph, ShowDifficultyGraph,
ShowHealthDisplayWhenCantFail, ShowHealthDisplayWhenCantFail,
FadePlayfieldWhenHealthLow, FadePlayfieldWhenHealthLow,
MouseDisableButtons, MouseDisableButtons,

View File

@ -0,0 +1,24 @@
// 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.Localisation;
namespace osu.Game.Localisation
{
public static class MultiplayerTeamResultsScreenStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.MultiplayerTeamResultsScreen";
/// <summary>
/// "Team {0} wins!"
/// </summary>
public static LocalisableString TeamWins(string winner) => new TranslatableString(getKey(@"team_wins"), @"Team {0} wins!", winner);
/// <summary>
/// "The teams are tied!"
/// </summary>
public static LocalisableString TheTeamsAreTied => new TranslatableString(getKey(@"the_teams_are_tied"), @"The teams are tied!");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -9,16 +9,16 @@ namespace osu.Game.Online.API.Requests
{ {
public class MarkChannelAsReadRequest : APIRequest public class MarkChannelAsReadRequest : APIRequest
{ {
private readonly Channel channel; public readonly Channel Channel;
private readonly Message message; public readonly Message Message;
public MarkChannelAsReadRequest(Channel channel, Message message) public MarkChannelAsReadRequest(Channel channel, Message message)
{ {
this.channel = channel; Channel = channel;
this.message = message; Message = message;
} }
protected override string Target => $"chat/channels/{channel.Id}/mark-as-read/{message.Id}"; protected override string Target => $"chat/channels/{Channel.Id}/mark-as-read/{Message.Id}";
protected override WebRequest CreateWebRequest() protected override WebRequest CreateWebRequest()
{ {

View File

@ -553,7 +553,7 @@ namespace osu.Game.Online.Chat
if (channel.LastMessageId == channel.LastReadId) if (channel.LastMessageId == channel.LastReadId)
return; return;
var message = channel.Messages.LastOrDefault(); var message = channel.Messages.FindLast(msg => !(msg is LocalMessage));
if (message == null) if (message == null)
return; return;

View File

@ -31,6 +31,15 @@ namespace osu.Game.Online.Multiplayer
/// <param name="user">The user.</param> /// <param name="user">The user.</param>
Task UserLeft(MultiplayerRoomUser user); Task UserLeft(MultiplayerRoomUser user);
/// <summary>
/// Signals that a user has been kicked from the room.
/// </summary>
/// <remarks>
/// This will also be sent to the user that was kicked.
/// </remarks>
/// <param name="user">The user.</param>
Task UserKicked(MultiplayerRoomUser user);
/// <summary> /// <summary>
/// Signal that the host of the room has changed. /// Signal that the host of the room has changed.
/// </summary> /// </summary>

View File

@ -389,6 +389,18 @@ namespace osu.Game.Online.Multiplayer
return Task.CompletedTask; return Task.CompletedTask;
} }
Task IMultiplayerClient.UserKicked(MultiplayerRoomUser user)
{
if (LocalUser == null)
return Task.CompletedTask;
if (user.Equals(LocalUser))
LeaveRoom();
// TODO: also inform users of the kick operation.
return ((IMultiplayerClient)this).UserLeft(user);
}
Task IMultiplayerClient.HostChanged(int userId) Task IMultiplayerClient.HostChanged(int userId)
{ {
if (Room == null) if (Room == null)

View File

@ -50,6 +50,7 @@ namespace osu.Game.Online.Multiplayer
connection.On<MultiplayerRoomState>(nameof(IMultiplayerClient.RoomStateChanged), ((IMultiplayerClient)this).RoomStateChanged); connection.On<MultiplayerRoomState>(nameof(IMultiplayerClient.RoomStateChanged), ((IMultiplayerClient)this).RoomStateChanged);
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined); connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined);
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft); connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft);
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserKicked), ((IMultiplayerClient)this).UserKicked);
connection.On<int>(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged); connection.On<int>(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged);
connection.On<MultiplayerRoomSettings>(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged); connection.On<MultiplayerRoomSettings>(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged);
connection.On<int, MultiplayerUserState>(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged); connection.On<int, MultiplayerUserState>(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged);

View File

@ -64,6 +64,11 @@ namespace osu.Game
/// </summary> /// </summary>
public class OsuGame : OsuGameBase, IKeyBindingHandler<GlobalAction> public class OsuGame : OsuGameBase, IKeyBindingHandler<GlobalAction>
{ {
/// <summary>
/// The amount of global offset to apply when a left/right anchored overlay is displayed (ie. settings or notifications).
/// </summary>
protected const float SIDE_OVERLAY_OFFSET_RATIO = 0.05f;
public Toolbar Toolbar; public Toolbar Toolbar;
private ChatOverlay chatOverlay; private ChatOverlay chatOverlay;
@ -71,7 +76,7 @@ namespace osu.Game
private ChannelManager channelManager; private ChannelManager channelManager;
[NotNull] [NotNull]
private readonly NotificationOverlay notifications = new NotificationOverlay(); protected readonly NotificationOverlay Notifications = new NotificationOverlay();
private BeatmapListingOverlay beatmapListing; private BeatmapListingOverlay beatmapListing;
@ -97,7 +102,7 @@ namespace osu.Game
private ScalingContainer screenContainer; private ScalingContainer screenContainer;
private Container screenOffsetContainer; protected Container ScreenOffsetContainer { get; private set; }
[Resolved] [Resolved]
private FrameworkConfigManager frameworkConfig { get; set; } private FrameworkConfigManager frameworkConfig { get; set; }
@ -312,7 +317,7 @@ namespace osu.Game
case LinkAction.OpenEditorTimestamp: case LinkAction.OpenEditorTimestamp:
case LinkAction.JoinMultiplayerMatch: case LinkAction.JoinMultiplayerMatch:
case LinkAction.Spectate: case LinkAction.Spectate:
waitForReady(() => notifications, _ => notifications.Post(new SimpleNotification waitForReady(() => Notifications, _ => Notifications.Post(new SimpleNotification
{ {
Text = @"This link type is not yet supported!", Text = @"This link type is not yet supported!",
Icon = FontAwesome.Solid.LifeRing, Icon = FontAwesome.Solid.LifeRing,
@ -611,12 +616,12 @@ namespace osu.Game
MenuCursorContainer.CanShowCursor = menuScreen?.CursorVisible ?? false; MenuCursorContainer.CanShowCursor = menuScreen?.CursorVisible ?? false;
// todo: all archive managers should be able to be looped here. // todo: all archive managers should be able to be looped here.
SkinManager.PostNotification = n => notifications.Post(n); SkinManager.PostNotification = n => Notifications.Post(n);
BeatmapManager.PostNotification = n => notifications.Post(n); BeatmapManager.PostNotification = n => Notifications.Post(n);
BeatmapManager.PresentImport = items => PresentBeatmap(items.First()); BeatmapManager.PresentImport = items => PresentBeatmap(items.First());
ScoreManager.PostNotification = n => notifications.Post(n); ScoreManager.PostNotification = n => Notifications.Post(n);
ScoreManager.PresentImport = items => PresentScore(items.First()); ScoreManager.PresentImport = items => PresentScore(items.First());
// make config aware of how to lookup skins for on-screen display purposes. // make config aware of how to lookup skins for on-screen display purposes.
@ -655,7 +660,7 @@ namespace osu.Game
ActionRequested = action => volume.Adjust(action), ActionRequested = action => volume.Adjust(action),
ScrollActionRequested = (action, amount, isPrecise) => volume.Adjust(action, amount, isPrecise), ScrollActionRequested = (action, amount, isPrecise) => volume.Adjust(action, amount, isPrecise),
}, },
screenOffsetContainer = new Container ScreenOffsetContainer = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
@ -724,7 +729,7 @@ namespace osu.Game
loadComponentSingleFile(onScreenDisplay, Add, true); loadComponentSingleFile(onScreenDisplay, Add, true);
loadComponentSingleFile(notifications.With(d => loadComponentSingleFile(Notifications.With(d =>
{ {
d.GetToolbarHeight = () => ToolbarOffset; d.GetToolbarHeight = () => ToolbarOffset;
d.Anchor = Anchor.TopRight; d.Anchor = Anchor.TopRight;
@ -733,7 +738,7 @@ namespace osu.Game
loadComponentSingleFile(new CollectionManager(Storage) loadComponentSingleFile(new CollectionManager(Storage)
{ {
PostNotification = n => notifications.Post(n), PostNotification = n => Notifications.Post(n),
}, Add, true); }, Add, true);
loadComponentSingleFile(stableImportManager, Add); loadComponentSingleFile(stableImportManager, Add);
@ -785,7 +790,7 @@ namespace osu.Game
Add(new MusicKeyBindingHandler()); Add(new MusicKeyBindingHandler());
// side overlays which cancel each other. // side overlays which cancel each other.
var singleDisplaySideOverlays = new OverlayContainer[] { Settings, notifications }; var singleDisplaySideOverlays = new OverlayContainer[] { Settings, Notifications };
foreach (var overlay in singleDisplaySideOverlays) foreach (var overlay in singleDisplaySideOverlays)
{ {
@ -828,21 +833,6 @@ namespace osu.Game
{ {
if (mode.NewValue != OverlayActivation.All) CloseAllOverlays(); if (mode.NewValue != OverlayActivation.All) CloseAllOverlays();
}; };
void updateScreenOffset()
{
float offset = 0;
if (Settings.State.Value == Visibility.Visible)
offset += Toolbar.HEIGHT / 2;
if (notifications.State.Value == Visibility.Visible)
offset -= Toolbar.HEIGHT / 2;
screenOffsetContainer.MoveToX(offset, SettingsPanel.TRANSITION_LENGTH, Easing.OutQuint);
}
Settings.State.ValueChanged += _ => updateScreenOffset();
notifications.State.ValueChanged += _ => updateScreenOffset();
} }
private void showOverlayAboveOthers(OverlayContainer overlay, OverlayContainer[] otherOverlays) private void showOverlayAboveOthers(OverlayContainer overlay, OverlayContainer[] otherOverlays)
@ -874,7 +864,7 @@ namespace osu.Game
if (recentLogCount < short_term_display_limit) if (recentLogCount < short_term_display_limit)
{ {
Schedule(() => notifications.Post(new SimpleErrorNotification Schedule(() => Notifications.Post(new SimpleErrorNotification
{ {
Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb, Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb,
Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty), Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty),
@ -882,7 +872,7 @@ namespace osu.Game
} }
else if (recentLogCount == short_term_display_limit) else if (recentLogCount == short_term_display_limit)
{ {
Schedule(() => notifications.Post(new SimpleNotification Schedule(() => Notifications.Post(new SimpleNotification
{ {
Icon = FontAwesome.Solid.EllipsisH, Icon = FontAwesome.Solid.EllipsisH,
Text = "Subsequent messages have been logged. Click to view log files.", Text = "Subsequent messages have been logged. Click to view log files.",
@ -1023,9 +1013,18 @@ namespace osu.Game
{ {
base.UpdateAfterChildren(); base.UpdateAfterChildren();
screenOffsetContainer.Padding = new MarginPadding { Top = ToolbarOffset }; ScreenOffsetContainer.Padding = new MarginPadding { Top = ToolbarOffset };
overlayContent.Padding = new MarginPadding { Top = ToolbarOffset }; overlayContent.Padding = new MarginPadding { Top = ToolbarOffset };
var horizontalOffset = 0f;
if (Settings.IsLoaded && Settings.IsPresent)
horizontalOffset += ToLocalSpace(Settings.ScreenSpaceDrawQuad.TopRight).X * SIDE_OVERLAY_OFFSET_RATIO;
if (Notifications.IsLoaded && Notifications.IsPresent)
horizontalOffset += (ToLocalSpace(Notifications.ScreenSpaceDrawQuad.TopLeft).X - DrawWidth) * SIDE_OVERLAY_OFFSET_RATIO;
ScreenOffsetContainer.X = horizontalOffset;
MenuCursorContainer.CanShowCursor = (ScreenStack.CurrentScreen as IOsuScreen)?.CursorVisible ?? false; MenuCursorContainer.CanShowCursor = (ScreenStack.CurrentScreen as IOsuScreen)?.CursorVisible ?? false;
} }

View File

@ -78,10 +78,10 @@ namespace osu.Game.Overlays.BeatmapSet
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Children = new[] Children = new[]
{ {
length = new Statistic(FontAwesome.Regular.Clock, "Length") { Width = 0.25f }, length = new Statistic(BeatmapStatisticsIconType.Length, "Length") { Width = 0.25f },
bpm = new Statistic(FontAwesome.Regular.Circle, "BPM") { Width = 0.25f }, bpm = new Statistic(BeatmapStatisticsIconType.Bpm, "BPM") { Width = 0.25f },
circleCount = new Statistic(FontAwesome.Regular.Circle, "Circle Count") { Width = 0.25f }, circleCount = new Statistic(BeatmapStatisticsIconType.Circles, "Circle Count") { Width = 0.25f },
sliderCount = new Statistic(FontAwesome.Regular.Circle, "Slider Count") { Width = 0.25f }, sliderCount = new Statistic(BeatmapStatisticsIconType.Sliders, "Slider Count") { Width = 0.25f },
}, },
}; };
} }
@ -104,7 +104,7 @@ namespace osu.Game.Overlays.BeatmapSet
set => this.value.Text = value; set => this.value.Text = value;
} }
public Statistic(IconUsage icon, string name) public Statistic(BeatmapStatisticsIconType icon, string name)
{ {
TooltipText = name; TooltipText = name;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
@ -133,8 +133,16 @@ namespace osu.Game.Overlays.BeatmapSet
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Icon = icon, Icon = FontAwesome.Regular.Circle,
Size = new Vector2(12), Size = new Vector2(10),
Rotation = 0,
Colour = Color4Extensions.FromHex(@"f7dd55"),
},
new BeatmapStatisticIcon(icon)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
Size = new Vector2(10),
Colour = Color4Extensions.FromHex(@"f7dd55"), Colour = Color4Extensions.FromHex(@"f7dd55"),
Scale = new Vector2(0.8f), Scale = new Vector2(0.8f),
}, },

View File

@ -24,7 +24,7 @@ namespace osu.Game.Overlays
public LocalisableString Title => NotificationsStrings.HeaderTitle; public LocalisableString Title => NotificationsStrings.HeaderTitle;
public LocalisableString Description => NotificationsStrings.HeaderDescription; public LocalisableString Description => NotificationsStrings.HeaderDescription;
private const float width = 320; public const float WIDTH = 320;
public const float TRANSITION_LENGTH = 600; public const float TRANSITION_LENGTH = 600;
@ -38,7 +38,8 @@ namespace osu.Game.Overlays
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Width = width; X = WIDTH;
Width = WIDTH;
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;
Children = new Drawable[] Children = new Drawable[]
@ -152,7 +153,7 @@ namespace osu.Game.Overlays
markAllRead(); markAllRead();
this.MoveToX(width, TRANSITION_LENGTH, Easing.OutQuint); this.MoveToX(WIDTH, TRANSITION_LENGTH, Easing.OutQuint);
this.FadeTo(0, TRANSITION_LENGTH, Easing.OutQuint); this.FadeTo(0, TRANSITION_LENGTH, Easing.OutQuint);
} }

View File

@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Show difficulty graph on progress bar", LabelText = "Show difficulty graph on progress bar",
Current = config.GetBindable<bool>(OsuSetting.ShowProgressGraph) Current = config.GetBindable<bool>(OsuSetting.ShowDifficultyGraph)
}, },
new SettingsCheckbox new SettingsCheckbox
{ {

View File

@ -9,7 +9,6 @@ using osu.Game.Overlays.Settings.Sections;
using osu.Game.Overlays.Settings.Sections.Input; using osu.Game.Overlays.Settings.Sections.Input;
using osuTK.Graphics; using osuTK.Graphics;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Localisation; using osu.Game.Localisation;
@ -38,6 +37,8 @@ namespace osu.Game.Overlays
private readonly List<SettingsSubPanel> subPanels = new List<SettingsSubPanel>(); private readonly List<SettingsSubPanel> subPanels = new List<SettingsSubPanel>();
private SettingsSubPanel lastOpenedSubPanel;
protected override Drawable CreateHeader() => new SettingsHeader(Title, Description); protected override Drawable CreateHeader() => new SettingsHeader(Title, Description);
protected override Drawable CreateFooter() => new SettingsFooter(); protected override Drawable CreateFooter() => new SettingsFooter();
@ -46,21 +47,21 @@ namespace osu.Game.Overlays
{ {
} }
public override bool AcceptsFocus => subPanels.All(s => s.State.Value != Visibility.Visible); public override bool AcceptsFocus => lastOpenedSubPanel == null || lastOpenedSubPanel.State.Value == Visibility.Hidden;
private T createSubPanel<T>(T subPanel) private T createSubPanel<T>(T subPanel)
where T : SettingsSubPanel where T : SettingsSubPanel
{ {
subPanel.Depth = 1; subPanel.Depth = 1;
subPanel.Anchor = Anchor.TopRight; subPanel.Anchor = Anchor.TopRight;
subPanel.State.ValueChanged += subPanelStateChanged; subPanel.State.ValueChanged += e => subPanelStateChanged(subPanel, e);
subPanels.Add(subPanel); subPanels.Add(subPanel);
return subPanel; return subPanel;
} }
private void subPanelStateChanged(ValueChangedEvent<Visibility> state) private void subPanelStateChanged(SettingsSubPanel panel, ValueChangedEvent<Visibility> state)
{ {
switch (state.NewValue) switch (state.NewValue)
{ {
@ -68,7 +69,9 @@ namespace osu.Game.Overlays
Sidebar?.FadeColour(Color4.DarkGray, 300, Easing.OutQuint); Sidebar?.FadeColour(Color4.DarkGray, 300, Easing.OutQuint);
SectionsContainer.FadeOut(300, Easing.OutQuint); SectionsContainer.FadeOut(300, Easing.OutQuint);
ContentContainer.MoveToX(-WIDTH, 500, Easing.OutQuint); ContentContainer.MoveToX(-PANEL_WIDTH, 500, Easing.OutQuint);
lastOpenedSubPanel = panel;
break; break;
case Visibility.Hidden: case Visibility.Hidden:
@ -80,7 +83,7 @@ namespace osu.Game.Overlays
} }
} }
protected override float ExpandedPosition => subPanels.Any(s => s.State.Value == Visibility.Visible) ? -WIDTH : base.ExpandedPosition; protected override float ExpandedPosition => lastOpenedSubPanel?.State.Value == Visibility.Visible ? -PANEL_WIDTH : base.ExpandedPosition;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()

View File

@ -28,7 +28,15 @@ namespace osu.Game.Overlays
private const float sidebar_width = Sidebar.DEFAULT_WIDTH; private const float sidebar_width = Sidebar.DEFAULT_WIDTH;
public const float WIDTH = 400; /// <summary>
/// The width of the settings panel content, excluding the sidebar.
/// </summary>
public const float PANEL_WIDTH = 400;
/// <summary>
/// The full width of the settings panel, including the sidebar.
/// </summary>
public const float WIDTH = sidebar_width + PANEL_WIDTH;
protected Container<Drawable> ContentContainer; protected Container<Drawable> ContentContainer;
@ -64,7 +72,8 @@ namespace osu.Game.Overlays
{ {
InternalChild = ContentContainer = new NonMaskedContent InternalChild = ContentContainer = new NonMaskedContent
{ {
Width = WIDTH, X = -WIDTH + ExpandedPosition,
Width = PANEL_WIDTH,
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Children = new Drawable[] Children = new Drawable[]
{ {

View File

@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using osu.Framework.Bindables;
using osu.Game.Configuration;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -11,9 +13,12 @@ namespace osu.Game.Rulesets.Mods
{ {
public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) }; public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) };
[SettingSource("Restart on fail", "Automatically restarts when failed.")]
public BindableBool Restart { get; } = new BindableBool();
public virtual bool PerformFail() => true; public virtual bool PerformFail() => true;
public virtual bool RestartOnFail => true; public virtual bool RestartOnFail => Restart.Value;
public void ApplyToHealthProcessor(HealthProcessor healthProcessor) public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
{ {

View File

@ -21,6 +21,11 @@ namespace osu.Game.Rulesets.Mods
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModSuddenDeath)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModSuddenDeath)).ToArray();
protected ModPerfect()
{
Restart.Value = Restart.Default = true;
}
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
=> result.Type.AffectsAccuracy() => result.Type.AffectsAccuracy()
&& result.Type != result.Judgement.MaxResult; && result.Type != result.Judgement.MaxResult;

View File

@ -10,12 +10,12 @@ namespace osu.Game.Screens.OnlinePlay.Components
{ {
public class OnlinePlayBackgroundSprite : OnlinePlayComposite public class OnlinePlayBackgroundSprite : OnlinePlayComposite
{ {
private readonly BeatmapSetCoverType beatmapSetCoverType; protected readonly BeatmapSetCoverType BeatmapSetCoverType;
private UpdateableBeatmapBackgroundSprite sprite; private UpdateableBeatmapBackgroundSprite sprite;
public OnlinePlayBackgroundSprite(BeatmapSetCoverType beatmapSetCoverType = BeatmapSetCoverType.Cover) public OnlinePlayBackgroundSprite(BeatmapSetCoverType beatmapSetCoverType = BeatmapSetCoverType.Cover)
{ {
this.beatmapSetCoverType = beatmapSetCoverType; BeatmapSetCoverType = beatmapSetCoverType;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -33,6 +33,6 @@ namespace osu.Game.Screens.OnlinePlay.Components
sprite.Beatmap.Value = Playlist.FirstOrDefault()?.Beatmap.Value; 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

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

@ -158,11 +158,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
Children = new Drawable[] Children = new Drawable[]
{ {
// This resolves internal 1px gaps due to applying the (parenting) corner radius and masking across multiple filling background sprites. // This resolves internal 1px gaps due to applying the (parenting) corner radius and masking across multiple filling background sprites.
new BufferedContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -172,8 +167,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}, },
}
},
new Container new Container
{ {
Name = @"Room content", Name = @"Room content",
@ -186,12 +179,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
Masking = true, Masking = true,
CornerRadius = corner_radius, CornerRadius = corner_radius,
Children = new Drawable[] Children = new Drawable[]
{
// This resolves internal 1px gaps due to applying the (parenting) corner radius and masking across multiple filling background sprites.
new BufferedContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{ {
new GridContainer new GridContainer
{ {
@ -217,8 +204,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
} }
} }
}, },
},
},
new Container new Container
{ {
Name = @"Left details", Name = @"Left details",

View File

@ -1,125 +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.UserInterface;
using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public abstract class FilterControl : CompositeDrawable
{
protected readonly FillFlowContainer Filters;
[Resolved(CanBeNull = true)]
private Bindable<FilterCriteria> filter { get; set; }
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; }
private readonly SearchTextBox search;
private readonly Dropdown<RoomStatusFilter> statusDropdown;
protected FilterControl()
{
RelativeSizeAxes = Axes.X;
Height = 70;
InternalChild = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(10),
Children = new Drawable[]
{
search = new FilterSearchTextBox
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.X,
Width = 0.6f,
},
Filters = new FillFlowContainer
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10),
Child = statusDropdown = new SlimEnumDropdown<RoomStatusFilter>
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.None,
Width = 160,
}
},
}
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
filter ??= new Bindable<FilterCriteria>();
}
protected override void LoadComplete()
{
base.LoadComplete();
search.Current.BindValueChanged(_ => updateFilterDebounced());
ruleset.BindValueChanged(_ => UpdateFilter());
statusDropdown.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 = statusDropdown.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,57 +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> categoryDropdown;
public PlaylistsFilterControl()
{
Filters.Add(categoryDropdown = new SlimEnumDropdown<PlaylistsCategory>
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.None,
Width = 160,
});
}
protected override void LoadComplete()
{
base.LoadComplete();
categoryDropdown.Current.BindValueChanged(_ => UpdateFilter());
}
protected override FilterCriteria CreateCriteria()
{
var criteria = base.CreateCriteria();
switch (categoryDropdown.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

@ -140,6 +140,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
roomFlow.Remove(toRemove); roomFlow.Remove(toRemove);
// selection may have a lease due to being in a sub screen.
if (!selectedRoom.Disabled)
selectedRoom.Value = null; selectedRoom.Value = null;
} }
} }
@ -152,6 +154,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)
{ {
if (!selectedRoom.Disabled)
selectedRoom.Value = null; selectedRoom.Value = null;
return base.OnClick(e); return base.OnClick(e);
} }
@ -214,6 +217,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
private void selectNext(int direction) private void selectNext(int direction)
{ {
if (selectedRoom.Disabled)
return;
var visibleRooms = Rooms.AsEnumerable().Where(r => r.IsPresent); var visibleRooms = Rooms.AsEnumerable().Where(r => r.IsPresent);
Room room; Room room;

View File

@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -9,18 +11,20 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Users; using osu.Game.Users;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Lounge namespace osu.Game.Screens.OnlinePlay.Lounge
{ {
@ -41,7 +45,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
private readonly IBindable<bool> initialRoomsReceived = new Bindable<bool>(); private readonly IBindable<bool> initialRoomsReceived = new Bindable<bool>();
private readonly IBindable<bool> operationInProgress = new Bindable<bool>(); private readonly IBindable<bool> operationInProgress = new Bindable<bool>();
private FilterControl filter;
private LoadingLayer loadingLayer; private LoadingLayer loadingLayer;
[Resolved] [Resolved]
@ -53,31 +56,37 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private OngoingOperationTracker ongoingOperationTracker { get; set; } private OngoingOperationTracker ongoingOperationTracker { get; set; }
[Resolved(CanBeNull = true)]
private Bindable<FilterCriteria> filter { get; set; }
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; }
[CanBeNull] [CanBeNull]
private IDisposable joiningRoomOperation { get; set; } private IDisposable joiningRoomOperation { get; set; }
private RoomsContainer roomsContainer; private RoomsContainer roomsContainer;
private SearchTextBox searchTextBox;
private Dropdown<RoomStatusFilter> statusDropdown;
[CanBeNull]
private LeasedBindable<Room> selectionLease;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
filter ??= new Bindable<FilterCriteria>(new FilterCriteria());
OsuScrollContainer scrollContainer; OsuScrollContainer scrollContainer;
InternalChildren = new Drawable[] InternalChildren = new[]
{ {
new Box loadingLayer = new LoadingLayer(true),
{
RelativeSizeAxes = Axes.X,
Height = 100,
Colour = Color4.Black,
Alpha = 0.5f,
},
new Container new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding Padding = new MarginPadding
{ {
Top = 20,
Left = WaveOverlayContainer.WIDTH_PADDING, Left = WaveOverlayContainer.WIDTH_PADDING,
Right = WaveOverlayContainer.WIDTH_PADDING, Right = WaveOverlayContainer.WIDTH_PADDING,
}, },
@ -86,28 +95,52 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
RowDimensions = new[] RowDimensions = new[]
{ {
new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.Absolute, Header.HEIGHT),
new Dimension(GridSizeMode.Absolute, 25),
new Dimension(GridSizeMode.Absolute, 20) new Dimension(GridSizeMode.Absolute, 20)
}, },
Content = new[] Content = new[]
{ {
new Drawable[]
{
searchTextBox = new LoungeSearchTextBox
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.X,
Width = 0.6f,
},
},
new Drawable[] new Drawable[]
{ {
new Container new Container
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.Both,
Height = 70, Depth = float.MinValue, // Contained filters should appear over the top of rooms.
Depth = -1,
Children = new Drawable[] Children = new Drawable[]
{ {
filter = CreateFilterControl(),
Buttons.WithChild(CreateNewRoomButton().With(d => Buttons.WithChild(CreateNewRoomButton().With(d =>
{ {
d.Size = new Vector2(150, 25); d.Anchor = Anchor.BottomLeft;
d.Origin = Anchor.BottomLeft;
d.Size = new Vector2(150, 37.5f);
d.Action = () => Open(); 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, null,
new Drawable[] new Drawable[]
@ -123,13 +156,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
ScrollbarOverlapsContent = false, ScrollbarOverlapsContent = false,
Child = roomsContainer = new RoomsContainer() Child = roomsContainer = new RoomsContainer()
}, },
loadingLayer = new LoadingLayer(true),
} }
}, },
} }
} }
}, },
} },
}; };
// scroll selected room into view on selection. // scroll selected room into view on selection.
@ -145,6 +177,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
{ {
base.LoadComplete(); base.LoadComplete();
searchTextBox.Current.BindValueChanged(_ => updateFilterDebounced());
ruleset.BindValueChanged(_ => UpdateFilter());
initialRoomsReceived.BindTo(RoomManager.InitialRoomsReceived); initialRoomsReceived.BindTo(RoomManager.InitialRoomsReceived);
initialRoomsReceived.BindValueChanged(_ => updateLoadingLayer()); initialRoomsReceived.BindValueChanged(_ => updateLoadingLayer());
@ -153,13 +188,50 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
operationInProgress.BindTo(ongoingOperationTracker.InProgress); operationInProgress.BindTo(ongoingOperationTracker.InProgress);
operationInProgress.BindValueChanged(_ => updateLoadingLayer(), true); operationInProgress.BindValueChanged(_ => updateLoadingLayer(), true);
} }
updateFilter();
} }
protected override void OnFocus(FocusEvent e) #region Filtering
protected void UpdateFilter() => Scheduler.AddOnce(updateFilter);
private ScheduledDelegate scheduledFilterUpdate;
private void updateFilterDebounced()
{ {
filter.TakeFocus(); 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>
{
RelativeSizeAxes = Axes.None,
Width = 160,
};
statusDropdown.Current.BindValueChanged(_ => UpdateFilter());
yield return statusDropdown;
}
#endregion
public override void OnEntering(IScreen last) public override void OnEntering(IScreen last)
{ {
base.OnEntering(last); base.OnEntering(last);
@ -171,6 +243,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
{ {
base.OnResuming(last); base.OnResuming(last);
Debug.Assert(selectionLease != null);
selectionLease.Return();
selectionLease = null;
if (selectedRoom.Value?.RoomID.Value == null) if (selectedRoom.Value?.RoomID.Value == null)
selectedRoom.Value = new Room(); selectedRoom.Value = new Room();
@ -191,14 +268,19 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
base.OnSuspending(next); base.OnSuspending(next);
} }
protected override void OnFocus(FocusEvent e)
{
searchTextBox.TakeFocus();
}
private void onReturning() private void onReturning()
{ {
filter.HoldFocus = true; searchTextBox.HoldFocus = true;
} }
private void onLeaving() private void onLeaving()
{ {
filter.HoldFocus = false; searchTextBox.HoldFocus = false;
// ensure any password prompt is dismissed. // ensure any password prompt is dismissed.
this.HidePopover(); this.HidePopover();
@ -238,13 +320,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
protected virtual void OpenNewRoom(Room room) protected virtual void OpenNewRoom(Room room)
{ {
selectedRoom.Value = room; selectionLease = selectedRoom.BeginLease(false);
Debug.Assert(selectionLease != null);
selectionLease.Value = room;
this.Push(CreateRoomSubScreen(room)); this.Push(CreateRoomSubScreen(room));
} }
protected abstract FilterControl CreateFilterControl();
protected abstract OsuButton CreateNewRoomButton(); protected abstract OsuButton CreateNewRoomButton();
/// <summary> /// <summary>
@ -262,5 +344,15 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
else else
loadingLayer.Hide(); loadingLayer.Hide();
} }
private class LoungeSearchTextBox : SearchTextBox
{
[BackgroundDependencyLoader]
private void load()
{
BackgroundUnfocused = OsuColour.Gray(0.06f);
BackgroundFocused = OsuColour.Gray(0.12f);
}
}
} }
} }

View File

@ -8,8 +8,10 @@ using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -62,8 +64,15 @@ namespace osu.Game.Screens.OnlinePlay.Match
protected RoomSubScreen() protected RoomSubScreen()
{ {
Padding = new MarginPadding { Top = Header.HEIGHT };
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
{ {
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"3e3a44") // This is super temporary.
},
BeatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker BeatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker
{ {
SelectedItem = { BindTarget = SelectedItem } SelectedItem = { BindTarget = SelectedItem }

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

@ -21,7 +21,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[Resolved] [Resolved]
private MultiplayerClient client { get; set; } private MultiplayerClient client { get; set; }
protected override FilterControl CreateFilterControl() => new MultiplayerFilterControl(); protected override FilterCriteria CreateFilterCriteria()
{
var criteria = base.CreateFilterCriteria();
criteria.Category = @"realtime";
return criteria;
}
protected override OsuButton CreateNewRoomButton() => new CreateMultiplayerMatchButton(); protected override OsuButton CreateNewRoomButton() => new CreateMultiplayerMatchButton();

View File

@ -274,7 +274,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
isConnected.BindValueChanged(connected => isConnected.BindValueChanged(connected =>
{ {
if (!connected.NewValue) if (!connected.NewValue)
Schedule(this.Exit); handleRoomLost();
}, true); }, true);
currentRoom.BindValueChanged(room => currentRoom.BindValueChanged(room =>
@ -284,7 +284,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
// the room has gone away. // the room has gone away.
// this could mean something happened during the join process, or an external connection issue occurred. // 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) // 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); }, true);
} }
@ -448,9 +448,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private void onRoomUpdated() 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); Scheduler.AddOnce(UpdateMods);
} }
private void handleRoomLost() => Schedule(() =>
{
if (this.IsCurrentScreen())
this.Exit();
else
ValidForResume = false;
});
private void onLoadRequested() private void onLoadRequested()
{ {
if (BeatmapAvailability.Value.State != DownloadState.LocallyAvailable) if (BeatmapAvailability.Value.State != DownloadState.LocallyAvailable)

View File

@ -181,7 +181,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override ResultsScreen CreateResults(ScoreInfo score) protected override ResultsScreen CreateResults(ScoreInfo score)
{ {
Debug.Assert(RoomId.Value != null); 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) protected override void Dispose(bool isDisposing)

View File

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

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Input; using osu.Game.Input;
@ -21,8 +22,9 @@ using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Users; using osu.Game.Users;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay namespace osu.Game.Screens.OnlinePlay
{ {
@ -71,9 +73,6 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private OsuLogo logo { get; set; } private OsuLogo logo { get; set; }
private Drawable header;
private Drawable headerBackground;
protected OnlinePlayScreen() protected OnlinePlayScreen()
{ {
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
@ -104,44 +103,23 @@ namespace osu.Game.Screens.OnlinePlay
new Container new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = Header.HEIGHT },
Children = new[]
{
header = new Container
{
RelativeSizeAxes = Axes.X,
Height = 400,
Children = new[]
{
headerBackground = new Container
{
RelativeSizeAxes = Axes.Both,
Width = 1.25f,
Masking = true,
Children = new Drawable[] Children = new Drawable[]
{ {
new HeaderBackgroundSprite new BeatmapBackgroundSprite
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.Both
Height = 400 // Keep a static height so the header doesn't change as it's resized between subscreens
}, },
} new Box
},
new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Bottom = -1 }, // 1px padding to avoid a 1px gap due to masking Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0.9f), Color4.Black.Opacity(0.6f))
Child = new Box },
screenStack = new OnlinePlaySubScreenStack
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both
Colour = ColourInfo.GradientVertical(backgroundColour.Opacity(0.5f), backgroundColour)
},
} }
} }
}, },
screenStack = new OnlinePlaySubScreenStack { RelativeSizeAxes = Axes.Both }
}
},
new Header(ScreenTitle, screenStack), new Header(ScreenTitle, screenStack),
RoomManager, RoomManager,
ongoingOperationTracker ongoingOperationTracker
@ -292,19 +270,6 @@ namespace osu.Game.Screens.OnlinePlay
private void subScreenChanged(IScreen lastScreen, IScreen newScreen) 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) if (lastScreen is IOsuScreen lastOsuScreen)
Activity.UnbindFrom(lastOsuScreen.Activity); Activity.UnbindFrom(lastOsuScreen.Activity);
@ -335,13 +300,48 @@ namespace osu.Game.Screens.OnlinePlay
} }
} }
private class HeaderBackgroundSprite : OnlinePlayBackgroundSprite private class BeatmapBackgroundSprite : OnlinePlayBackgroundSprite
{ {
protected override UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new BackgroundSprite { RelativeSizeAxes = Axes.Both }; protected override UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new BlurredBackgroundSprite(BeatmapSetCoverType) { RelativeSizeAxes = Axes.Both };
private class BackgroundSprite : UpdateableBeatmapBackgroundSprite public class BlurredBackgroundSprite : UpdateableBeatmapBackgroundSprite
{ {
protected override double TransformDuration => 200; public BlurredBackgroundSprite(BeatmapSetCoverType type)
: base(type)
{
}
protected override double LoadDelay => 200;
protected override Drawable CreateDrawable(BeatmapInfo model) =>
new BufferedLoader(base.CreateDrawable(model));
}
// This class is an unfortunate requirement due to `LongRunningLoad` requiring direct async loading.
// It means that if the web request fetching the beatmap background takes too long, it will suddenly appear.
internal class BufferedLoader : BufferedContainer
{
private readonly Drawable drawable;
public BufferedLoader(Drawable drawable)
{
this.drawable = drawable;
RelativeSizeAxes = Axes.Both;
BlurSigma = new Vector2(10);
FrameBufferScale = new Vector2(0.5f);
CacheDrawnFrameBuffer = true;
}
[BackgroundDependencyLoader]
private void load()
{
LoadComponentAsync(drawable, d =>
{
Add(d);
ForceRedraw();
});
}
} }
} }

View File

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

View File

@ -1,7 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
@ -16,7 +20,38 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
[Resolved] [Resolved]
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; }
protected override FilterControl CreateFilterControl() => new PlaylistsFilterControl(); 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 OsuButton CreateNewRoomButton() => new CreatePlaylistsRoomButton();
@ -30,5 +65,12 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
} }
protected override RoomSubScreen CreateRoomSubScreen(Room room) => new PlaylistsRoomSubScreen(room); protected override RoomSubScreen CreateRoomSubScreen(Room room) => new PlaylistsRoomSubScreen(room);
private enum PlaylistsCategory
{
Any,
Normal,
Spotlight
}
} }
} }

View File

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

View File

@ -81,7 +81,10 @@ namespace osu.Game.Screens.Play.HUD
[CanBeNull] [CanBeNull]
public User User { get; } public User User { get; }
private readonly bool trackedPlayer; /// <summary>
/// Whether this score is the local user or a replay player (and should be focused / always visible).
/// </summary>
public readonly bool Tracked;
private Container mainFillContainer; private Container mainFillContainer;
@ -97,11 +100,11 @@ namespace osu.Game.Screens.Play.HUD
/// Creates a new <see cref="GameplayLeaderboardScore"/>. /// Creates a new <see cref="GameplayLeaderboardScore"/>.
/// </summary> /// </summary>
/// <param name="user">The score's player.</param> /// <param name="user">The score's player.</param>
/// <param name="trackedPlayer">Whether the player is the local user or a replay player.</param> /// <param name="tracked">Whether the player is the local user or a replay player.</param>
public GameplayLeaderboardScore([CanBeNull] User user, bool trackedPlayer) public GameplayLeaderboardScore([CanBeNull] User user, bool tracked)
{ {
User = user; User = user;
this.trackedPlayer = trackedPlayer; Tracked = tracked;
AutoSizeAxes = Axes.X; AutoSizeAxes = Axes.X;
Height = PANEL_HEIGHT; Height = PANEL_HEIGHT;
@ -338,7 +341,7 @@ namespace osu.Game.Screens.Play.HUD
panelColour = BackgroundColour ?? Color4Extensions.FromHex("7fcc33"); panelColour = BackgroundColour ?? Color4Extensions.FromHex("7fcc33");
textColour = TextColour ?? Color4.White; textColour = TextColour ?? Color4.White;
} }
else if (trackedPlayer) else if (Tracked)
{ {
widthExtension = true; widthExtension = true;
panelColour = BackgroundColour ?? Color4Extensions.FromHex("ffd966"); panelColour = BackgroundColour ?? Color4Extensions.FromHex("ffd966");

View File

@ -7,6 +7,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -104,7 +105,7 @@ namespace osu.Game.Screens.Play.HUD
base.LoadComplete(); base.LoadComplete();
Team1Score.BindValueChanged(_ => updateScores()); Team1Score.BindValueChanged(_ => updateScores());
Team2Score.BindValueChanged(_ => updateScores()); Team2Score.BindValueChanged(_ => updateScores(), true);
} }
private void updateScores() private void updateScores()
@ -171,6 +172,8 @@ namespace osu.Game.Screens.Play.HUD
=> displayedSpriteText.Font = winning => displayedSpriteText.Font = winning
? OsuFont.Torus.With(weight: FontWeight.Bold, size: font_size, fixedWidth: true) ? OsuFont.Torus.With(weight: FontWeight.Bold, size: font_size, fixedWidth: true)
: OsuFont.Torus.With(weight: FontWeight.Regular, size: font_size * 0.8f, fixedWidth: true); : OsuFont.Torus.With(weight: FontWeight.Regular, size: font_size * 0.8f, fixedWidth: true);
protected override LocalisableString FormatCount(double count) => count.ToLocalisableString(@"N0");
} }
} }
} }

View File

@ -85,7 +85,7 @@ namespace osu.Game.Screens.Play.HUD
var trackedUser = UserScores[user.Id]; var trackedUser = UserScores[user.Id];
var leaderboardScore = AddPlayer(user, user.Id == api.LocalUser.Value.Id); var leaderboardScore = Add(user, user.Id == api.LocalUser.Value.Id);
leaderboardScore.Accuracy.BindTo(trackedUser.Accuracy); leaderboardScore.Accuracy.BindTo(trackedUser.Accuracy);
leaderboardScore.TotalScore.BindTo(trackedUser.Score); leaderboardScore.TotalScore.BindTo(trackedUser.Score);
leaderboardScore.Combo.BindTo(trackedUser.CurrentCombo); leaderboardScore.Combo.BindTo(trackedUser.CurrentCombo);
@ -184,7 +184,7 @@ namespace osu.Game.Screens.Play.HUD
continue; continue;
if (TeamScores.TryGetValue(u.Team.Value, out var team)) if (TeamScores.TryGetValue(u.Team.Value, out var team))
team.Value += (int)u.Score.Value; team.Value += (int)Math.Round(u.Score.Value);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -178,7 +178,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
Debug.Assert(Room != null); Debug.Assert(Room != null);
return ((IMultiplayerClient)this).UserLeft(Room.Users.Single(u => u.UserID == userId)); return ((IMultiplayerClient)this).UserKicked(Room.Users.Single(u => u.UserID == userId));
} }
public override async Task ChangeSettings(MultiplayerRoomSettings settings) public override async Task ChangeSettings(MultiplayerRoomSettings settings)

View File

@ -37,7 +37,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="10.3.0" /> <PackageReference Include="Realm" Version="10.3.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.813.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.813.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.810.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.813.0" />
<PackageReference Include="Sentry" Version="3.8.3" /> <PackageReference Include="Sentry" Version="3.8.3" />
<PackageReference Include="SharpCompress" Version="0.28.3" /> <PackageReference Include="SharpCompress" Version="0.28.3" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />

View File

@ -71,7 +71,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.813.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.813.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.810.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.813.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) --> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
<PropertyGroup> <PropertyGroup>