1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-15 13:33:03 +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" />
</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" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">

View File

@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
AddSliderStep<float>("circle size", 0, 8, 5, createCatcher);
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 many random fruit", () =>

View File

@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Catch
Banana,
Droplet,
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 (GetTexture(@"fruit-ryuuta") != null ||
GetTexture(@"fruit-ryuuta-0") != null)
if (hasOldStyleCatcherSprite())
return new LegacyCatcherOld();
}
if (GetTexture(@"fruit-catcher-idle") != null ||
GetTexture(@"fruit-catcher-idle-0") != null)
if (hasNewStyleCatcherSprite())
return new LegacyCatcherNew();
return null;
@ -86,12 +84,26 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
return new LegacyCatchComboCounter(Skin);
return null;
case CatchSkinComponents.HitExplosion:
if (hasOldStyleCatcherSprite() || hasNewStyleCatcherSprite())
return new LegacyHitExplosion();
return null;
}
}
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)
{
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
{
[Cached]
public class Catcher : SkinReloadableDrawable
{
/// <summary>
@ -106,7 +107,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// <summary>
/// Width of the area that can be used to attempt catches during gameplay.
/// </summary>
private readonly float catchWidth;
public readonly float CatchWidth;
private readonly SkinnableCatcher body;
@ -133,7 +134,7 @@ namespace osu.Game.Rulesets.Catch.UI
if (difficulty != null)
Scale = calculateScale(difficulty);
catchWidth = CalculateCatchWidth(Scale);
CatchWidth = CalculateCatchWidth(Scale);
InternalChildren = new Drawable[]
{
@ -193,7 +194,7 @@ namespace osu.Game.Rulesets.Catch.UI
if (!(hitObject is PalpableCatchHitObject fruit))
return false;
float halfCatchWidth = catchWidth * 0.5f;
float halfCatchWidth = CatchWidth * 0.5f;
return fruit.EffectiveX >= X - halfCatchWidth &&
fruit.EffectiveX <= X + halfCatchWidth;
}
@ -216,7 +217,7 @@ namespace osu.Game.Rulesets.Catch.UI
placeCaughtObject(palpableObject, positionInStack);
if (hitLighting.Value)
addLighting(hitObject, positionInStack.X, drawableObject.AccentColour.Value);
addLighting(result, drawableObject.AccentColour.Value, positionInStack.X);
}
// droplet doesn't affect the catcher state
@ -365,8 +366,8 @@ namespace osu.Game.Rulesets.Catch.UI
return position;
}
private void addLighting(CatchHitObject hitObject, float x, Color4 colour) =>
hitExplosionContainer.Add(new HitExplosionEntry(Time.Current, x, hitObject.Scale, colour, hitObject.RandomSeed));
private void addLighting(JudgementResult judgementResult, Color4 colour, float x) =>
hitExplosionContainer.Add(new HitExplosionEntry(Time.Current, judgementResult, colour, x));
private CaughtObject getCaughtObject(PalpableCatchHitObject source)
{

View File

@ -1,129 +1,56 @@
// 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.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.Skinning.Default;
using osu.Game.Rulesets.Objects.Pooling;
using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
using osu.Game.Skinning;
#nullable enable
namespace osu.Game.Rulesets.Catch.UI
{
public class HitExplosion : PoolableDrawableWithLifetime<HitExplosionEntry>
{
private readonly CircularContainer largeFaint;
private readonly CircularContainer smallFaint;
private readonly CircularContainer directionalGlow1;
private readonly CircularContainer directionalGlow2;
private readonly SkinnableDrawable skinnableExplosion;
public HitExplosion()
{
Size = new Vector2(20);
Anchor = Anchor.TopCentre;
RelativeSizeAxes = Axes.Both;
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
// scale roughly in-line with visual appearance of notes
const float initial_height = 10;
InternalChildren = new Drawable[]
InternalChild = skinnableExplosion = new SkinnableDrawable(new CatchSkinComponent(CatchSkinComponents.HitExplosion), _ => new DefaultHitExplosion())
{
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,
}
CentreComponent = false,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre
};
}
protected override void OnApply(HitExplosionEntry entry)
{
X = entry.Position;
Scale = new Vector2(entry.Scale);
setColour(entry.ObjectColour);
using (BeginAbsoluteSequence(entry.LifetimeStart))
applyTransforms(entry.RNGSeed);
base.OnApply(entry);
if (IsLoaded)
apply(entry);
}
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);
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).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,
};
(skinnableExplosion.Drawable as IHitExplosion)?.Animate(entry);
LifetimeEnd = skinnableExplosion.Drawable.LatestTransformEndTime;
}
}
}

