merged latest ppy/master

This commit is contained in:
Xexxar 2021-08-16 22:20:12 +00:00
commit ae076c9a4e
206 changed files with 5332 additions and 1916 deletions

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.810.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.813.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.811.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.813.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
namespace osu.Game.Rulesets.Catch.UI
{
/// <summary>
/// Common interface for all hit explosion skinnables.
/// </summary>
public interface IHitExplosion
{
/// <summary>
/// Begins animating this <see cref="IHitExplosion"/>.
/// </summary>
void Animate(HitExplosionEntry entry);
}
}

View File

@ -41,6 +41,11 @@ namespace osu.Game.Rulesets.Osu.Edit
protected override GameplayCursorContainer CreateCursor() => null; protected override GameplayCursorContainer CreateCursor() => null;
public OsuEditorPlayfield()
{
HitPolicy = new AnyOrderHitPolicy();
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config)
{ {

View File

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

View File

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

View File

@ -0,0 +1,22 @@
// 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.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.UI
{
/// <summary>
/// An <see cref="IHitPolicy"/> which allows hitobjects to be hit in any order.
/// </summary>
public class AnyOrderHitPolicy : IHitPolicy
{
public IHitObjectContainer HitObjectContainer { get; set; }
public bool IsHittable(DrawableHitObject hitObject, double time) => true;
public void HandleHit(DrawableHitObject hitObject)
{
}
}
}

View File

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

View File

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

View File

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

View File

@ -204,7 +204,7 @@ namespace osu.Game.Tests.Gameplay
this.resources = resources; this.resources = resources;
} }
protected override ISkin GetSkin() => new TestSkin("test-sample", resources); protected internal override ISkin GetSkin() => new TestSkin("test-sample", resources);
} }
private class TestDrawableStoryboardSample : DrawableStoryboardSample private class TestDrawableStoryboardSample : DrawableStoryboardSample

View File

@ -133,11 +133,12 @@ namespace osu.Game.Tests.Skins
[Test] [Test]
public void TestEmptyComboColoursNoFallback() public void TestEmptyComboColoursNoFallback()
{ {
AddStep("Add custom combo colours to user skin", () => userSource.Configuration.AddComboColours( AddStep("Add custom combo colours to user skin", () => userSource.Configuration.CustomComboColours = new List<Color4>
{
new Color4(100, 150, 200, 255), new Color4(100, 150, 200, 255),
new Color4(55, 110, 166, 255), new Color4(55, 110, 166, 255),
new Color4(75, 125, 175, 255) new Color4(75, 125, 175, 255)
)); });
AddStep("Disallow default colours fallback in beatmap skin", () => beatmapSource.Configuration.AllowDefaultComboColoursFallback = false); AddStep("Disallow default colours fallback in beatmap skin", () => beatmapSource.Configuration.AllowDefaultComboColoursFallback = false);

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Testing;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Online; using osu.Game.Online;
using osuTK; using osuTK;
@ -15,6 +16,7 @@ using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Components namespace osu.Game.Tests.Visual.Components
{ {
[HeadlessTest]
public class TestScenePollingComponent : OsuTestScene public class TestScenePollingComponent : OsuTestScene
{ {
private Container pollBox; private Container pollBox;

View File

@ -111,7 +111,7 @@ namespace osu.Game.Tests.Visual.Gameplay
this.beatmapSkin = beatmapSkin; this.beatmapSkin = beatmapSkin;
} }
protected override ISkin GetSkin() => beatmapSkin; protected internal override ISkin GetSkin() => beatmapSkin;
} }
private class TestOsuRuleset : OsuRuleset private class TestOsuRuleset : OsuRuleset

View File

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

View File

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

View File

@ -0,0 +1,42 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Tests.Visual.Navigation;
namespace osu.Game.Tests.Visual.Menus
{
public class TestSceneSideOverlays : OsuGameTestScene
{
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
AddAssert("no screen offset applied", () => Game.ScreenOffsetContainer.X == 0f);
AddUntilStep("wait for overlays", () => Game.Settings.IsLoaded && Game.Notifications.IsLoaded);
}
[Test]
public void TestScreenOffsettingOnSettingsOverlay()
{
AddStep("open settings", () => Game.Settings.Show());
AddUntilStep("right screen offset applied", () => Game.ScreenOffsetContainer.X == SettingsPanel.WIDTH * TestOsuGame.SIDE_OVERLAY_OFFSET_RATIO);
AddStep("hide settings", () => Game.Settings.Hide());
AddUntilStep("screen offset removed", () => Game.ScreenOffsetContainer.X == 0f);
}
[Test]
public void TestScreenOffsettingOnNotificationOverlay()
{
AddStep("open notifications", () => Game.Notifications.Show());
AddUntilStep("right screen offset applied", () => Game.ScreenOffsetContainer.X == -NotificationOverlay.WIDTH * TestOsuGame.SIDE_OVERLAY_OFFSET_RATIO);
AddStep("hide notifications", () => Game.Notifications.Hide());
AddUntilStep("screen offset removed", () => Game.ScreenOffsetContainer.X == 0f);
}
}
}

View File

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

View File

@ -0,0 +1,168 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Overlays;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Tests.Beatmaps;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneDrawableRoom : OsuTestScene
{
[Cached]
private readonly Bindable<Room> selectedRoom = new Bindable<Room>();
[Cached]
protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
[Test]
public void TestMultipleStatuses()
{
AddStep("create rooms", () =>
{
Child = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.9f),
Spacing = new Vector2(10),
Children = new Drawable[]
{
createDrawableRoom(new Room
{
Name = { Value = "Flyte's Trash Playlist" },
Status = { Value = new RoomStatusOpen() },
EndDate = { Value = DateTimeOffset.Now.AddDays(1) },
Playlist =
{
new PlaylistItem
{
Beatmap =
{
Value = new TestBeatmap(new OsuRuleset().RulesetInfo)
{
BeatmapInfo =
{
StarDifficulty = 2.5
}
}.BeatmapInfo,
}
}
}
}),
createDrawableRoom(new Room
{
Name = { Value = "Room 2" },
Status = { Value = new RoomStatusPlaying() },
EndDate = { Value = DateTimeOffset.Now.AddDays(1) },
Playlist =
{
new PlaylistItem
{
Beatmap =
{
Value = new TestBeatmap(new OsuRuleset().RulesetInfo)
{
BeatmapInfo =
{
StarDifficulty = 2.5
}
}.BeatmapInfo,
}
},
new PlaylistItem
{
Beatmap =
{
Value = new TestBeatmap(new OsuRuleset().RulesetInfo)
{
BeatmapInfo =
{
StarDifficulty = 4.5
}
}.BeatmapInfo,
}
}
}
}),
createDrawableRoom(new Room
{
Name = { Value = "Room 3" },
Status = { Value = new RoomStatusEnded() },
EndDate = { Value = DateTimeOffset.Now },
}),
createDrawableRoom(new Room
{
Name = { Value = "Room 4 (realtime)" },
Status = { Value = new RoomStatusOpen() },
Category = { Value = RoomCategory.Realtime },
}),
createDrawableRoom(new Room
{
Name = { Value = "Room 4 (spotlight)" },
Status = { Value = new RoomStatusOpen() },
Category = { Value = RoomCategory.Spotlight },
}),
}
};
});
}
[Test]
public void TestEnableAndDisablePassword()
{
DrawableRoom drawableRoom = null;
Room room = null;
AddStep("create room", () => Child = drawableRoom = createDrawableRoom(room = new Room
{
Name = { Value = "Room with password" },
Status = { Value = new RoomStatusOpen() },
Category = { Value = RoomCategory.Realtime },
}));
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
AddStep("set password", () => room.Password.Value = "password");
AddAssert("password icon visible", () => Precision.AlmostEquals(1, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
AddStep("unset password", () => room.Password.Value = string.Empty);
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
}
private DrawableRoom createDrawableRoom(Room room)
{
room.Host.Value ??= new User { Username = "peppy", Id = 2 };
if (room.RecentParticipants.Count == 0)
{
room.RecentParticipants.AddRange(Enumerable.Range(0, 20).Select(i => new User
{
Id = i,
Username = $"User {i}"
}));
}
var drawableRoom = new DrawableRoom(room) { MatchingFilter = true };
drawableRoom.Action = () => drawableRoom.State = drawableRoom.State == SelectionState.Selected ? SelectionState.NotSelected : SelectionState.Selected;
return drawableRoom;
}
}
}

View File

@ -1,49 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneLoungeRoomInfo : OnlinePlayTestScene
{
[SetUp]
public new void Setup() => Schedule(() =>
{
SelectedRoom.Value = new Room();
Child = new RoomInfo
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 500
};
});
[Test]
public void TestNonSelectedRoom()
{
AddStep("set null room", () => SelectedRoom.Value.RoomID.Value = null);
}
[Test]
public void TestOpenRoom()
{
AddStep("set open room", () =>
{
SelectedRoom.Value.RoomID.Value = 0;
SelectedRoom.Value.Name.Value = "Room 0";
SelectedRoom.Value.Host.Value = new User { Username = "peppy", Id = 2 };
SelectedRoom.Value.EndDate.Value = DateTimeOffset.Now.AddMonths(1);
SelectedRoom.Value.Status.Value = new RoomStatusOpen();
});
}
}
}

View File

@ -6,9 +6,11 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
@ -31,7 +33,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
}; };
foreach (var (userId, _) in clocks) foreach (var (userId, _) in clocks)
{
SpectatorClient.StartPlay(userId, 0); SpectatorClient.StartPlay(userId, 0);
OnlinePlayDependencies.Client.AddUser(new User { Id = userId });
}
}); });
AddStep("create leaderboard", () => AddStep("create leaderboard", () =>
@ -41,7 +46,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
var scoreProcessor = new OsuScoreProcessor(); var scoreProcessor = new OsuScoreProcessor();
scoreProcessor.ApplyBeatmap(playable); scoreProcessor.ApplyBeatmap(playable);
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, clocks.Keys.ToArray()) { Expanded = { Value = true } }, Add); LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray()) { Expanded = { Value = true } }, Add);
}); });
AddUntilStep("wait for load", () => leaderboard.IsLoaded); AddUntilStep("wait for load", () => leaderboard.IsLoaded);

View File

@ -8,6 +8,8 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -26,7 +28,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private MultiSpectatorScreen spectatorScreen; private MultiSpectatorScreen spectatorScreen;
private readonly List<int> playingUserIds = new List<int>(); private readonly List<MultiplayerRoomUser> playingUsers = new List<MultiplayerRoomUser>();
private BeatmapSetInfo importedSet; private BeatmapSetInfo importedSet;
private BeatmapInfo importedBeatmap; private BeatmapInfo importedBeatmap;
@ -41,7 +43,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
[SetUp] [SetUp]
public new void Setup() => Schedule(() => playingUserIds.Clear()); public new void Setup() => Schedule(() => playingUsers.Clear());
[Test] [Test]
public void TestDelayedStart() public void TestDelayedStart()
@ -51,8 +53,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_1_ID }, true); OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_1_ID }, true);
OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_2_ID }, true); OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_2_ID }, true);
playingUserIds.Add(PLAYER_1_ID); playingUsers.Add(new MultiplayerRoomUser(PLAYER_1_ID));
playingUserIds.Add(PLAYER_2_ID); playingUsers.Add(new MultiplayerRoomUser(PLAYER_2_ID));
}); });
loadSpectateScreen(false); loadSpectateScreen(false);
@ -78,6 +80,38 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddWaitStep("wait a bit", 20); AddWaitStep("wait a bit", 20);
} }
[Test]
public void TestTeamDisplay()
{
AddStep("start players", () =>
{
var player1 = OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_1_ID }, true);
player1.MatchState = new TeamVersusUserState
{
TeamID = 0,
};
var player2 = OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_2_ID }, true);
player2.MatchState = new TeamVersusUserState
{
TeamID = 1,
};
SpectatorClient.StartPlay(player1.UserID, importedBeatmapId);
SpectatorClient.StartPlay(player2.UserID, importedBeatmapId);
playingUsers.Add(player1);
playingUsers.Add(player2);
});
loadSpectateScreen();
sendFrames(PLAYER_1_ID, 1000);
sendFrames(PLAYER_2_ID, 1000);
AddWaitStep("wait a bit", 20);
}
[Test] [Test]
public void TestTimeDoesNotProgressWhileAllPlayersPaused() public void TestTimeDoesNotProgressWhileAllPlayersPaused()
{ {
@ -254,7 +288,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Beatmap.Value = beatmapManager.GetWorkingBeatmap(importedBeatmap); Beatmap.Value = beatmapManager.GetWorkingBeatmap(importedBeatmap);
Ruleset.Value = importedBeatmap.Ruleset; Ruleset.Value = importedBeatmap.Ruleset;
LoadScreen(spectatorScreen = new MultiSpectatorScreen(playingUserIds.ToArray())); LoadScreen(spectatorScreen = new MultiSpectatorScreen(playingUsers.ToArray()));
}); });
AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded)); AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded));
@ -269,7 +303,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
OnlinePlayDependencies.Client.AddUser(new User { Id = id }, true); OnlinePlayDependencies.Client.AddUser(new User { Id = id }, true);
SpectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId); SpectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId);
playingUserIds.Add(id); playingUsers.Add(new MultiplayerRoomUser(id));
} }
}); });
} }

View File

@ -25,7 +25,7 @@ using osu.Game.Screens;
using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
@ -87,6 +87,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestEmpty() public void TestEmpty()
{ {
// used to test the flow of multiplayer from visual tests. // used to test the flow of multiplayer from visual tests.
AddStep("empty step", () => { });
} }
[Test] [Test]
@ -312,6 +313,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddUntilStep("wait for spectating user state", () => client.LocalUser?.State == MultiplayerUserState.Spectating);
AddStep("start match externally", () => client.StartMatch()); AddStep("start match externally", () => client.StartMatch());
AddAssert("play not started", () => multiplayerScreen.IsCurrentScreen()); AddAssert("play not started", () => multiplayerScreen.IsCurrentScreen());
@ -348,6 +351,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddUntilStep("wait for spectating user state", () => client.LocalUser?.State == MultiplayerUserState.Spectating);
AddStep("start match externally", () => client.StartMatch()); AddStep("start match externally", () => client.StartMatch());
AddStep("restore beatmap", () => AddStep("restore beatmap", () =>
@ -396,7 +401,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
}); });
AddStep("open mod overlay", () => this.ChildrenOfType<PurpleTriangleButton>().ElementAt(2).TriggerClick()); AddStep("open mod overlay", () => this.ChildrenOfType<RoomSubScreen.UserModSelectButton>().Single().TriggerClick());
AddStep("invoke on back button", () => multiplayerScreen.OnBackButton()); AddStep("invoke on back button", () => multiplayerScreen.OnBackButton());
@ -404,8 +409,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden); AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden);
testLeave("lounge tab item", () => this.ChildrenOfType<BreadcrumbControl<IScreen>.BreadcrumbTabItem>().First().TriggerClick());
testLeave("back button", () => multiplayerScreen.OnBackButton()); testLeave("back button", () => multiplayerScreen.OnBackButton());
// mimics home button and OS window close // mimics home button and OS window close
@ -423,10 +426,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void createRoom(Func<Room> room) private void createRoom(Func<Room> room)
{ {
AddStep("open room", () => AddUntilStep("wait for lounge", () => multiplayerScreen.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true);
{ AddStep("open room", () => multiplayerScreen.ChildrenOfType<LoungeSubScreen>().Single().Open(room()));
multiplayerScreen.OpenNewRoom(room());
});
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true); AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddWaitStep("wait for transition", 2); AddWaitStep("wait for transition", 2);

View File

@ -12,6 +12,7 @@ using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator; using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy; using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Scoring;
@ -51,12 +52,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
OsuScoreProcessor scoreProcessor; OsuScoreProcessor scoreProcessor;
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
var multiplayerUsers = new List<MultiplayerRoomUser>();
foreach (var user in users) foreach (var user in users)
{ {
SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
OnlinePlayDependencies.Client.AddUser(new User { Id = user }, true); multiplayerUsers.Add(OnlinePlayDependencies.Client.AddUser(new User { Id = user }, true));
} }
Children = new Drawable[] Children = new Drawable[]
@ -64,9 +66,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
scoreProcessor = new OsuScoreProcessor(), scoreProcessor = new OsuScoreProcessor(),
}; };
scoreProcessor.ApplyBeatmap(playable); scoreProcessor.ApplyBeatmap(playableBeatmap);
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, users.ToArray()) LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, multiplayerUsers.ToArray())
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -0,0 +1,121 @@
// 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.Graphics;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Tests.Visual.Spectator;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerGameplayLeaderboardTeams : MultiplayerTestScene
{
private static IEnumerable<int> users => Enumerable.Range(0, 16);
public new TestSceneMultiplayerGameplayLeaderboard.TestMultiplayerSpectatorClient SpectatorClient =>
(TestSceneMultiplayerGameplayLeaderboard.TestMultiplayerSpectatorClient)OnlinePlayDependencies?.SpectatorClient;
protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies();
protected class TestDependencies : MultiplayerTestSceneDependencies
{
protected override TestSpectatorClient CreateSpectatorClient() => new TestSceneMultiplayerGameplayLeaderboard.TestMultiplayerSpectatorClient();
}
private MultiplayerGameplayLeaderboard leaderboard;
private GameplayMatchScoreDisplay gameplayScoreDisplay;
protected override Room CreateRoom()
{
var room = base.CreateRoom();
room.Type.Value = MatchType.TeamVersus;
return room;
}
[SetUpSteps]
public override void SetUpSteps()
{
AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = LookupCache.GetUserAsync(1).Result);
AddStep("create leaderboard", () =>
{
leaderboard?.Expire();
OsuScoreProcessor scoreProcessor;
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
var multiplayerUsers = new List<MultiplayerRoomUser>();
foreach (var user in users)
{
SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
var roomUser = OnlinePlayDependencies.Client.AddUser(new User { Id = user }, true);
roomUser.MatchState = new TeamVersusUserState
{
TeamID = RNG.Next(0, 2)
};
multiplayerUsers.Add(roomUser);
}
Children = new Drawable[]
{
scoreProcessor = new OsuScoreProcessor(),
};
scoreProcessor.ApplyBeatmap(playableBeatmap);
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, multiplayerUsers.ToArray())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}, gameplayLeaderboard =>
{
LoadComponentAsync(new MatchScoreDisplay
{
Team1Score = { BindTarget = leaderboard.TeamScores[0] },
Team2Score = { BindTarget = leaderboard.TeamScores[1] }
}, Add);
LoadComponentAsync(gameplayScoreDisplay = new GameplayMatchScoreDisplay
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Team1Score = { BindTarget = leaderboard.TeamScores[0] },
Team2Score = { BindTarget = leaderboard.TeamScores[1] }
}, Add);
Add(gameplayLeaderboard);
});
});
AddUntilStep("wait for load", () => leaderboard.IsLoaded);
AddUntilStep("wait for user population", () => Client.CurrentMatchPlayingUserIds.Count > 0);
}
[Test]
public void TestScoreUpdates()
{
AddRepeatStep("update state", () => SpectatorClient.RandomlyUpdateState(), 100);
AddToggleStep("switch compact mode", expanded =>
{
leaderboard.Expanded.Value = expanded;
gameplayScoreDisplay.Expanded.Value = expanded;
});
}
}
}

View File

@ -129,6 +129,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddUntilStep("wait for spectating user state", () => Client.LocalUser?.State == MultiplayerUserState.Spectating);
AddUntilStep("wait for ready button to be enabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().ChildrenOfType<ReadyButton>().Single().Enabled.Value); AddUntilStep("wait for ready button to be enabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().ChildrenOfType<ReadyButton>().Single().Enabled.Value);
AddStep("click ready button", () => AddStep("click ready button", () =>

View File

@ -4,6 +4,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -48,9 +49,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
AddAssert("one unique panel", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.User).Distinct().Count() == 1); AddAssert("one unique panel", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.User).Distinct().Count() == 1);
AddStep("add non-resolvable user", () => Client.AddNullUser(-3)); AddStep("add non-resolvable user", () => Client.AddNullUser());
AddAssert("null user added", () => Client.Room.AsNonNull().Users.Count(u => u.User == null) == 1);
AddUntilStep("two unique panels", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.User).Distinct().Count() == 2); AddUntilStep("two unique panels", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.User).Distinct().Count() == 2);
AddStep("kick null user", () => this.ChildrenOfType<ParticipantPanel>().Single(p => p.User.User == null)
.ChildrenOfType<ParticipantPanel.KickButton>().Single().TriggerClick());
AddAssert("null user kicked", () => Client.Room.AsNonNull().Users.Count == 1);
} }
[Test] [Test]
@ -155,6 +162,42 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("second user crown visible", () => this.ChildrenOfType<ParticipantPanel>().ElementAt(1).ChildrenOfType<SpriteIcon>().First().Alpha == 1); AddUntilStep("second user crown visible", () => this.ChildrenOfType<ParticipantPanel>().ElementAt(1).ChildrenOfType<SpriteIcon>().First().Alpha == 1);
} }
[Test]
public void TestKickButtonOnlyPresentWhenHost()
{
AddStep("add user", () => Client.AddUser(new User
{
Id = 3,
Username = "Second",
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}));
AddUntilStep("kick buttons visible", () => this.ChildrenOfType<ParticipantPanel.KickButton>().Count(d => d.IsPresent) == 1);
AddStep("make second user host", () => Client.TransferHost(3));
AddUntilStep("kick buttons not visible", () => this.ChildrenOfType<ParticipantPanel.KickButton>().Count(d => d.IsPresent) == 0);
AddStep("make local user host again", () => Client.TransferHost(API.LocalUser.Value.Id));
AddUntilStep("kick buttons visible", () => this.ChildrenOfType<ParticipantPanel.KickButton>().Count(d => d.IsPresent) == 1);
}
[Test]
public void TestKickButtonKicks()
{
AddStep("add user", () => Client.AddUser(new User
{
Id = 3,
Username = "Second",
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}));
AddStep("kick second user", () => this.ChildrenOfType<ParticipantPanel.KickButton>().Single(d => d.IsPresent).TriggerClick());
AddAssert("second user kicked", () => Client.Room?.Users.Single().UserID == API.LocalUser.Value.Id);
}
[Test] [Test]
public void TestManyUsers() public void TestManyUsers()
{ {

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

@ -0,0 +1,95 @@
// 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.Graphics;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneRankRangePill : MultiplayerTestScene
{
[SetUp]
public new void Setup() => Schedule(() =>
{
Child = new RankRangePill
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
};
});
[Test]
public void TestSingleUser()
{
AddStep("add user", () =>
{
Client.AddUser(new User
{
Id = 2,
Statistics = { GlobalRank = 1234 }
});
// Remove the local user so only the one above is displayed.
Client.RemoveUser(API.LocalUser.Value);
});
}
[Test]
public void TestMultipleUsers()
{
AddStep("add users", () =>
{
Client.AddUser(new User
{
Id = 2,
Statistics = { GlobalRank = 1234 }
});
Client.AddUser(new User
{
Id = 3,
Statistics = { GlobalRank = 3333 }
});
Client.AddUser(new User
{
Id = 4,
Statistics = { GlobalRank = 4321 }
});
// Remove the local user so only the ones above are displayed.
Client.RemoveUser(API.LocalUser.Value);
});
}
[TestCase(1, 10)]
[TestCase(10, 100)]
[TestCase(100, 1000)]
[TestCase(1000, 10000)]
[TestCase(10000, 100000)]
[TestCase(100000, 1000000)]
[TestCase(1000000, 10000000)]
public void TestRange(int min, int max)
{
AddStep("add users", () =>
{
Client.AddUser(new User
{
Id = 2,
Statistics = { GlobalRank = min }
});
Client.AddUser(new User
{
Id = 3,
Statistics = { GlobalRank = max }
});
// Remove the local user so only the ones above are displayed.
Client.RemoveUser(API.LocalUser.Value);
});
}
}
}

View File