View File

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

View File

@ -2,24 +2,42 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Performance;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Judgements;
using osuTK.Graphics;
#nullable enable
namespace osu.Game.Rulesets.Catch.UI
{
public class HitExplosionEntry : LifetimeEntry
{
public readonly float Position;
public readonly float Scale;
public readonly Color4 ObjectColour;
public readonly int RNGSeed;
/// <summary>
/// The judgement result that triggered this explosion.
/// </summary>
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;
Position = position;
Scale = scale;
JudgementResult = judgementResult;
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.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
@ -32,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Mods
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)
@ -128,8 +129,21 @@ namespace osu.Game.Rulesets.Osu.Mods
protected override void Update()
{
float start = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopLeft).X;
float end = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopRight).X;
float start, end;
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;

View File

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

View File

@ -26,6 +26,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
{
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 int currentIndex;
private IShader shader;
@ -141,22 +146,25 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
protected override bool OnMouseMove(MouseMoveEvent e)
{
Vector2 pos = e.ScreenSpaceMousePosition;
AddTrail(e.ScreenSpaceMousePosition);
return base.OnMouseMove(e);
}
if (lastPosition == null)
protected void AddTrail(Vector2 position)
{
if (InterpolateMovements)
{
lastPosition = pos;
resampler.AddPosition(lastPosition.Value);
return base.OnMouseMove(e);
}
foreach (Vector2 pos2 in resampler.AddPosition(pos))
{
Trace.Assert(lastPosition.HasValue);
if (InterpolateMovements)
if (!lastPosition.HasValue)
{
// ReSharper disable once PossibleInvalidOperationException
lastPosition = position;
resampler.AddPosition(lastPosition.Value);
return;
}
foreach (Vector2 pos2 in resampler.AddPosition(position))
{
Trace.Assert(lastPosition.HasValue);
Vector2 pos1 = lastPosition.Value;
Vector2 diff = pos2 - pos1;
float distance = diff.Length;
@ -170,14 +178,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
addPart(lastPosition.Value);
}
}
else
{
lastPosition = pos2;
addPart(lastPosition.Value);
}
}
return base.OnMouseMove(e);
else
{
lastPosition = position;
addPart(lastPosition.Value);
}
}
private void addPart(Vector2 screenSpacePosition)
@ -206,10 +212,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private Texture texture;
private float time;
private float fadeExponent;
private readonly TrailPart[] parts = new TrailPart[max_sprites];
private Vector2 size;
private Vector2 originPosition;
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;
size = Source.partSize;
time = Source.time;
fadeExponent = Source.FadeExponent;
originPosition = Vector2.Zero;
@ -249,6 +256,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
shader.Bind();
shader.GetUniform<float>("g_FadeClock").UpdateValue(ref time);
shader.GetUniform<float>("g_FadeExponent").UpdateValue(ref fadeExponent);
texture.TextureGL.Bind();

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@ -19,6 +20,7 @@ namespace osu.Game.Tests.Chat
{
private ChannelManager channelManager;
private int currentMessageId;
private List<Message> sentMessages;
[SetUp]
public void Setup() => Schedule(() =>
@ -34,6 +36,7 @@ namespace osu.Game.Tests.Chat
AddStep("register request handling", () =>
{
currentMessageId = 0;
sentMessages = new List<Message>();
((DummyAPIAccess)API).HandleRequest = req =>
{
@ -44,16 +47,11 @@ namespace osu.Game.Tests.Chat
return true;
case PostMessageRequest postMessage:
postMessage.TriggerSuccess(new Message(++currentMessageId)
{
IsAction = postMessage.Message.IsAction,
ChannelId = postMessage.Message.ChannelId,
Content = postMessage.Message.Content,
Links = postMessage.Message.Links,
Timestamp = postMessage.Message.Timestamp,
Sender = postMessage.Message.Sender
});
handlePostMessageRequest(postMessage);
return true;
case MarkChannelAsReadRequest markRead:
handleMarkChannelAsReadRequest(markRead);
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"));
}
[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())
{
Id = id,
Name = $"Channel {id}",
Topic = $"Topic of channel {id} with type {type}",
Type = type,
LastMessageId = 0,
};
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("toggle expanded", () => leaderboard.Expanded.Value = !leaderboard.Expanded.Value);
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 }));
}
[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 createLeaderboardScore(BindableDouble score, User user, bool isTracked = false)
{
var leaderboardScore = leaderboard.AddPlayer(user, isTracked);
var leaderboardScore = leaderboard.Add(user, isTracked);
leaderboardScore.TotalScore.BindTo(score);
}
private class TestGameplayLeaderboard : GameplayLeaderboard
{
public float Spacing => Flow.Spacing.Y;
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;
}

View File

@ -12,6 +12,7 @@ using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
@ -142,6 +143,22 @@ namespace osu.Game.Tests.Visual.Gameplay
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)
{
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()
{
// used to test the flow of multiplayer from visual tests.
AddStep("empty step", () => { });
}
[Test]
@ -408,8 +409,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
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());
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Platform;
using osu.Framework.Screens;
@ -95,6 +96,8 @@ namespace osu.Game.Tests.Visual.Navigation
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 BackButton BackButton => base.BackButton;
@ -103,7 +106,11 @@ namespace osu.Game.Tests.Visual.Navigation
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;

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]));
}
[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) =>
loungeScreen.ChildrenOfType<OsuScrollContainer>().First().ScreenSpaceDrawQuad
.Contains(room.ScreenSpaceDrawQuad.Centre);