@ -0,0 +1,143 @@
// 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;
using osu.Framework.Testing;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Users;
using osu.Game.Users.Drawables;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneRecentParticipantsList : OnlinePlayTestScene
{
private RecentParticipantsList list;
[SetUp]
public new void Setup() => Schedule(() =>
{
SelectedRoom.Value = new Room { Name = { Value = "test room" } };
Child = list = new RecentParticipantsList
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
NumberOfCircles = 4
};
});
[Test]
public void TestCircleCountNearLimit()
{
AddStep("add 8 users", () =>
{
for (int i = 0; i < 8; i++)
addUser(i);
});
AddStep("set 8 circles", () => list.NumberOfCircles = 8);
AddAssert("0 hidden users", () => list.ChildrenOfType<RecentParticipantsList.HiddenUserCount>().Single().Count == 0);
AddStep("add one more user", () => addUser(9));
AddAssert("2 hidden users", () => list.ChildrenOfType<RecentParticipantsList.HiddenUserCount>().Single().Count == 2);
AddStep("remove first user", () => removeUserAt(0));
AddAssert("0 hidden users", () => list.ChildrenOfType<RecentParticipantsList.HiddenUserCount>().Single().Count == 0);
AddStep("add one more user", () => addUser(9));
AddAssert("2 hidden users", () => list.ChildrenOfType<RecentParticipantsList.HiddenUserCount>().Single().Count == 2);
AddStep("remove last user", () => removeUserAt(8));
AddAssert("0 hidden users", () => list.ChildrenOfType<RecentParticipantsList.HiddenUserCount>().Single().Count == 0);
}
[Test]
public void TestHiddenUsersBecomeDisplayed()
{
AddStep("add 8 users", () =>
{
for (int i = 0; i < 8; i++)
addUser(i);
});
AddStep("set 3 circles", () => list.NumberOfCircles = 3);
for (int i = 0; i < 8; i++)
{
AddStep("remove user", () => removeUserAt(0));
int remainingUsers = 7 - i;
int displayedUsers = remainingUsers > 3 ? 2 : remainingUsers;
AddAssert($"{displayedUsers} avatars displayed", () => list.ChildrenOfType<UpdateableAvatar>().Count() == displayedUsers);
}
}
[Test]
public void TestCircleCount()
{
AddStep("add 50 users", () =>
{
for (int i = 0; i < 50; i++)
addUser(i);
});
AddStep("set 3 circles", () => list.NumberOfCircles = 3);
AddAssert("2 users displayed", () => list.ChildrenOfType<UpdateableAvatar>().Count() == 2);
AddAssert("48 hidden users", () => list.ChildrenOfType<RecentParticipantsList.HiddenUserCount>().Single().Count == 48);
AddStep("set 10 circles", () => list.NumberOfCircles = 10);
AddAssert("9 users displayed", () => list.ChildrenOfType<UpdateableAvatar>().Count() == 9);
AddAssert("41 hidden users", () => list.ChildrenOfType<RecentParticipantsList.HiddenUserCount>().Single().Count == 41);
}
[Test]
public void TestAddAndRemoveUsers()
{
AddStep("add 50 users", () =>
{
for (int i = 0; i < 50; i++)
addUser(i);
});
AddStep("remove from start", () => removeUserAt(0));
AddAssert("3 circles displayed", () => list.ChildrenOfType<UpdateableAvatar>().Count() == 3);
AddAssert("46 hidden users", () => list.ChildrenOfType<RecentParticipantsList.HiddenUserCount>().Single().Count == 46);
AddStep("remove from end", () => removeUserAt(SelectedRoom.Value.RecentParticipants.Count - 1));
AddAssert("3 circles displayed", () => list.ChildrenOfType<UpdateableAvatar>().Count() == 3);
AddAssert("45 hidden users", () => list.ChildrenOfType<RecentParticipantsList.HiddenUserCount>().Single().Count == 45);
AddRepeatStep("remove 45 users", () => removeUserAt(0), 45);
AddAssert("3 circles displayed", () => list.ChildrenOfType<UpdateableAvatar>().Count() == 3);
AddAssert("0 hidden users", () => list.ChildrenOfType<RecentParticipantsList.HiddenUserCount>().Single().Count == 0);
AddAssert("hidden users bubble hidden", () => list.ChildrenOfType<RecentParticipantsList.HiddenUserCount>().Single().Alpha < 0.5f);
AddStep("remove another user", () => removeUserAt(0));
AddAssert("2 circles displayed", () => list.ChildrenOfType<UpdateableAvatar>().Count() == 2);
AddAssert("0 hidden users", () => list.ChildrenOfType<RecentParticipantsList.HiddenUserCount>().Single().Count == 0);
AddRepeatStep("remove the remaining two users", () => removeUserAt(0), 2);
AddAssert("0 circles displayed", () => !list.ChildrenOfType<UpdateableAvatar>().Any());
}
private void addUser(int id)
{
SelectedRoom.Value.RecentParticipants.Add(new User
{
Id = id,
Username = $"User {id}"
});
SelectedRoom.Value.ParticipantCount.Value++;
}
private void removeUserAt(int index)
{
SelectedRoom.Value.RecentParticipants.RemoveAt(index);
SelectedRoom.Value.ParticipantCount.Value--;
}
}
}

View File

@ -1,81 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneRoomStatus : OsuTestScene
{
[Test]
public void TestMultipleStatuses()
{
AddStep("create rooms", () =>
{
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Children = new Drawable[]
{
new DrawableRoom(new Room
{
Name = { Value = "Open - ending in 1 day" },
Status = { Value = new RoomStatusOpen() },
EndDate = { Value = DateTimeOffset.Now.AddDays(1) }
}) { MatchingFilter = true },
new DrawableRoom(new Room
{
Name = { Value = "Playing - ending in 1 day" },
Status = { Value = new RoomStatusPlaying() },
EndDate = { Value = DateTimeOffset.Now.AddDays(1) }
}) { MatchingFilter = true },
new DrawableRoom(new Room
{
Name = { Value = "Ended" },
Status = { Value = new RoomStatusEnded() },
EndDate = { Value = DateTimeOffset.Now }
}) { MatchingFilter = true },
new DrawableRoom(new Room
{
Name = { Value = "Open" },
Status = { Value = new RoomStatusOpen() },
Category = { Value = RoomCategory.Realtime }
}) { MatchingFilter = true },
}
};
});
}
[Test]
public void TestEnableAndDisablePassword()
{
DrawableRoom drawableRoom = null;
Room room = null;
AddStep("create room", () => Child = drawableRoom = new DrawableRoom(room = new Room
{
Name = { Value = "Room with password" },
Status = { Value = new RoomStatusOpen() },
Category = { Value = RoomCategory.Realtime },
}) { MatchingFilter = true });
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
AddStep("set password", () => room.Password.Value = "password");
AddAssert("password icon visible", () => Precision.AlmostEquals(1, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
AddStep("unset password", () => room.Password.Value = string.Empty);
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
}
}
}

View File

@ -17,6 +17,7 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens; using osu.Game.Screens;
using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
@ -150,10 +151,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void createRoom(Func<Room> room) private void createRoom(Func<Room> room)
{ {
AddStep("open room", () => AddStep("open room", () => multiplayerScreen.ChildrenOfType<LoungeSubScreen>().Single().Open(room()));
{
multiplayerScreen.OpenNewRoom(room());
});
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true); AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddWaitStep("wait for transition", 2); AddWaitStep("wait for transition", 2);

View File

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

View File

@ -16,6 +16,7 @@ using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
@ -316,7 +317,8 @@ namespace osu.Game.Tests.Visual.Navigation
PushAndConfirm(() => multiplayer = new TestMultiplayer()); PushAndConfirm(() => multiplayer = new TestMultiplayer());
AddStep("open room", () => multiplayer.OpenNewRoom()); AddUntilStep("wait for lounge", () => multiplayer.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true);
AddStep("open room", () => multiplayer.ChildrenOfType<LoungeSubScreen>().Single().Open());
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action()); AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
AddWaitStep("wait two frames", 2); AddWaitStep("wait two frames", 2);
} }

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Humanizer; using Humanizer;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
@ -95,9 +96,11 @@ namespace osu.Game.Tests.Visual.Online
AddAssert(@"no stream selected", () => changelog.Header.Streams.Current.Value == null); AddAssert(@"no stream selected", () => changelog.Header.Streams.Current.Value == null);
} }
[Test] [TestCase(false)]
public void ShowWithBuild() [TestCase(true)]
public void ShowWithBuild(bool isSupporter)
{ {
AddStep(@"set supporter", () => dummyAPI.LocalUser.Value.IsSupporter = isSupporter);
showBuild(() => new APIChangelogBuild showBuild(() => new APIChangelogBuild
{ {
Version = "2018.712.0", Version = "2018.712.0",
@ -155,6 +158,8 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep(@"wait for streams", () => changelog.Streams?.Count > 0); AddUntilStep(@"wait for streams", () => changelog.Streams?.Count > 0);
AddAssert(@"correct build displayed", () => changelog.Current.Value.Version == "2018.712.0"); AddAssert(@"correct build displayed", () => changelog.Current.Value.Version == "2018.712.0");
AddAssert(@"correct stream selected", () => changelog.Header.Streams.Current.Value.Id == 5); AddAssert(@"correct stream selected", () => changelog.Header.Streams.Current.Value.Id == 5);
AddUntilStep(@"wait for content load", () => changelog.ChildrenOfType<ChangelogSupporterPromo>().Any());
AddAssert(@"supporter promo showed", () => changelog.ChildrenOfType<ChangelogSupporterPromo>().First().Alpha == (isSupporter ? 0 : 1));
} }
[Test] [Test]

View File

@ -0,0 +1,35 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Overlays;
using osu.Game.Overlays.Changelog;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneChangelogSupporterPromo : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
public TestSceneChangelogSupporterPromo()
{
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
},
new ChangelogSupporterPromo(),
}
};
}
}
}

View File

@ -3,84 +3,52 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics;
using osu.Game.Overlays.Comments; using osu.Game.Overlays.Comments;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Users; using osu.Game.Users;
using osu.Game.Graphics.UserInterface;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics;
using osuTK;
using JetBrains.Annotations; using JetBrains.Annotations;
using NUnit.Framework; using osu.Framework.Testing;
namespace osu.Game.Tests.Visual.Online namespace osu.Game.Tests.Visual.Online
{ {
public class TestSceneCommentsPage : OsuTestScene public class TestSceneOfflineCommentsContainer : OsuTestScene
{ {
[Cached] [Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
private readonly BindableBool showDeleted = new BindableBool(); private TestCommentsContainer comments;
private readonly Container content;
private TestCommentsPage commentsPage; [SetUp]
public void SetUp() => Schedule(() =>
public TestSceneCommentsPage()
{ {
Add(new FillFlowContainer Clear();
Add(new BasicScrollContainer
{ {
AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Both,
RelativeSizeAxes = Axes.X, Child = comments = new TestCommentsContainer()
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 10),
Children = new Drawable[]
{
new Container
{
AutoSizeAxes = Axes.Y,
Width = 200,
Child = new OsuCheckbox
{
Current = showDeleted,
LabelText = @"Show Deleted"
}
},
content = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}
}
}); });
} });
[Test] [Test]
public void TestAppendDuplicatedComment() public void TestAppendDuplicatedComment()
{ {
AddStep("Create page", () => createPage(getCommentBundle())); AddStep("Add comment bundle", () => comments.ShowComments(getCommentBundle()));
AddAssert("Dictionary length is 10", () => commentsPage?.DictionaryLength == 10); AddUntilStep("Dictionary length is 10", () => comments.DictionaryLength == 10);
AddStep("Append existing comment", () => commentsPage?.AppendComments(getCommentSubBundle())); AddStep("Append existing comment", () => comments.AppendComments(getCommentSubBundle()));
AddAssert("Dictionary length is 10", () => commentsPage?.DictionaryLength == 10); AddAssert("Dictionary length is 10", () => comments.DictionaryLength == 10);
} }
[Test] [Test]
public void TestEmptyBundle() public void TestLocalCommentBundle()
{ {
AddStep("Create page", () => createPage(getEmptyCommentBundle())); AddStep("Add comment bundle", () => comments.ShowComments(getCommentBundle()));
AddAssert("Dictionary length is 0", () => commentsPage?.DictionaryLength == 0); AddStep("Add empty comment bundle", () => comments.ShowComments(getEmptyCommentBundle()));
}
private void createPage(CommentBundle commentBundle)
{
commentsPage = null;
content.Clear();
content.Add(commentsPage = new TestCommentsPage(commentBundle)
{
ShowDeleted = { BindTarget = showDeleted }
});
} }
private CommentBundle getEmptyCommentBundle() => new CommentBundle private CommentBundle getEmptyCommentBundle() => new CommentBundle
@ -193,6 +161,7 @@ namespace osu.Game.Tests.Visual.Online
Username = "Good_Admin" Username = "Good_Admin"
} }
}, },
Total = 10
}; };
private CommentBundle getCommentSubBundle() => new CommentBundle private CommentBundle getCommentSubBundle() => new CommentBundle
@ -211,16 +180,18 @@ namespace osu.Game.Tests.Visual.Online
IncludedComments = new List<Comment>(), IncludedComments = new List<Comment>(),
}; };
private class TestCommentsPage : CommentsPage private class TestCommentsContainer : CommentsContainer
{ {
public TestCommentsPage(CommentBundle commentBundle)
: base(commentBundle)
{
}
public new void AppendComments([NotNull] CommentBundle bundle) => base.AppendComments(bundle); public new void AppendComments([NotNull] CommentBundle bundle) => base.AppendComments(bundle);
public int DictionaryLength => CommentDictionary.Count; public int DictionaryLength => CommentDictionary.Count;
public void ShowComments(CommentBundle bundle)
{
this.ChildrenOfType<TotalCommentsCounter>().Single().Current.Value = 0;
ClearComments();
OnSuccess(bundle);
}
} }
} }
} }

View File

@ -1,23 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
namespace osu.Game.Tests.Visual.Playlists
{
public class TestScenePlaylistsFilterControl : OsuTestScene
{
public TestScenePlaylistsFilterControl()
{
Child = new PlaylistsFilterControl
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Width = 0.7f,
Height = 80,
};
}
}
}

View File

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

View File

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

View File

@ -1,8 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -174,6 +176,60 @@ namespace osu.Game.Tests.Visual.UserInterface
checkBindableAtValue("Circle Size", null); checkBindableAtValue("Circle Size", null);
} }
[Test]
public void TestModSettingChangeTracker()
{
ModSettingChangeTracker tracker = null;
Queue<Mod> settingsChangedQueue = null;
setBeatmapWithDifficultyParameters(5);
AddStep("add mod settings change tracker", () =>
{
settingsChangedQueue = new Queue<Mod>();
tracker = new ModSettingChangeTracker(modDifficultyAdjust.Yield())
{
SettingChanged = settingsChangedQueue.Enqueue
};
});
AddAssert("no settings changed", () => settingsChangedQueue.Count == 0);
setSliderValue("Circle Size", 3);
settingsChangedFired();
setSliderValue("Circle Size", 5);
checkBindableAtValue("Circle Size", 5);
settingsChangedFired();
AddStep("reset mod settings", () => modDifficultyAdjust.CircleSize.SetDefault());
checkBindableAtValue("Circle Size", null);
settingsChangedFired();
setExtendedLimits(true);
settingsChangedFired();
AddStep("dispose tracker", () =>
{
tracker.Dispose();
tracker = null;
});
void settingsChangedFired()
{
AddAssert("setting changed event fired", () =>
{
settingsChangedQueue.Dequeue();
return settingsChangedQueue.Count == 0;
});
}
}
private void resetToDefault(string name) private void resetToDefault(string name)
{ {
AddStep($"Reset {name} to default", () => AddStep($"Reset {name} to default", () =>

View File

@ -53,7 +53,7 @@ namespace osu.Game.Tests
protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile)); protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile));
protected override ISkin GetSkin() => null; protected internal override ISkin GetSkin() => null;
public override Stream GetStream(string storagePath) => null; public override Stream GetStream(string storagePath) => null;

View File

@ -16,7 +16,7 @@ namespace osu.Game.Tournament.Tests.Components
public TestSceneMatchScoreDisplay() public TestSceneMatchScoreDisplay()
{ {
Add(new MatchScoreDisplay Add(new TournamentMatchScoreDisplay
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -16,7 +16,8 @@ using osuTK;
namespace osu.Game.Tournament.Screens.Gameplay.Components namespace osu.Game.Tournament.Screens.Gameplay.Components
{ {
public class MatchScoreDisplay : CompositeDrawable // TODO: Update to derive from osu-side class?
public class TournamentMatchScoreDisplay : CompositeDrawable
{ {
private const float bar_height = 18; private const float bar_height = 18;
@ -29,7 +30,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
private readonly Drawable score1Bar; private readonly Drawable score1Bar;
private readonly Drawable score2Bar; private readonly Drawable score2Bar;
public MatchScoreDisplay() public TournamentMatchScoreDisplay()
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;

View File

@ -86,7 +86,7 @@ namespace osu.Game.Tournament.Screens.Gameplay
}, },
} }
}, },
scoreDisplay = new MatchScoreDisplay scoreDisplay = new TournamentMatchScoreDisplay
{ {
Y = -147, Y = -147,
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
@ -148,7 +148,7 @@ namespace osu.Game.Tournament.Screens.Gameplay
} }
private ScheduledDelegate scheduledOperation; private ScheduledDelegate scheduledOperation;
private MatchScoreDisplay scoreDisplay; private TournamentMatchScoreDisplay scoreDisplay;
private TourneyState lastState; private TourneyState lastState;
private MatchHeader header; private MatchHeader header;

View File

@ -26,8 +26,8 @@ namespace osu.Game.Tournament
{ {
public static ColourInfo GetTeamColour(TeamColour teamColour) => teamColour == TeamColour.Red ? COLOUR_RED : COLOUR_BLUE; public static ColourInfo GetTeamColour(TeamColour teamColour) => teamColour == TeamColour.Red ? COLOUR_RED : COLOUR_BLUE;
public static readonly Color4 COLOUR_RED = Color4Extensions.FromHex("#AA1414"); public static readonly Color4 COLOUR_RED = new OsuColour().TeamColourRed;
public static readonly Color4 COLOUR_BLUE = Color4Extensions.FromHex("#1462AA"); public static readonly Color4 COLOUR_BLUE = new OsuColour().TeamColourBlue;
public static readonly Color4 ELEMENT_BACKGROUND_COLOUR = Color4Extensions.FromHex("#fff"); public static readonly Color4 ELEMENT_BACKGROUND_COLOUR = Color4Extensions.FromHex("#fff");
public static readonly Color4 ELEMENT_FOREGROUND_COLOUR = Color4Extensions.FromHex("#000"); public static readonly Color4 ELEMENT_FOREGROUND_COLOUR = Color4Extensions.FromHex("#000");

View File

@ -534,7 +534,7 @@ namespace osu.Game.Beatmaps
protected override IBeatmap GetBeatmap() => beatmap; protected override IBeatmap GetBeatmap() => beatmap;
protected override Texture GetBackground() => null; protected override Texture GetBackground() => null;
protected override Track GetBeatmapTrack() => null; protected override Track GetBeatmapTrack() => null;
protected override ISkin GetSkin() => null; protected internal override ISkin GetSkin() => null;
public override Stream GetStream(string storagePath) => null; public override Stream GetStream(string storagePath) => null;
} }
} }

View File

@ -128,7 +128,7 @@ namespace osu.Game.Beatmaps
return storyboard; return storyboard;
} }
protected override ISkin GetSkin() protected internal override ISkin GetSkin()
{ {
try try
{ {

View File

@ -50,7 +50,7 @@ namespace osu.Game.Beatmaps
protected override Track GetBeatmapTrack() => GetVirtualTrack(); protected override Track GetBeatmapTrack() => GetVirtualTrack();
protected override ISkin GetSkin() => null; protected internal override ISkin GetSkin() => null;
public override Stream GetStream(string storagePath) => null; public override Stream GetStream(string storagePath) => null;

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using osuTK.Graphics; using osuTK.Graphics;
@ -13,9 +14,17 @@ namespace osu.Game.Beatmaps.Formats
/// </summary> /// </summary>
IReadOnlyList<Color4> ComboColours { get; } IReadOnlyList<Color4> ComboColours { get; }
/// <summary>
/// The list of custom combo colours.
/// If non-empty, <see cref="ComboColours"/> will return these colours;
/// if empty, <see cref="ComboColours"/> will fall back to default combo colours.
/// </summary>
List<Color4> CustomComboColours { get; }
/// <summary> /// <summary>
/// Adds combo colours to the list. /// Adds combo colours to the list.
/// </summary> /// </summary>
[Obsolete("Use CustomComboColours directly.")] // can be removed 20220215
void AddComboColours(params Color4[] colours); void AddComboColours(params Color4[] colours);
} }
} }

View File

@ -123,7 +123,7 @@ namespace osu.Game.Beatmaps.Formats
{ {
if (!(output is IHasComboColours tHasComboColours)) return; if (!(output is IHasComboColours tHasComboColours)) return;
tHasComboColours.AddComboColours(colour); tHasComboColours.CustomComboColours.Add(colour);
} }
else else
{ {

View File

@ -327,7 +327,15 @@ namespace osu.Game.Beatmaps
public bool SkinLoaded => skin.IsResultAvailable; public bool SkinLoaded => skin.IsResultAvailable;
public ISkin Skin => skin.Value; public ISkin Skin => skin.Value;
protected abstract ISkin GetSkin(); /// <summary>
/// Creates a new skin instance for this beatmap.
/// </summary>
/// <remarks>
/// This should only be called externally in scenarios where it is explicitly desired to get a new instance of a skin
/// (e.g. for editing purposes, to avoid state pollution).
/// For standard reading purposes, <see cref="Skin"/> should always be used directly.
/// </remarks>
protected internal abstract ISkin GetSkin();
private readonly RecyclableLazy<ISkin> skin; private readonly RecyclableLazy<ISkin> skin;

View File

@ -201,6 +201,8 @@ namespace osu.Game.Configuration
public Func<GlobalAction, string> LookupKeyBindings { get; set; } public Func<GlobalAction, string> LookupKeyBindings { get; set; }
} }
// IMPORTANT: These are used in user configuration files.
// The naming of these keys should not be changed once they are deployed in a release, unless migration logic is also added.
public enum OsuSetting public enum OsuSetting
{ {
Ruleset, Ruleset,

View File

@ -130,6 +130,9 @@ namespace osu.Game.Graphics
return Gray(brightness > 0.5f ? 0.2f : 0.9f); return Gray(brightness > 0.5f ? 0.2f : 0.9f);
} }
public readonly Color4 TeamColourRed = Color4Extensions.FromHex("#AA1414");
public readonly Color4 TeamColourBlue = Color4Extensions.FromHex("#1462AA");
// See https://github.com/ppy/osu-web/blob/master/resources/assets/less/colors.less // See https://github.com/ppy/osu-web/blob/master/resources/assets/less/colors.less
public readonly Color4 PurpleLighter = Color4Extensions.FromHex(@"eeeeff"); public readonly Color4 PurpleLighter = Color4Extensions.FromHex(@"eeeeff");
public readonly Color4 PurpleLight = Color4Extensions.FromHex(@"aa88ff"); public readonly Color4 PurpleLight = Color4Extensions.FromHex(@"aa88ff");

View File

@ -0,0 +1,64 @@
// 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 AudioSettingsStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.AudioSettings";
/// <summary>
/// "Audio"
/// </summary>
public static LocalisableString AudioSectionHeader => new TranslatableString(getKey(@"audio_section_header"), @"Audio");
/// <summary>
/// "Devices"
/// </summary>
public static LocalisableString AudioDevicesHeader => new TranslatableString(getKey(@"audio_devices_header"), @"Devices");
/// <summary>
/// "Volume"
/// </summary>
public static LocalisableString VolumeHeader => new TranslatableString(getKey(@"volume_header"), @"Volume");
/// <summary>
/// "Master"
/// </summary>
public static LocalisableString MasterVolume => new TranslatableString(getKey(@"master_volume"), @"Master");
/// <summary>
/// "Master (window inactive)"
/// </summary>
public static LocalisableString MasterVolumeInactive => new TranslatableString(getKey(@"master_volume_inactive"), @"Master (window inactive)");
/// <summary>
/// "Effect"
/// </summary>
public static LocalisableString EffectVolume => new TranslatableString(getKey(@"effect_volume"), @"Effect");
/// <summary>
/// "Music"
/// </summary>
public static LocalisableString MusicVolume => new TranslatableString(getKey(@"music_volume"), @"Music");
/// <summary>
/// "Offset Adjustment"
/// </summary>
public static LocalisableString OffsetHeader => new TranslatableString(getKey(@"offset_header"), @"Offset Adjustment");
/// <summary>
/// "Audio offset"
/// </summary>
public static LocalisableString AudioOffset => new TranslatableString(getKey(@"audio_offset"), @"Audio offset");
/// <summary>
/// "Offset wizard"
/// </summary>
public static LocalisableString OffsetWizard => new TranslatableString(getKey(@"offset_wizard"), @"Offset wizard");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