View File

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

View File

@ -101,7 +101,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.HitLighting, true);
SetDefault(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always);
SetDefault(OsuSetting.ShowProgressGraph, true);
SetDefault(OsuSetting.ShowDifficultyGraph, true);
SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true);
SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true);
SetDefault(OsuSetting.KeyOverlay, false);
@ -217,7 +217,7 @@ namespace osu.Game.Configuration
AlwaysPlayFirstComboBreak,
FloatingComments,
HUDVisibilityMode,
ShowProgressGraph,
ShowDifficultyGraph,
ShowHealthDisplayWhenCantFail,
FadePlayfieldWhenHealthLow,
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
{
private readonly Channel channel;
private readonly Message message;
public readonly Channel Channel;
public readonly Message Message;
public MarkChannelAsReadRequest(Channel channel, Message message)
{
this.channel = channel;
this.message = message;
Channel = channel;
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()
{

View File

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

View File

@ -31,6 +31,15 @@ namespace osu.Game.Online.Multiplayer
/// <param name="user">The user.</param>
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>
/// Signal that the host of the room has changed.
/// </summary>

View File

@ -389,6 +389,18 @@ namespace osu.Game.Online.Multiplayer
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)
{
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<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined);
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<MultiplayerRoomSettings>(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged);
connection.On<int, MultiplayerUserState>(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged);

View File

@ -64,6 +64,11 @@ namespace osu.Game
/// </summary>
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;
private ChatOverlay chatOverlay;
@ -71,7 +76,7 @@ namespace osu.Game
private ChannelManager channelManager;
[NotNull]
private readonly NotificationOverlay notifications = new NotificationOverlay();
protected readonly NotificationOverlay Notifications = new NotificationOverlay();
private BeatmapListingOverlay beatmapListing;
@ -97,7 +102,7 @@ namespace osu.Game
private ScalingContainer screenContainer;
private Container screenOffsetContainer;
protected Container ScreenOffsetContainer { get; private set; }
[Resolved]
private FrameworkConfigManager frameworkConfig { get; set; }
@ -312,7 +317,7 @@ namespace osu.Game
case LinkAction.OpenEditorTimestamp:
case LinkAction.JoinMultiplayerMatch:
case LinkAction.Spectate:
waitForReady(() => notifications, _ => notifications.Post(new SimpleNotification
waitForReady(() => Notifications, _ => Notifications.Post(new SimpleNotification
{
Text = @"This link type is not yet supported!",
Icon = FontAwesome.Solid.LifeRing,
@ -611,12 +616,12 @@ namespace osu.Game
MenuCursorContainer.CanShowCursor = menuScreen?.CursorVisible ?? false;
// 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());
ScoreManager.PostNotification = n => notifications.Post(n);
ScoreManager.PostNotification = n => Notifications.Post(n);
ScoreManager.PresentImport = items => PresentScore(items.First());
// 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),
ScrollActionRequested = (action, amount, isPrecise) => volume.Adjust(action, amount, isPrecise),
},
screenOffsetContainer = new Container
ScreenOffsetContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
@ -724,7 +729,7 @@ namespace osu.Game
loadComponentSingleFile(onScreenDisplay, Add, true);
loadComponentSingleFile(notifications.With(d =>
loadComponentSingleFile(Notifications.With(d =>
{
d.GetToolbarHeight = () => ToolbarOffset;
d.Anchor = Anchor.TopRight;
@ -733,7 +738,7 @@ namespace osu.Game
loadComponentSingleFile(new CollectionManager(Storage)
{
PostNotification = n => notifications.Post(n),
PostNotification = n => Notifications.Post(n),
}, Add, true);
loadComponentSingleFile(stableImportManager, Add);
@ -785,7 +790,7 @@ namespace osu.Game
Add(new MusicKeyBindingHandler());
// side overlays which cancel each other.
var singleDisplaySideOverlays = new OverlayContainer[] { Settings, notifications };
var singleDisplaySideOverlays = new OverlayContainer[] { Settings, Notifications };
foreach (var overlay in singleDisplaySideOverlays)
{
@ -828,21 +833,6 @@ namespace osu.Game
{
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)
@ -874,7 +864,7 @@ namespace osu.Game
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,
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)
{
Schedule(() => notifications.Post(new SimpleNotification
Schedule(() => Notifications.Post(new SimpleNotification
{
Icon = FontAwesome.Solid.EllipsisH,
Text = "Subsequent messages have been logged. Click to view log files.",
@ -1023,9 +1013,18 @@ namespace osu.Game
{
base.UpdateAfterChildren();
screenOffsetContainer.Padding = new MarginPadding { Top = ToolbarOffset };
ScreenOffsetContainer.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;
}

View File

@ -78,10 +78,10 @@ namespace osu.Game.Overlays.BeatmapSet
Direction = FillDirection.Horizontal,
Children = new[]
{
length = new Statistic(FontAwesome.Regular.Clock, "Length") { Width = 0.25f },
bpm = new Statistic(FontAwesome.Regular.Circle, "BPM") { Width = 0.25f },
circleCount = new Statistic(FontAwesome.Regular.Circle, "Circle Count") { Width = 0.25f },
sliderCount = new Statistic(FontAwesome.Regular.Circle, "Slider Count") { Width = 0.25f },
length = new Statistic(BeatmapStatisticsIconType.Length, "Length") { Width = 0.25f },
bpm = new Statistic(BeatmapStatisticsIconType.Bpm, "BPM") { Width = 0.25f },
circleCount = new Statistic(BeatmapStatisticsIconType.Circles, "Circle 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;
}
public Statistic(IconUsage icon, string name)
public Statistic(BeatmapStatisticsIconType icon, string name)
{
TooltipText = name;
RelativeSizeAxes = Axes.X;
@ -133,8 +133,16 @@ namespace osu.Game.Overlays.BeatmapSet
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
Icon = icon,
Size = new Vector2(12),
Icon = FontAwesome.Regular.Circle,
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"),
Scale = new Vector2(0.8f),
},

View File

@ -24,7 +24,7 @@ namespace osu.Game.Overlays
public LocalisableString Title => NotificationsStrings.HeaderTitle;
public LocalisableString Description => NotificationsStrings.HeaderDescription;
private const float width = 320;
public const float WIDTH = 320;
public const float TRANSITION_LENGTH = 600;
@ -38,7 +38,8 @@ namespace osu.Game.Overlays
[BackgroundDependencyLoader]
private void load()
{
Width = width;
X = WIDTH;
Width = WIDTH;
RelativeSizeAxes = Axes.Y;
Children = new Drawable[]
@ -152,7 +153,7 @@ namespace osu.Game.Overlays
markAllRead();
this.MoveToX(width, TRANSITION_LENGTH, Easing.OutQuint);
this.MoveToX(WIDTH, 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
{
LabelText = "Show difficulty graph on progress bar",
Current = config.GetBindable<bool>(OsuSetting.ShowProgressGraph)
Current = config.GetBindable<bool>(OsuSetting.ShowDifficultyGraph)
},
new SettingsCheckbox
{

View File

@ -9,7 +9,6 @@ using osu.Game.Overlays.Settings.Sections;
using osu.Game.Overlays.Settings.Sections.Input;
using osuTK.Graphics;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Localisation;
@ -38,6 +37,8 @@ namespace osu.Game.Overlays
private readonly List<SettingsSubPanel> subPanels = new List<SettingsSubPanel>();
private SettingsSubPanel lastOpenedSubPanel;
protected override Drawable CreateHeader() => new SettingsHeader(Title, Description);
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)
where T : SettingsSubPanel
{
subPanel.Depth = 1;
subPanel.Anchor = Anchor.TopRight;
subPanel.State.ValueChanged += subPanelStateChanged;
subPanel.State.ValueChanged += e => subPanelStateChanged(subPanel, e);
subPanels.Add(subPanel);
return subPanel;
}
private void subPanelStateChanged(ValueChangedEvent<Visibility> state)
private void subPanelStateChanged(SettingsSubPanel panel, ValueChangedEvent<Visibility> state)
{
switch (state.NewValue)
{
@ -68,7 +69,9 @@ namespace osu.Game.Overlays
Sidebar?.FadeColour(Color4.DarkGray, 300, Easing.OutQuint);
SectionsContainer.FadeOut(300, Easing.OutQuint);
ContentContainer.MoveToX(-WIDTH, 500, Easing.OutQuint);
ContentContainer.MoveToX(-PANEL_WIDTH, 500, Easing.OutQuint);
lastOpenedSubPanel = panel;
break;
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]
private void load()

View File

@ -28,7 +28,15 @@ namespace osu.Game.Overlays
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;
@ -64,7 +72,8 @@ namespace osu.Game.Overlays
{
InternalChild = ContentContainer = new NonMaskedContent
{
Width = WIDTH,
X = -WIDTH + ExpandedPosition,
Width = PANEL_WIDTH,
RelativeSizeAxes = Axes.Y,
Children = new Drawable[]
{

View File

@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables;
using osu.Game.Configuration;
using osu.Game.Rulesets.Judgements;
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) };
[SettingSource("Restart on fail", "Automatically restarts when failed.")]
public BindableBool Restart { get; } = new BindableBool();
public virtual bool PerformFail() => true;
public virtual bool RestartOnFail => true;
public virtual bool RestartOnFail => Restart.Value;
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();
protected ModPerfect()
{
Restart.Value = Restart.Default = true;
}
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
=> result.Type.AffectsAccuracy()
&& result.Type != result.Judgement.MaxResult;

View File

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

View File

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

View File

@ -158,21 +158,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
Children = new Drawable[]
{
// This resolves internal 1px gaps due to applying the (parenting) corner radius and masking across multiple filling background sprites.
new BufferedContainer
new Box
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Background5,
},
new OnlinePlayBackgroundSprite
{
RelativeSizeAxes = Axes.Both
},
}
Colour = colours.Background5,
},
new OnlinePlayBackgroundSprite
{
RelativeSizeAxes = Axes.Both
},
new Container
{
@ -187,37 +180,29 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
CornerRadius = corner_radius,
Children = new Drawable[]
{
// This resolves internal 1px gaps due to applying the (parenting) corner radius and masking across multiple filling background sprites.
new BufferedContainer
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
ColumnDimensions = new[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Relative, 0.2f)
},
Content = new[]
{
new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Background5,
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(colours.Background5, colours.Background5.Opacity(0.3f))
},
}
}
},
new Dimension(GridSizeMode.Relative, 0.2f)
},
Content = new[]
{
new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Background5,
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(colours.Background5, colours.Background5.Opacity(0.3f))
},
}
}
},
new Container
{

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

View File

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

View File

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

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]
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();

View File

@ -274,7 +274,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
isConnected.BindValueChanged(connected =>
{
if (!connected.NewValue)
Schedule(this.Exit);
handleRoomLost();
}, true);
currentRoom.BindValueChanged(room =>
@ -284,7 +284,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
// the room has gone away.
// this could mean something happened during the join process, or an external connection issue occurred.
// one specific scenario is where the underlying room is created, but the signalr server returns an error during the join process. this triggers a PartRoom operation (see https://github.com/ppy/osu/blob/7654df94f6f37b8382be7dfcb4f674e03bd35427/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs#L97)
Schedule(this.Exit);
handleRoomLost();
}
}, true);
}
@ -448,9 +448,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private void onRoomUpdated()
{
// may happen if the client is kicked or otherwise removed from the room.
if (client.Room == null)
{
handleRoomLost();
return;
}
Scheduler.AddOnce(UpdateMods);
}
private void handleRoomLost() => Schedule(() =>
{
if (this.IsCurrentScreen())
this.Exit();
else
ValidForResume = false;
});
private void onLoadRequested()
{
if (BeatmapAvailability.Value.State != DownloadState.LocallyAvailable)

View File

@ -181,7 +181,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override ResultsScreen CreateResults(ScoreInfo score)
{
Debug.Assert(RoomId.Value != null);
return new MultiplayerResultsScreen(score, RoomId.Value.Value, PlaylistItem);
return leaderboard.TeamScores.Count == 2
? new MultiplayerTeamResultsScreen(score, RoomId.Value.Value, PlaylistItem, leaderboard.TeamScores)
: new MultiplayerResultsScreen(score, RoomId.Value.Value, PlaylistItem);
}
protected override void Dispose(bool isDisposing)

View File

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

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics.Containers;
using osu.Game.Input;
@ -21,8 +22,9 @@ using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Users;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay
{
@ -71,9 +73,6 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved(CanBeNull = true)]
private OsuLogo logo { get; set; }
private Drawable header;
private Drawable headerBackground;
protected OnlinePlayScreen()
{
Anchor = Anchor.Centre;
@ -104,42 +103,21 @@ namespace osu.Game.Screens.OnlinePlay
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = Header.HEIGHT },
Children = new[]
Children = new Drawable[]
{
header = new Container
new BeatmapBackgroundSprite
{
RelativeSizeAxes = Axes.X,
Height = 400,
Children = new[]
{
headerBackground = new Container
{
RelativeSizeAxes = Axes.Both,
Width = 1.25f,
Masking = true,
Children = new Drawable[]
{
new HeaderBackgroundSprite
{
RelativeSizeAxes = Axes.X,
Height = 400 // Keep a static height so the header doesn't change as it's resized between subscreens
},
}
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Bottom = -1 }, // 1px padding to avoid a 1px gap due to masking
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(backgroundColour.Opacity(0.5f), backgroundColour)
},
}
}
RelativeSizeAxes = Axes.Both
},
screenStack = new OnlinePlaySubScreenStack { RelativeSizeAxes = Axes.Both }
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0.9f), Color4.Black.Opacity(0.6f))
},
screenStack = new OnlinePlaySubScreenStack
{
RelativeSizeAxes = Axes.Both
}
}
},
new Header(ScreenTitle, screenStack),
@ -292,19 +270,6 @@ namespace osu.Game.Screens.OnlinePlay
private void subScreenChanged(IScreen lastScreen, IScreen newScreen)
{
switch (newScreen)
{
case LoungeSubScreen _:
header.Delay(OnlinePlaySubScreen.RESUME_TRANSITION_DELAY).ResizeHeightTo(400, OnlinePlaySubScreen.APPEAR_DURATION, Easing.OutQuint);
headerBackground.MoveToX(0, OnlinePlaySubScreen.X_MOVE_DURATION, Easing.OutQuint);
break;
case RoomSubScreen _:
header.ResizeHeightTo(135, OnlinePlaySubScreen.APPEAR_DURATION, Easing.OutQuint);
headerBackground.MoveToX(-OnlinePlaySubScreen.X_SHIFT, OnlinePlaySubScreen.X_MOVE_DURATION, Easing.OutQuint);
break;
}
if (lastScreen is IOsuScreen lastOsuScreen)
Activity.UnbindFrom(lastOsuScreen.Activity);
@ -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]
private void load()
{
LeftArea.Padding = new MarginPadding { Top = Header.HEIGHT };
initialBeatmap = Beatmap.Value;
initialRuleset = Ruleset.Value;
initialMods = Mods.Value.ToList();

View File

@ -1,7 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
@ -16,7 +20,38 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
[Resolved]
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();
@ -30,5 +65,12 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
}
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 osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers;
using osu.Game.Users;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD
{
public class GameplayLeaderboard : FillFlowContainer<GameplayLeaderboardScore>
public class GameplayLeaderboard : CompositeDrawable
{
private readonly int maxPanels;
private readonly Cached sorting = new Cached();
public Bindable<bool> Expanded = new Bindable<bool>();
public GameplayLeaderboard()
protected readonly FillFlowContainer<GameplayLeaderboardScore> Flow;
private bool requiresScroll;
private readonly OsuScrollContainer scroll;
private GameplayLeaderboardScore trackedScore;
/// <summary>
/// Create a new leaderboard.
/// </summary>
/// <param name="maxPanels">The maximum panels to show at once. Defines the maximum height of this component.</param>
public GameplayLeaderboard(int maxPanels = 8)
{
this.maxPanels = maxPanels;
Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH;
Direction = FillDirection.Vertical;
Spacing = new Vector2(2.5f);
LayoutDuration = 250;
LayoutEasing = Easing.OutQuint;
InternalChildren = new Drawable[]
{
scroll = new InputDisabledScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = Flow = new FillFlowContainer<GameplayLeaderboardScore>
{
RelativeSizeAxes = Axes.X,
X = GameplayLeaderboardScore.SHEAR_WIDTH,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(2.5f),
LayoutDuration = 450,
LayoutEasing = Easing.OutQuint,
}
}
};
}
protected override void LoadComplete()
@ -46,26 +75,87 @@ namespace osu.Game.Screens.Play.HUD
/// Whether the player should be tracked on the leaderboard.
/// Set to <c>true</c> for the local player or a player whose replay is currently being played.
/// </param>
public ILeaderboardScore AddPlayer([CanBeNull] User user, bool isTracked)
public ILeaderboardScore Add([CanBeNull] User user, bool isTracked)
{
var drawable = CreateLeaderboardScoreDrawable(user, isTracked);
if (isTracked)
{
if (trackedScore != null)
throw new InvalidOperationException("Cannot track more than one score.");
trackedScore = drawable;
}
drawable.Expanded.BindTo(Expanded);
base.Add(drawable);
Flow.Add(drawable);
drawable.TotalScore.BindValueChanged(_ => sorting.Invalidate(), true);
Height = Count * (GameplayLeaderboardScore.PANEL_HEIGHT + Spacing.Y);
int displayCount = Math.Min(Flow.Count, maxPanels);
Height = displayCount * (GameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y);
requiresScroll = displayCount != Flow.Count;
return drawable;
}
public void Clear()
{
Flow.Clear();
trackedScore = null;
scroll.ScrollToStart(false);
}
protected virtual GameplayLeaderboardScore CreateLeaderboardScoreDrawable(User user, bool 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()
@ -73,15 +163,26 @@ namespace osu.Game.Screens.Play.HUD
if (sorting.IsValid)
return;
var orderedByScore = this.OrderByDescending(i => i.TotalScore.Value).ToList();
var orderedByScore = Flow.OrderByDescending(i => i.TotalScore.Value).ToList();
for (int i = 0; i < Count; i++)
for (int i = 0; i < Flow.Count; i++)
{
SetLayoutPosition(orderedByScore[i], i);
Flow.SetLayoutPosition(orderedByScore[i], i);
orderedByScore[i].ScorePosition = i + 1;
}
sorting.Validate();
}
private class InputDisabledScrollContainer : OsuScrollContainer
{
public InputDisabledScrollContainer()
{
ScrollbarVisible = false;
}
public override bool HandlePositionalInput => false;
public override bool HandleNonPositionalInput => false;
}
}
}

View File

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

View File

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

View File

@ -85,7 +85,7 @@ namespace osu.Game.Screens.Play.HUD
var trackedUser = UserScores[user.Id];
var leaderboardScore = AddPlayer(user, user.Id == api.LocalUser.Value.Id);
var leaderboardScore = Add(user, user.Id == api.LocalUser.Value.Id);
leaderboardScore.Accuracy.BindTo(trackedUser.Accuracy);
leaderboardScore.TotalScore.BindTo(trackedUser.Score);
leaderboardScore.Combo.BindTo(trackedUser.CurrentCombo);
@ -184,7 +184,7 @@ namespace osu.Game.Screens.Play.HUD
continue;
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 readonly Container visibilityContainer;
private readonly BindableBool replayLoaded = new BindableBool();
private static bool hasShownNotificationOnce;
@ -72,7 +70,7 @@ namespace osu.Game.Screens.Play
private readonly SkinnableTargetContainer mainComponents;
private IEnumerable<Drawable> hideTargets => new Drawable[] { visibilityContainer, KeyCounter, topRightElements };
private IEnumerable<Drawable> hideTargets => new Drawable[] { mainComponents, KeyCounter, topRightElements };
public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods)
{
@ -84,13 +82,9 @@ namespace osu.Game.Screens.Play
Children = new Drawable[]
{
CreateFailingLayer(),
visibilityContainer = new Container
mainComponents = new SkinnableTargetContainer(SkinnableTarget.MainHUDComponents)
{
RelativeSizeAxes = Axes.Both,
Child = mainComponents = new SkinnableTargetContainer(SkinnableTarget.MainHUDComponents)
{
RelativeSizeAxes = Axes.Both,
},
},
topRightElements = new FillFlowContainer
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -178,7 +178,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
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)

View File

@ -37,7 +37,7 @@
</PackageReference>
<PackageReference Include="Realm" Version="10.3.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="SharpCompress" Version="0.28.3" />
<PackageReference Include="NUnit" Version="3.13.2" />

View File

@ -71,7 +71,7 @@
</ItemGroup>
<ItemGroup Label="Package References">
<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>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
<PropertyGroup>