@ -14,11 +14,21 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString Cancel => new TranslatableString(getKey(@"cancel"), @"Cancel"); public static LocalisableString Cancel => new TranslatableString(getKey(@"cancel"), @"Cancel");
/// <summary>
/// "Clear"
/// </summary>
public static LocalisableString Clear => new TranslatableString(getKey(@"clear"), @"Clear");
/// <summary> /// <summary>
/// "Enabled" /// "Enabled"
/// </summary> /// </summary>
public static LocalisableString Enabled => new TranslatableString(getKey(@"enabled"), @"Enabled"); public static LocalisableString Enabled => new TranslatableString(getKey(@"enabled"), @"Enabled");
/// <summary>
/// "Default"
/// </summary>
public static LocalisableString Default => new TranslatableString(getKey(@"default"), @"Default");
/// <summary> /// <summary>
/// "Width" /// "Width"
/// </summary> /// </summary>
@ -31,4 +41,4 @@ namespace osu.Game.Localisation
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -0,0 +1,49 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class DebugSettingsStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.DebugSettings";
/// <summary>
/// "Debug"
/// </summary>
public static LocalisableString DebugSectionHeader => new TranslatableString(getKey(@"debug_section_header"), @"Debug");
/// <summary>
/// "General"
/// </summary>
public static LocalisableString GeneralHeader => new TranslatableString(getKey(@"general_header"), @"General");
/// <summary>
/// "Show log overlay"
/// </summary>
public static LocalisableString ShowLogOverlay => new TranslatableString(getKey(@"show_log_overlay"), @"Show log overlay");
/// <summary>
/// "Bypass front-to-back render pass"
/// </summary>
public static LocalisableString BypassFrontToBackPass => new TranslatableString(getKey(@"bypass_front_to_back_pass"), @"Bypass front-to-back render pass");
/// <summary>
/// "Import files"
/// </summary>
public static LocalisableString ImportFiles => new TranslatableString(getKey(@"import_files"), @"Import files");
/// <summary>
/// "Memory"
/// </summary>
public static LocalisableString MemoryHeader => new TranslatableString(getKey(@"memory_header"), @"Memory");
/// <summary>
/// "Clear all caches"
/// </summary>
public static LocalisableString ClearAllCaches => new TranslatableString(getKey(@"clear_all_caches"), @"Clear all caches");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

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 osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class GameplaySettingsStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.GameplaySettings";
/// <summary>
/// "Gameplay"
/// </summary>
public static LocalisableString GameplaySectionHeader => new TranslatableString(getKey(@"gameplay_section_header"), @"Gameplay");
/// <summary>
/// "General"
/// </summary>
public static LocalisableString GeneralHeader => new TranslatableString(getKey(@"general_header"), @"General");
/// <summary>
/// "Background dim"
/// </summary>
public static LocalisableString BackgroundDim => new TranslatableString(getKey(@"dim"), @"Background dim");
/// <summary>
/// "Background blur"
/// </summary>
public static LocalisableString BackgroundBlur => new TranslatableString(getKey(@"blur"), @"Background blur");
/// <summary>
/// "Lighten playfield during breaks"
/// </summary>
public static LocalisableString LightenDuringBreaks => new TranslatableString(getKey(@"lighten_during_breaks"), @"Lighten playfield during breaks");
/// <summary>
/// "HUD overlay visibility mode"
/// </summary>
public static LocalisableString HUDVisibilityMode => new TranslatableString(getKey(@"hud_visibility_mode"), @"HUD overlay visibility mode");
/// <summary>
/// "Show difficulty graph on progress bar"
/// </summary>
public static LocalisableString ShowDifficultyGraph => new TranslatableString(getKey(@"show_difficulty_graph"), @"Show difficulty graph on progress bar");
/// <summary>
/// "Show health display even when you can't fail"
/// </summary>
public static LocalisableString ShowHealthDisplayWhenCantFail => new TranslatableString(getKey(@"show_health_display_when_cant_fail"), @"Show health display even when you can't fail");
/// <summary>
/// "Fade playfield to red when health is low"
/// </summary>
public static LocalisableString FadePlayfieldWhenHealthLow => new TranslatableString(getKey(@"fade_playfield_when_health_low"), @"Fade playfield to red when health is low");
/// <summary>
/// "Always show key overlay"
/// </summary>
public static LocalisableString AlwaysShowKeyOverlay => new TranslatableString(getKey(@"key_overlay"), @"Always show key overlay");
/// <summary>
/// "Positional hitsounds"
/// </summary>
public static LocalisableString PositionalHitsounds => new TranslatableString(getKey(@"positional_hitsounds"), @"Positional hitsounds");
/// <summary>
/// "Always play first combo break sound"
/// </summary>
public static LocalisableString AlwaysPlayFirstComboBreak => new TranslatableString(getKey(@"always_play_first_combo_break"), @"Always play first combo break sound");
/// <summary>
/// "Score display mode"
/// </summary>
public static LocalisableString ScoreDisplayMode => new TranslatableString(getKey(@"score_display_mode"), @"Score display mode");
/// <summary>
/// "Disable Windows key during gameplay"
/// </summary>
public static LocalisableString DisableWinKey => new TranslatableString(getKey(@"disable_win_key"), @"Disable Windows key during gameplay");
/// <summary>
/// "Mods"
/// </summary>
public static LocalisableString ModsHeader => new TranslatableString(getKey(@"mods_header"), @"Mods");
/// <summary>
/// "Increase visibility of first object when visual impairment mods are enabled"
/// </summary>
public static LocalisableString IncreaseFirstObjectVisibility => new TranslatableString(getKey(@"increase_first_object_visibility"), @"Increase visibility of first object when visual impairment mods are enabled");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

@ -0,0 +1,59 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class GeneralSettingsStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.GeneralSettings";
/// <summary>
/// "General"
/// </summary>
public static LocalisableString GeneralSectionHeader => new TranslatableString(getKey(@"general_section_header"), @"General");
/// <summary>
/// "Language"
/// </summary>
public static LocalisableString LanguageHeader => new TranslatableString(getKey(@"language_header"), @"Language");
/// <summary>
/// "Language"
/// </summary>
public static LocalisableString LanguageDropdown => new TranslatableString(getKey(@"language_dropdown"), @"Language");
/// <summary>
/// "Prefer metadata in original language"
/// </summary>
public static LocalisableString PreferOriginalMetadataLanguage => new TranslatableString(getKey(@"prefer_original"), @"Prefer metadata in original language");
/// <summary>
/// "Updates"
/// </summary>
public static LocalisableString UpdateHeader => new TranslatableString(getKey(@"update_header"), @"Updates");
/// <summary>
/// "Release stream"
/// </summary>
public static LocalisableString ReleaseStream => new TranslatableString(getKey(@"release_stream"), @"Release stream");
/// <summary>
/// "Check for updates"
/// </summary>
public static LocalisableString CheckUpdate => new TranslatableString(getKey(@"check_update"), @"Check for updates");
/// <summary>
/// "Open osu! folder"
/// </summary>
public static LocalisableString OpenOsuFolder => new TranslatableString(getKey(@"open_osu_folder"), @"Open osu! folder");
/// <summary>
/// "Change folder location..."
/// </summary>
public static LocalisableString ChangeFolderLocation => new TranslatableString(getKey(@"change_folder_location"), @"Change folder location...");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

@ -0,0 +1,119 @@
// 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 GraphicsSettingsStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.GraphicsSettings";
/// <summary>
/// "Graphics"
/// </summary>
public static LocalisableString GraphicsSectionHeader => new TranslatableString(getKey(@"graphics_section_header"), @"Graphics");
/// <summary>
/// "Renderer"
/// </summary>
public static LocalisableString RendererHeader => new TranslatableString(getKey(@"renderer_header"), @"Renderer");
/// <summary>
/// "Frame limiter"
/// </summary>
public static LocalisableString FrameLimiter => new TranslatableString(getKey(@"frame_limiter"), @"Frame limiter");
/// <summary>
/// "Threading mode"
/// </summary>
public static LocalisableString ThreadingMode => new TranslatableString(getKey(@"threading_mode"), @"Threading mode");
/// <summary>
/// "Show FPS"
/// </summary>
public static LocalisableString ShowFPS => new TranslatableString(getKey(@"show_fps"), @"Show FPS");
/// <summary>
/// "Using unlimited frame limiter can lead to stutters, bad performance and overheating. It will not improve perceived latency. "2x refresh rate" is recommended."
/// </summary>
public static LocalisableString UnlimitedFramesNote => new TranslatableString(getKey(@"unlimited_frames_note"), @"Using unlimited frame limiter can lead to stutters, bad performance and overheating. It will not improve perceived latency. ""2x refresh rate"" is recommended.");
/// <summary>
/// "Layout"
/// </summary>
public static LocalisableString LayoutHeader => new TranslatableString(getKey(@"layout_header"), @"Layout");
/// <summary>
/// "Screen mode"
/// </summary>
public static LocalisableString ScreenMode => new TranslatableString(getKey(@"screen_mode"), @"Screen mode");
/// <summary>
/// "Resolution"
/// </summary>
public static LocalisableString Resolution => new TranslatableString(getKey(@"resolution"), @"Resolution");
/// <summary>
/// "UI scaling"
/// </summary>
public static LocalisableString UIScaling => new TranslatableString(getKey(@"ui_scaling"), @"UI scaling");
/// <summary>
/// "Screen scaling"
/// </summary>
public static LocalisableString ScreenScaling => new TranslatableString(getKey(@"screen_scaling"), @"Screen scaling");
/// <summary>
/// "Horizontal position"
/// </summary>
public static LocalisableString HorizontalPosition => new TranslatableString(getKey(@"horizontal_position"), @"Horizontal position");
/// <summary>
/// "Vertical position"
/// </summary>
public static LocalisableString VerticalPosition => new TranslatableString(getKey(@"vertical_position"), @"Vertical position");
/// <summary>
/// "Horizontal scale"
/// </summary>
public static LocalisableString HorizontalScale => new TranslatableString(getKey(@"horizontal_scale"), @"Horizontal scale");
/// <summary>
/// "Vertical scale"
/// </summary>
public static LocalisableString VerticalScale => new TranslatableString(getKey(@"vertical_scale"), @"Vertical scale");
/// <summary>
/// "Running without fullscreen mode will increase your input latency!"
/// </summary>
public static LocalisableString NotFullscreenNote => new TranslatableString(getKey(@"not_fullscreen_note"), @"Running without fullscreen mode will increase your input latency!");
/// <summary>
/// "Detail Settings"
/// </summary>
public static LocalisableString DetailSettingsHeader => new TranslatableString(getKey(@"detail_settings_header"), @"Detail Settings");
/// <summary>
/// "Storyboard / video"
/// </summary>
public static LocalisableString StoryboardVideo => new TranslatableString(getKey(@"storyboard_video"), @"Storyboard / video");
/// <summary>
/// "Hit lighting"
/// </summary>
public static LocalisableString HitLighting => new TranslatableString(getKey(@"hit_lighting"), @"Hit lighting");
/// <summary>
/// "Screenshot format"
/// </summary>
public static LocalisableString ScreenshotFormat => new TranslatableString(getKey(@"screenshot_format"), @"Screenshot format");
/// <summary>
/// "Show menu cursor in screenshots"
/// </summary>
public static LocalisableString ShowCursorInScreenshots => new TranslatableString(getKey(@"show_cursor_in_screenshots"), @"Show menu cursor in screenshots");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

@ -0,0 +1,59 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class InputSettingsStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.InputSettings";
/// <summary>
/// "Input"
/// </summary>
public static LocalisableString InputSectionHeader => new TranslatableString(getKey(@"input_section_header"), @"Input");
/// <summary>
/// "Global"
/// </summary>
public static LocalisableString GlobalKeyBindingHeader => new TranslatableString(getKey(@"global_key_binding_header"), @"Global");
/// <summary>
/// "Song Select"
/// </summary>
public static LocalisableString SongSelectSection => new TranslatableString(getKey(@"song_select_section"), @"Song Select");
/// <summary>
/// "In Game"
/// </summary>
public static LocalisableString InGameSection => new TranslatableString(getKey(@"in_game_section"), @"In Game");
/// <summary>
/// "Audio"
/// </summary>
public static LocalisableString AudioSection => new TranslatableString(getKey(@"audio_section"), @"Audio");
/// <summary>
/// "Editor"
/// </summary>
public static LocalisableString EditorSection => new TranslatableString(getKey(@"editor_section"), @"Editor");
/// <summary>
/// "Reset all bindings in section"
/// </summary>
public static LocalisableString ResetSectionButton => new TranslatableString(getKey(@"reset_section_button"), @"Reset all bindings in section");
/// <summary>
/// "key configuration"
/// </summary>
public static LocalisableString KeyBindingPanelHeader => new TranslatableString(getKey(@"key_binding_panel_header"), @"key configuration");
/// <summary>
/// "Customise your keys!"
/// </summary>
public static LocalisableString KeyBindingPanelDescription => new TranslatableString(getKey(@"key_binding_panel_description"), @"Customise your keys!");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

@ -0,0 +1,74 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class MaintenanceSettingsStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.MaintenanceSettings";
/// <summary>
/// "Maintenance"
/// </summary>
public static LocalisableString MaintenanceSectionHeader => new TranslatableString(getKey(@"maintenance_section_header"), @"Maintenance");
/// <summary>
/// "Select directory"
/// </summary>
public static LocalisableString SelectDirectory => new TranslatableString(getKey(@"select_directory"), @"Select directory");
/// <summary>
/// "Import beatmaps from stable"
/// </summary>
public static LocalisableString ImportBeatmapsFromStable => new TranslatableString(getKey(@"import_beatmaps_from_stable"), @"Import beatmaps from stable");
/// <summary>
/// "Delete ALL beatmaps"
/// </summary>
public static LocalisableString DeleteAllBeatmaps => new TranslatableString(getKey(@"delete_all_beatmaps"), @"Delete ALL beatmaps");
/// <summary>
/// "Import scores from stable"
/// </summary>
public static LocalisableString ImportScoresFromStable => new TranslatableString(getKey(@"import_scores_from_stable"), @"Import scores from stable");
/// <summary>
/// "Delete ALL scores"
/// </summary>
public static LocalisableString DeleteAllScores => new TranslatableString(getKey(@"delete_all_scores"), @"Delete ALL scores");
/// <summary>
/// "Import skins from stable"
/// </summary>
public static LocalisableString ImportSkinsFromStable => new TranslatableString(getKey(@"import_skins_from_stable"), @"Import skins from stable");
/// <summary>
/// "Delete ALL skins"
/// </summary>
public static LocalisableString DeleteAllSkins => new TranslatableString(getKey(@"delete_all_skins"), @"Delete ALL skins");
/// <summary>
/// "Import collections from stable"
/// </summary>
public static LocalisableString ImportCollectionsFromStable => new TranslatableString(getKey(@"import_collections_from_stable"), @"Import collections from stable");
/// <summary>
/// "Delete ALL collections"
/// </summary>
public static LocalisableString DeleteAllCollections => new TranslatableString(getKey(@"delete_all_collections"), @"Delete ALL collections");
/// <summary>
/// "Restore all hidden difficulties"
/// </summary>
public static LocalisableString RestoreAllHiddenDifficulties => new TranslatableString(getKey(@"restore_all_hidden_difficulties"), @"Restore all hidden difficulties");
/// <summary>
/// "Restore all recently deleted beatmaps"
/// </summary>
public static LocalisableString RestoreAllRecentlyDeletedBeatmaps => new TranslatableString(getKey(@"restore_all_recently_deleted_beatmaps"), @"Restore all recently deleted beatmaps");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

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

@ -0,0 +1,69 @@
// 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 OnlineSettingsStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.OnlineSettings";
/// <summary>
/// "Online"
/// </summary>
public static LocalisableString OnlineSectionHeader => new TranslatableString(getKey(@"online_section_header"), @"Online");
/// <summary>
/// "Alerts and Privacy"
/// </summary>
public static LocalisableString AlertsAndPrivacyHeader => new TranslatableString(getKey(@"alerts_and_privacy_header"), @"Alerts and Privacy");
/// <summary>
/// "Show a notification when someone mentions your name"
/// </summary>
public static LocalisableString NotifyOnMentioned => new TranslatableString(getKey(@"notify_on_mentioned"), @"Show a notification when someone mentions your name");
/// <summary>
/// "Show a notification when you receive a private message"
/// </summary>
public static LocalisableString NotifyOnPrivateMessage => new TranslatableString(getKey(@"notify_on_private_message"), @"Show a notification when you receive a private message");
/// <summary>
/// "Integrations"
/// </summary>
public static LocalisableString IntegrationsHeader => new TranslatableString(getKey(@"integrations_header"), @"Integrations");
/// <summary>
/// "Discord Rich Presence"
/// </summary>
public static LocalisableString DiscordRichPresence => new TranslatableString(getKey(@"discord_rich_presence"), @"Discord Rich Presence");
/// <summary>
/// "Web"
/// </summary>
public static LocalisableString WebHeader => new TranslatableString(getKey(@"web_header"), @"Web");
/// <summary>
/// "Warn about opening external links"
/// </summary>
public static LocalisableString ExternalLinkWarning => new TranslatableString(getKey(@"external_link_warning"), @"Warn about opening external links");
/// <summary>
/// "Prefer downloads without video"
/// </summary>
public static LocalisableString PreferNoVideo => new TranslatableString(getKey(@"prefer_no_video"), @"Prefer downloads without video");
/// <summary>
/// "Automatically download beatmaps when spectating"
/// </summary>
public static LocalisableString AutomaticallyDownloadWhenSpectating => new TranslatableString(getKey(@"automatically_download_when_spectating"), @"Automatically download beatmaps when spectating");
/// <summary>
/// "Show explicit content in search results"
/// </summary>
public static LocalisableString ShowExplicitContent => new TranslatableString(getKey(@"show_explicit_content"), @"Show explicit content in search results");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

@ -0,0 +1,54 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class SkinSettingsStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.SkinSettings";
/// <summary>
/// "Skin"
/// </summary>
public static LocalisableString SkinSectionHeader => new TranslatableString(getKey(@"skin_section_header"), @"Skin");
/// <summary>
/// "Skin layout editor"
/// </summary>
public static LocalisableString SkinLayoutEditor => new TranslatableString(getKey(@"skin_layout_editor"), @"Skin layout editor");
/// <summary>
/// "Gameplay cursor size"
/// </summary>
public static LocalisableString GameplayCursorSize => new TranslatableString(getKey(@"gameplay_cursor_size"), @"Gameplay cursor size");
/// <summary>
/// "Adjust gameplay cursor size based on current beatmap"
/// </summary>
public static LocalisableString AutoCursorSize => new TranslatableString(getKey(@"auto_cursor_size"), @"Adjust gameplay cursor size based on current beatmap");
/// <summary>
/// "Beatmap skins"
/// </summary>
public static LocalisableString BeatmapSkins => new TranslatableString(getKey(@"beatmap_skins"), @"Beatmap skins");
/// <summary>
/// "Beatmap colours"
/// </summary>
public static LocalisableString BeatmapColours => new TranslatableString(getKey(@"beatmap_colours"), @"Beatmap colours");
/// <summary>
/// "Beatmap hitsounds"
/// </summary>
public static LocalisableString BeatmapHitsounds => new TranslatableString(getKey(@"beatmap_hitsounds"), @"Beatmap hitsounds");
/// <summary>
/// "Export selected skin"
/// </summary>
public static LocalisableString ExportSkinButton => new TranslatableString(getKey(@"export_skin_button"), @"Export selected skin");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

@ -0,0 +1,114 @@
// 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 UserInterfaceStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.UserInterface";
/// <summary>
/// "User Interface"
/// </summary>
public static LocalisableString UserInterfaceSectionHeader => new TranslatableString(getKey(@"user_interface_section_header"), @"User Interface");
/// <summary>
/// "General"
/// </summary>
public static LocalisableString GeneralHeader => new TranslatableString(getKey(@"general_header"), @"General");
/// <summary>
/// "Rotate cursor when dragging"
/// </summary>
public static LocalisableString CursorRotation => new TranslatableString(getKey(@"cursor_rotation"), @"Rotate cursor when dragging");
/// <summary>
/// "Menu cursor size"
/// </summary>
public static LocalisableString MenuCursorSize => new TranslatableString(getKey(@"menu_cursor_size"), @"Menu cursor size");
/// <summary>
/// "Parallax"
/// </summary>
public static LocalisableString Parallax => new TranslatableString(getKey(@"parallax"), @"Parallax");
/// <summary>
/// "Hold-to-confirm activation time"
/// </summary>
public static LocalisableString HoldToConfirmActivationTime => new TranslatableString(getKey(@"hold_to_confirm_activation_time"), @"Hold-to-confirm activation time");
/// <summary>
/// "Main Menu"
/// </summary>
public static LocalisableString MainMenuHeader => new TranslatableString(getKey(@"main_menu_header"), @"Main Menu");
/// <summary>
/// "Interface voices"
/// </summary>
public static LocalisableString InterfaceVoices => new TranslatableString(getKey(@"interface_voices"), @"Interface voices");
/// <summary>
/// "osu! music theme"
/// </summary>
public static LocalisableString OsuMusicTheme => new TranslatableString(getKey(@"osu_music_theme"), @"osu! music theme");
/// <summary>
/// "Intro sequence"
/// </summary>
public static LocalisableString IntroSequence => new TranslatableString(getKey(@"intro_sequence"), @"Intro sequence");
/// <summary>
/// "Background source"
/// </summary>
public static LocalisableString BackgroundSource => new TranslatableString(getKey(@"background_source"), @"Background source");
/// <summary>
/// "Seasonal backgrounds"
/// </summary>
public static LocalisableString SeasonalBackgrounds => new TranslatableString(getKey(@"seasonal_backgrounds"), @"Seasonal backgrounds");
/// <summary>
/// "Changes to this setting will only apply with an active osu!supporter tag."
/// </summary>
public static LocalisableString NotSupporterNote => new TranslatableString(getKey(@"not_supporter_note"), @"Changes to this setting will only apply with an active osu!supporter tag.");
/// <summary>
/// "Song Select"
/// </summary>
public static LocalisableString SongSelectHeader => new TranslatableString(getKey(@"song_select_header"), @"Song Select");
/// <summary>
/// "Right mouse drag to absolute scroll"
/// </summary>
public static LocalisableString RightMouseScroll => new TranslatableString(getKey(@"right_mouse_scroll"), @"Right mouse drag to absolute scroll");
/// <summary>
/// "Show converted beatmaps"
/// </summary>
public static LocalisableString ShowConvertedBeatmaps => new TranslatableString(getKey(@"show_converted_beatmaps"), @"Show converted beatmaps");
/// <summary>
/// "Display beatmaps from"
/// </summary>
public static LocalisableString StarsMinimum => new TranslatableString(getKey(@"stars_minimum"), @"Display beatmaps from");
/// <summary>
/// "up to"
/// </summary>
public static LocalisableString StarsMaximum => new TranslatableString(getKey(@"stars_maximum"), @"up to");
/// <summary>
/// "Random selection algorithm"
/// </summary>
public static LocalisableString RandomSelectionAlgorithm => new TranslatableString(getKey(@"random_selection_algorithm"), @"Random selection algorithm");
/// <summary>
/// "no limit"
/// </summary>
public static LocalisableString NoLimit => new TranslatableString(getKey(@"no_limit"), @"no limit");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

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

View File

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

View File

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

View File

@ -27,6 +27,14 @@ namespace osu.Game.Online.Multiplayer
/// <exception cref="NotJoinedRoomException">If the user is not in a room.</exception> /// <exception cref="NotJoinedRoomException">If the user is not in a room.</exception>
Task TransferHost(int userId); Task TransferHost(int userId);
/// <summary>
/// As the host, kick another user from the room.
/// </summary>
/// <param name="userId">The user to kick..</param>
/// <exception cref="NotHostException">A user other than the current host is attempting to kick a user.</exception>
/// <exception cref="NotJoinedRoomException">If the user is not in a room.</exception>
Task KickUser(int userId);
/// <summary> /// <summary>
/// As the host, update the settings of the currently joined room. /// As the host, update the settings of the currently joined room.
/// </summary> /// </summary>

View File

@ -293,6 +293,8 @@ namespace osu.Game.Online.Multiplayer
public abstract Task TransferHost(int userId); public abstract Task TransferHost(int userId);
public abstract Task KickUser(int userId);
public abstract Task ChangeSettings(MultiplayerRoomSettings settings); public abstract Task ChangeSettings(MultiplayerRoomSettings settings);
public abstract Task ChangeState(MultiplayerUserState newState); public abstract Task ChangeState(MultiplayerUserState newState);
@ -387,6 +389,18 @@ namespace osu.Game.Online.Multiplayer
return Task.CompletedTask; return Task.CompletedTask;
} }
Task IMultiplayerClient.UserKicked(MultiplayerRoomUser user)
{
if (LocalUser == null)
return Task.CompletedTask;
if (user.Equals(LocalUser))
LeaveRoom();
// TODO: also inform users of the kick operation.
return ((IMultiplayerClient)this).UserLeft(user);
}
Task IMultiplayerClient.HostChanged(int userId) Task IMultiplayerClient.HostChanged(int userId)
{ {
if (Room == null) if (Room == null)

View File

@ -50,6 +50,7 @@ namespace osu.Game.Online.Multiplayer
connection.On<MultiplayerRoomState>(nameof(IMultiplayerClient.RoomStateChanged), ((IMultiplayerClient)this).RoomStateChanged); connection.On<MultiplayerRoomState>(nameof(IMultiplayerClient.RoomStateChanged), ((IMultiplayerClient)this).RoomStateChanged);
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined); connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined);
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft); connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft);
connection.On<MultiplayerRoomUser>(nameof(IMultiplayerClient.UserKicked), ((IMultiplayerClient)this).UserKicked);
connection.On<int>(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged); connection.On<int>(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged);
connection.On<MultiplayerRoomSettings>(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged); connection.On<MultiplayerRoomSettings>(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged);
connection.On<int, MultiplayerUserState>(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged); connection.On<int, MultiplayerUserState>(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged);
@ -91,6 +92,14 @@ namespace osu.Game.Online.Multiplayer
return connection.InvokeAsync(nameof(IMultiplayerServer.TransferHost), userId); return connection.InvokeAsync(nameof(IMultiplayerServer.TransferHost), userId);
} }
public override Task KickUser(int userId)
{
if (!IsConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.KickUser), userId);
}
public override Task ChangeSettings(MultiplayerRoomSettings settings) public override Task ChangeSettings(MultiplayerRoomSettings settings)
{ {
if (!IsConnected.Value) if (!IsConnected.Value)

View File

@ -8,7 +8,7 @@ namespace osu.Game.Online.Rooms.RoomStatuses
{ {
public class RoomStatusEnded : RoomStatus public class RoomStatusEnded : RoomStatus
{ {
public override string Message => @"Ended"; public override string Message => "Ended";
public override Color4 GetAppropriateColour(OsuColour colours) => colours.YellowDarker; public override Color4 GetAppropriateColour(OsuColour colours) => colours.YellowDarker;
} }
} }

View File

@ -8,7 +8,7 @@ namespace osu.Game.Online.Rooms.RoomStatuses
{ {
public class RoomStatusOpen : RoomStatus public class RoomStatusOpen : RoomStatus
{ {
public override string Message => @"Welcoming Players"; public override string Message => "Open";
public override Color4 GetAppropriateColour(OsuColour colours) => colours.GreenLight; public override Color4 GetAppropriateColour(OsuColour colours) => colours.GreenLight;
} }
} }

View File

@ -8,7 +8,7 @@ namespace osu.Game.Online.Rooms.RoomStatuses
{ {
public class RoomStatusPlaying : RoomStatus public class RoomStatusPlaying : RoomStatus
{ {
public override string Message => @"Now Playing"; public override string Message => "Playing";
public override Color4 GetAppropriateColour(OsuColour colours) => colours.Purple; public override Color4 GetAppropriateColour(OsuColour colours) => colours.Purple;
} }
} }

View File

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

View File

@ -139,19 +139,24 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
LoadComponentAsync(Preview = previewTrackManager.Get(beatmapSet), preview => LoadComponentAsync(Preview = previewTrackManager.Get(beatmapSet), preview =>
{ {
// beatmapset may have changed. // Make sure that we schedule to after the next audio frame to fix crashes in single-threaded execution.
if (Preview != preview) // See: https://github.com/ppy/osu-framework/issues/4692
return; Schedule(() =>
{
// beatmapset may have changed.
if (Preview != preview)
return;
AddInternal(preview); AddInternal(preview);
loading = false; loading = false;
// make sure that the update of value of Playing (and the ensuing value change callbacks) // make sure that the update of value of Playing (and the ensuing value change callbacks)
// are marshaled back to the update thread. // are marshaled back to the update thread.
preview.Stopped += () => Schedule(() => playing.Value = false); preview.Stopped += () => Schedule(() => playing.Value = false);
// user may have changed their mind. // user may have changed their mind.
if (playing.Value) if (playing.Value)
attemptStart(); attemptStart();
});
}); });
} }
else else

View File

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

View File

@ -71,6 +71,17 @@ namespace osu.Game.Overlays.Changelog
Colour = colourProvider.Background6, Colour = colourProvider.Background6,
Margin = new MarginPadding { Top = 30 }, Margin = new MarginPadding { Top = 30 },
}, },
new ChangelogSupporterPromo
{
Alpha = api.LocalUser.Value.IsSupporter ? 0 : 1,
},
new Box
{
RelativeSizeAxes = Axes.X,
Height = 2,
Colour = colourProvider.Background6,
Alpha = api.LocalUser.Value.IsSupporter ? 0 : 1,
},
comments = new CommentsContainer() comments = new CommentsContainer()
}; };

View File

@ -0,0 +1,200 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Chat;
using osu.Game.Resources.Localisation.Web;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Changelog
{
public class ChangelogSupporterPromo : CompositeDrawable
{
private const float image_container_width = 164;
private const float heart_size = 75;
private readonly FillFlowContainer textContainer;
private readonly Container imageContainer;
public ChangelogSupporterPromo()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Padding = new MarginPadding
{
Vertical = 20,
Horizontal = 50,
};
InternalChildren = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Masking = true,
CornerRadius = 6,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Colour = Color4.Black.Opacity(0.25f),
Offset = new Vector2(0, 1),
Radius = 3,
},
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black.Opacity(0.3f),
},
new Container
{
RelativeSizeAxes = Axes.X,
Height = 200,
Padding = new MarginPadding { Horizontal = 75 },
Children = new Drawable[]
{
textContainer = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Padding = new MarginPadding { Right = 50 + image_container_width },
},
imageContainer = new Container
{
RelativeSizeAxes = Axes.Y,
Width = image_container_width,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
}
}
},
}
},
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colour, TextureStore textures)
{
SupporterPromoLinkFlowContainer supportLinkText;
textContainer.Children = new Drawable[]
{
new OsuSpriteText
{
Text = ChangelogStrings.SupportHeading,
Font = OsuFont.GetFont(size: 20, weight: FontWeight.Light),
Margin = new MarginPadding { Bottom = 20 },
},
supportLinkText = new SupporterPromoLinkFlowContainer(t =>
{
t.Font = t.Font.With(size: 14);
t.Colour = colour.PinkLighter;
})
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
new OsuTextFlowContainer(t =>
{
t.Font = t.Font.With(size: 12);
t.Colour = colour.PinkLighter;
})
{
Text = ChangelogStrings.SupportText2.ToString(),
Margin = new MarginPadding { Top = 10 },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}
};
supportLinkText.AddText("Support further development of osu! and ");
supportLinkText.AddLink("become and osu!supporter", "https://osu.ppy.sh/home/support", t => t.Font = t.Font.With(weight: FontWeight.Bold));
supportLinkText.AddText(" today!");
imageContainer.Children = new Drawable[]
{
new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding { Bottom = 28 },
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill,
Texture = textures.Get(@"Online/supporter-pippi"),
},
new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Size = new Vector2(heart_size),
Margin = new MarginPadding { Top = 70 },
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Colour = colour.Pink,
Radius = 10,
Roundness = heart_size / 2,
},
Child = new Sprite
{
Size = new Vector2(heart_size),
Texture = textures.Get(@"Online/supporter-heart"),
},
},
};
}
private class SupporterPromoLinkFlowContainer : LinkFlowContainer
{
public SupporterPromoLinkFlowContainer(Action<SpriteText> defaultCreationParameters)
: base(defaultCreationParameters)
{
}
public new void AddLink(string text, string url, Action<SpriteText> creationParameters) =>
AddInternal(new SupporterPromoLinkCompiler(AddText(text, creationParameters)) { Url = url });
private class SupporterPromoLinkCompiler : DrawableLinkCompiler
{
[Resolved(CanBeNull = true)]
private OsuGame game { get; set; }
public string Url;
public SupporterPromoLinkCompiler(IEnumerable<Drawable> parts)
: base(parts)
{
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(OsuColour colour)
{
TooltipText = Url;
Action = () => game?.HandleLink(Url);
IdleColour = colour.PinkDark;
HoverColour = Color4.White;
}
}
}
}
}

View File

@ -14,6 +14,9 @@ using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Users; using osu.Game.Users;
using System.Collections.Generic;
using JetBrains.Annotations;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Overlays.Comments namespace osu.Game.Overlays.Comments
{ {
@ -147,7 +150,7 @@ namespace osu.Game.Overlays.Comments
private void refetchComments() private void refetchComments()
{ {
clearComments(); ClearComments();
getComments(); getComments();
} }
@ -160,50 +163,125 @@ namespace osu.Game.Overlays.Comments
loadCancellation?.Cancel(); loadCancellation?.Cancel();
scheduledCommentsLoad?.Cancel(); scheduledCommentsLoad?.Cancel();
request = new GetCommentsRequest(id.Value, type.Value, Sort.Value, currentPage++, 0); request = new GetCommentsRequest(id.Value, type.Value, Sort.Value, currentPage++, 0);
request.Success += res => scheduledCommentsLoad = Schedule(() => onSuccess(res)); request.Success += res => scheduledCommentsLoad = Schedule(() => OnSuccess(res));
api.PerformAsync(request); api.PerformAsync(request);
} }
private void clearComments() protected void ClearComments()
{ {
currentPage = 1; currentPage = 1;
deletedCommentsCounter.Count.Value = 0; deletedCommentsCounter.Count.Value = 0;
moreButton.Show(); moreButton.Show();
moreButton.IsLoading = true; moreButton.IsLoading = true;
content.Clear(); content.Clear();
CommentDictionary.Clear();
} }
private void onSuccess(CommentBundle response) protected readonly Dictionary<long, DrawableComment> CommentDictionary = new Dictionary<long, DrawableComment>();
protected void OnSuccess(CommentBundle response)
{ {
loadCancellation = new CancellationTokenSource(); commentCounter.Current.Value = response.Total;
LoadComponentAsync(new CommentsPage(response) if (!response.Comments.Any())
{ {
ShowDeleted = { BindTarget = ShowDeleted }, content.Add(new NoCommentsPlaceholder());
Sort = { BindTarget = Sort }, moreButton.Hide();
Type = { BindTarget = type }, return;
CommentableId = { BindTarget = id } }
}, loaded =>
AppendComments(response);
}
/// <summary>
/// Appends retrieved comments to the subtree rooted of comments in this page.
/// </summary>
/// <param name="bundle">The bundle of comments to add.</param>
protected void AppendComments([NotNull] CommentBundle bundle)
{
var topLevelComments = new List<DrawableComment>();
var orphaned = new List<Comment>();
foreach (var comment in bundle.Comments.Concat(bundle.IncludedComments))
{ {
content.Add(loaded); // Exclude possible duplicated comments.
if (CommentDictionary.ContainsKey(comment.Id))
continue;
deletedCommentsCounter.Count.Value += response.Comments.Count(c => c.IsDeleted && c.IsTopLevel); addNewComment(comment);
}
if (response.HasMore) // Comments whose parents were seen later than themselves can now be added.
foreach (var o in orphaned)
addNewComment(o);
if (topLevelComments.Any())
{
LoadComponentsAsync(topLevelComments, loaded =>
{ {
int loadedTopLevelComments = 0; content.AddRange(loaded);
content.Children.OfType<FillFlowContainer>().ForEach(p => loadedTopLevelComments += p.Children.OfType<DrawableComment>().Count());
moreButton.Current.Value = response.TopLevelCount - loadedTopLevelComments; deletedCommentsCounter.Count.Value += topLevelComments.Select(d => d.Comment).Count(c => c.IsDeleted && c.IsTopLevel);
moreButton.IsLoading = false;
if (bundle.HasMore)
{
int loadedTopLevelComments = 0;
content.Children.OfType<DrawableComment>().ForEach(p => loadedTopLevelComments++);
moreButton.Current.Value = bundle.TopLevelCount - loadedTopLevelComments;
moreButton.IsLoading = false;
}
else
{
moreButton.Hide();
}
}, (loadCancellation = new CancellationTokenSource()).Token);
}
void addNewComment(Comment comment)
{
var drawableComment = getDrawableComment(comment);
if (comment.ParentId == null)
{
// Comments that have no parent are added as top-level comments to the flow.
topLevelComments.Add(drawableComment);
}
else if (CommentDictionary.TryGetValue(comment.ParentId.Value, out var parentDrawable))
{
// The comment's parent has already been seen, so the parent<-> child links can be added.
comment.ParentComment = parentDrawable.Comment;
parentDrawable.Replies.Add(drawableComment);
} }
else else
{ {
moreButton.Hide(); // The comment's parent has not been seen yet, so keep it orphaned for the time being. This can occur if the comments arrive out of order.
// Since this comment has now been seen, any further children can be added to it without being orphaned themselves.
orphaned.Add(comment);
} }
}
}
commentCounter.Current.Value = response.Total; private DrawableComment getDrawableComment(Comment comment)
}, loadCancellation.Token); {
if (CommentDictionary.TryGetValue(comment.Id, out var existing))
return existing;
return CommentDictionary[comment.Id] = new DrawableComment(comment)
{
ShowDeleted = { BindTarget = ShowDeleted },
Sort = { BindTarget = Sort },
RepliesRequested = onCommentRepliesRequested
};
}
private void onCommentRepliesRequested(DrawableComment drawableComment, int page)
{
var req = new GetCommentsRequest(id.Value, type.Value, Sort.Value, page, drawableComment.Comment.Id);
req.Success += response => Schedule(() => AppendComments(response));
api.PerformAsync(req);
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
@ -212,5 +290,30 @@ namespace osu.Game.Overlays.Comments
loadCancellation?.Cancel(); loadCancellation?.Cancel();
base.Dispose(isDisposing); base.Dispose(isDisposing);
} }
private class NoCommentsPlaceholder : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Height = 80;
RelativeSizeAxes = Axes.X;
AddRangeInternal(new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Left = 50 },
Text = @"No comments yet."
}
});
}
}
} }
} }

View File

@ -1,161 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics;
using osu.Framework.Bindables;
using osu.Game.Online.API.Requests.Responses;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Sprites;
using System.Linq;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API;
using System.Collections.Generic;
using JetBrains.Annotations;
namespace osu.Game.Overlays.Comments
{
public class CommentsPage : CompositeDrawable
{
public readonly BindableBool ShowDeleted = new BindableBool();
public readonly Bindable<CommentsSortCriteria> Sort = new Bindable<CommentsSortCriteria>();
public readonly Bindable<CommentableType> Type = new Bindable<CommentableType>();
public readonly BindableLong CommentableId = new BindableLong();
[Resolved]
private IAPIProvider api { get; set; }
private readonly CommentBundle commentBundle;
private FillFlowContainer flow;
public CommentsPage(CommentBundle commentBundle)
{
this.commentBundle = commentBundle;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
AddRangeInternal(new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5
},
flow = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
}
});
if (!commentBundle.Comments.Any())
{
flow.Add(new NoCommentsPlaceholder());
return;
}
AppendComments(commentBundle);
}
private DrawableComment getDrawableComment(Comment comment)
{
if (CommentDictionary.TryGetValue(comment.Id, out var existing))
return existing;
return CommentDictionary[comment.Id] = new DrawableComment(comment)
{
ShowDeleted = { BindTarget = ShowDeleted },
Sort = { BindTarget = Sort },
RepliesRequested = onCommentRepliesRequested
};
}
private void onCommentRepliesRequested(DrawableComment drawableComment, int page)
{
var request = new GetCommentsRequest(CommentableId.Value, Type.Value, Sort.Value, page, drawableComment.Comment.Id);
request.Success += response => Schedule(() => AppendComments(response));
api.PerformAsync(request);
}
protected readonly Dictionary<long, DrawableComment> CommentDictionary = new Dictionary<long, DrawableComment>();
/// <summary>
/// Appends retrieved comments to the subtree rooted of comments in this page.
/// </summary>
/// <param name="bundle">The bundle of comments to add.</param>
protected void AppendComments([NotNull] CommentBundle bundle)
{
var orphaned = new List<Comment>();
foreach (var comment in bundle.Comments.Concat(bundle.IncludedComments))
{
// Exclude possible duplicated comments.
if (CommentDictionary.ContainsKey(comment.Id))
continue;
addNewComment(comment);
}
// Comments whose parents were seen later than themselves can now be added.
foreach (var o in orphaned)
addNewComment(o);
void addNewComment(Comment comment)
{
var drawableComment = getDrawableComment(comment);
if (comment.ParentId == null)
{
// Comments that have no parent are added as top-level comments to the flow.
flow.Add(drawableComment);
}
else if (CommentDictionary.TryGetValue(comment.ParentId.Value, out var parentDrawable))
{
// The comment's parent has already been seen, so the parent<-> child links can be added.
comment.ParentComment = parentDrawable.Comment;
parentDrawable.Replies.Add(drawableComment);
}
else
{
// The comment's parent has not been seen yet, so keep it orphaned for the time being. This can occur if the comments arrive out of order.
// Since this comment has now been seen, any further children can be added to it without being orphaned themselves.
orphaned.Add(comment);
}
}
}
private class NoCommentsPlaceholder : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Height = 80;
RelativeSizeAxes = Axes.X;
AddRangeInternal(new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Left = 50 },
Text = @"No comments yet."
}
});
}
}
}
}

View File

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

View File

@ -18,6 +18,7 @@ namespace osu.Game.Overlays
public static OverlayColourProvider Green { get; } = new OverlayColourProvider(OverlayColourScheme.Green); public static OverlayColourProvider Green { get; } = new OverlayColourProvider(OverlayColourScheme.Green);
public static OverlayColourProvider Purple { get; } = new OverlayColourProvider(OverlayColourScheme.Purple); public static OverlayColourProvider Purple { get; } = new OverlayColourProvider(OverlayColourScheme.Purple);
public static OverlayColourProvider Blue { get; } = new OverlayColourProvider(OverlayColourScheme.Blue); public static OverlayColourProvider Blue { get; } = new OverlayColourProvider(OverlayColourScheme.Blue);
public static OverlayColourProvider Plum { get; } = new OverlayColourProvider(OverlayColourScheme.Plum);
public OverlayColourProvider(OverlayColourScheme colourScheme) public OverlayColourProvider(OverlayColourScheme colourScheme)
{ {
@ -80,6 +81,9 @@ namespace osu.Game.Overlays
case OverlayColourScheme.Blue: case OverlayColourScheme.Blue:
return 200 / 360f; return 200 / 360f;
case OverlayColourScheme.Plum:
return 320 / 360f;
} }
} }
} }
@ -92,6 +96,7 @@ namespace osu.Game.Overlays
Lime, Lime,
Green, Green,
Purple, Purple,
Blue Blue,
Plum,
} }
} }

View File

@ -8,12 +8,13 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Settings.Sections.Audio namespace osu.Game.Overlays.Settings.Sections.Audio
{ {
public class AudioDevicesSettings : SettingsSubsection public class AudioDevicesSettings : SettingsSubsection
{ {
protected override LocalisableString Header => "Devices"; protected override LocalisableString Header => AudioSettingsStrings.AudioDevicesHeader;
[Resolved] [Resolved]
private AudioManager audio { get; set; } private AudioManager audio { get; set; }
@ -78,7 +79,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
private class AudioDeviceDropdownControl : DropdownControl private class AudioDeviceDropdownControl : DropdownControl
{ {
protected override LocalisableString GenerateItemText(string item) protected override LocalisableString GenerateItemText(string item)
=> string.IsNullOrEmpty(item) ? "Default" : base.GenerateItemText(item); => string.IsNullOrEmpty(item) ? CommonStrings.Default : base.GenerateItemText(item);
} }
} }
} }

View File

@ -1,17 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Settings.Sections.Audio namespace osu.Game.Overlays.Settings.Sections.Audio
{ {
public class OffsetSettings : SettingsSubsection public class OffsetSettings : SettingsSubsection
{ {
protected override LocalisableString Header => "Offset Adjustment"; protected override LocalisableString Header => AudioSettingsStrings.OffsetHeader;
public override IEnumerable<string> FilterTerms => base.FilterTerms.Concat(new[] { "universal", "uo", "timing" });
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config)
@ -20,13 +25,13 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
{ {
new SettingsSlider<double, OffsetSlider> new SettingsSlider<double, OffsetSlider>
{ {
LabelText = "Audio offset", LabelText = AudioSettingsStrings.AudioOffset,
Current = config.GetBindable<double>(OsuSetting.AudioOffset), Current = config.GetBindable<double>(OsuSetting.AudioOffset),
KeyboardStep = 1f KeyboardStep = 1f
}, },
new SettingsButton new SettingsButton
{ {
Text = "Offset wizard" Text = AudioSettingsStrings.OffsetWizard
} }
}; };
} }

View File

@ -6,12 +6,13 @@ using osu.Framework.Audio;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Settings.Sections.Audio namespace osu.Game.Overlays.Settings.Sections.Audio
{ {
public class VolumeSettings : SettingsSubsection public class VolumeSettings : SettingsSubsection
{ {
protected override LocalisableString Header => "Volume"; protected override LocalisableString Header => AudioSettingsStrings.VolumeHeader;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio, OsuConfigManager config) private void load(AudioManager audio, OsuConfigManager config)
@ -20,28 +21,28 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
{ {
new SettingsSlider<double> new SettingsSlider<double>
{ {
LabelText = "Master", LabelText = AudioSettingsStrings.MasterVolume,
Current = audio.Volume, Current = audio.Volume,
KeyboardStep = 0.01f, KeyboardStep = 0.01f,
DisplayAsPercentage = true DisplayAsPercentage = true
}, },
new SettingsSlider<double> new SettingsSlider<double>
{ {
LabelText = "Master (window inactive)", LabelText = AudioSettingsStrings.MasterVolumeInactive,
Current = config.GetBindable<double>(OsuSetting.VolumeInactive), Current = config.GetBindable<double>(OsuSetting.VolumeInactive),
KeyboardStep = 0.01f, KeyboardStep = 0.01f,
DisplayAsPercentage = true DisplayAsPercentage = true
}, },
new SettingsSlider<double> new SettingsSlider<double>
{ {
LabelText = "Effect", LabelText = AudioSettingsStrings.EffectVolume,
Current = audio.VolumeSample, Current = audio.VolumeSample,
KeyboardStep = 0.01f, KeyboardStep = 0.01f,
DisplayAsPercentage = true DisplayAsPercentage = true
}, },
new SettingsSlider<double> new SettingsSlider<double>
{ {
LabelText = "Music", LabelText = AudioSettingsStrings.MusicVolume,
Current = audio.VolumeTrack, Current = audio.VolumeTrack,
KeyboardStep = 0.01f, KeyboardStep = 0.01f,
DisplayAsPercentage = true DisplayAsPercentage = true

View File

@ -3,15 +3,17 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Localisation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings.Sections.Audio; using osu.Game.Overlays.Settings.Sections.Audio;
namespace osu.Game.Overlays.Settings.Sections namespace osu.Game.Overlays.Settings.Sections
{ {
public class AudioSection : SettingsSection public class AudioSection : SettingsSection
{ {
public override string Header => "Audio"; public override LocalisableString Header => AudioSettingsStrings.AudioSectionHeader;
public override Drawable CreateIcon() => new SpriteIcon public override Drawable CreateIcon() => new SpriteIcon
{ {

View File

@ -6,13 +6,14 @@ using osu.Framework.Configuration;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Localisation;
using osu.Game.Screens.Import; using osu.Game.Screens.Import;
namespace osu.Game.Overlays.Settings.Sections.Debug namespace osu.Game.Overlays.Settings.Sections.Debug
{ {
public class GeneralSettings : SettingsSubsection public class GeneralSettings : SettingsSubsection
{ {
protected override LocalisableString Header => "General"; protected override LocalisableString Header => DebugSettingsStrings.GeneralHeader;
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig, OsuGame game) private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig, OsuGame game)
@ -21,18 +22,18 @@ namespace osu.Game.Overlays.Settings.Sections.Debug
{ {
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Show log overlay", LabelText = DebugSettingsStrings.ShowLogOverlay,
Current = frameworkConfig.GetBindable<bool>(FrameworkSetting.ShowLogOverlay) Current = frameworkConfig.GetBindable<bool>(FrameworkSetting.ShowLogOverlay)
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Bypass front-to-back render pass", LabelText = DebugSettingsStrings.BypassFrontToBackPass,
Current = config.GetBindable<bool>(DebugSetting.BypassFrontToBackPass) Current = config.GetBindable<bool>(DebugSetting.BypassFrontToBackPass)
} }
}; };
Add(new SettingsButton Add(new SettingsButton
{ {
Text = "Import files", Text = DebugSettingsStrings.ImportFiles,
Action = () => game?.PerformFromScreen(menu => menu.Push(new FileImportScreen())) Action = () => game?.PerformFromScreen(menu => menu.Push(new FileImportScreen()))
}); });
} }

View File

@ -6,12 +6,13 @@ using osu.Framework.Configuration;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Settings.Sections.Debug namespace osu.Game.Overlays.Settings.Sections.Debug
{ {
public class MemorySettings : SettingsSubsection public class MemorySettings : SettingsSubsection
{ {
protected override LocalisableString Header => "Memory"; protected override LocalisableString Header => DebugSettingsStrings.MemoryHeader;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(FrameworkDebugConfigManager config, GameHost host) private void load(FrameworkDebugConfigManager config, GameHost host)
@ -20,7 +21,7 @@ namespace osu.Game.Overlays.Settings.Sections.Debug
{ {
new SettingsButton new SettingsButton
{ {
Text = "Clear all caches", Text = DebugSettingsStrings.ClearAllCaches,
Action = host.Collect Action = host.Collect
}, },
}; };

View File

@ -3,13 +3,15 @@
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings.Sections.Debug; using osu.Game.Overlays.Settings.Sections.Debug;
namespace osu.Game.Overlays.Settings.Sections namespace osu.Game.Overlays.Settings.Sections
{ {
public class DebugSection : SettingsSection public class DebugSection : SettingsSection
{ {
public override string Header => "Debug"; public override LocalisableString Header => DebugSettingsStrings.DebugSectionHeader;
public override Drawable CreateIcon() => new SpriteIcon public override Drawable CreateIcon() => new SpriteIcon
{ {

Some files were not shown because too many files have changed in this diff Show More