Merge branch 'master' into star-rating-display-v2

This commit is contained in:
Dean Herbert
2021-08-18 17:22:33 +09:00
272 changed files with 6432 additions and 2718 deletions

View File

@ -4,7 +4,7 @@
# osu! # osu!
[![Build status](https://ci.appveyor.com/api/projects/status/u2p01nx7l6og8buh?svg=true)](https://ci.appveyor.com/project/peppy/osu) [![Build status](https://github.com/ppy/osu/actions/workflows/ci.yml/badge.svg?branch=master&event=push)](https://github.com/ppy/osu/actions/workflows/ci.yml)
[![GitHub release](https://img.shields.io/github/release/ppy/osu.svg)](https://github.com/ppy/osu/releases/latest) [![GitHub release](https://img.shields.io/github/release/ppy/osu.svg)](https://github.com/ppy/osu/releases/latest)
[![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu) [![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu)
[![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy) [![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy)

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.803.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.813.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.805.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.818.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

@ -5,23 +5,23 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Screens.Play;
namespace osu.Desktop.Windows namespace osu.Desktop.Windows
{ {
public class GameplayWinKeyBlocker : Component public class GameplayWinKeyBlocker : Component
{ {
private Bindable<bool> disableWinKey; private Bindable<bool> disableWinKey;
private Bindable<bool> localUserPlaying; private IBindable<bool> localUserPlaying;
[Resolved] [Resolved]
private GameHost host { get; set; } private GameHost host { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuGame game, OsuConfigManager config) private void load(ILocalUserPlayInfo localUserInfo, OsuConfigManager config)
{ {
localUserPlaying = game.LocalUserPlaying.GetBoundCopy(); localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy();
localUserPlaying.BindValueChanged(_ => updateBlocking()); localUserPlaying.BindValueChanged(_ => updateBlocking());
disableWinKey = config.GetBindable<bool>(OsuSetting.GameplayDisableWinKey); disableWinKey = config.GetBindable<bool>(OsuSetting.GameplayDisableWinKey);

View File

@ -0,0 +1,120 @@
// 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 NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests.Mods
{
[TestFixture]
public class CatchModMirrorTest
{
[Test]
public void TestModMirror()
{
IBeatmap original = createBeatmap(false);
IBeatmap mirrored = createBeatmap(true);
assertEffectivePositionsMirrored(original, mirrored);
}
private static IBeatmap createBeatmap(bool withMirrorMod)
{
var beatmap = createRawBeatmap();
var mirrorMod = new CatchModMirror();
var beatmapProcessor = new CatchBeatmapProcessor(beatmap);
beatmapProcessor.PreProcess();
foreach (var hitObject in beatmap.HitObjects)
hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
beatmapProcessor.PostProcess();
if (withMirrorMod)
mirrorMod.ApplyToBeatmap(beatmap);
return beatmap;
}
private static IBeatmap createRawBeatmap() => new Beatmap
{
HitObjects = new List<HitObject>
{
new Fruit
{
OriginalX = 150,
StartTime = 0
},
new Fruit
{
OriginalX = 450,
StartTime = 500
},
new JuiceStream
{
OriginalX = 250,
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(new Vector2(-100, 1)),
new PathControlPoint(new Vector2(0, 2)),
new PathControlPoint(new Vector2(100, 3)),
new PathControlPoint(new Vector2(0, 4))
}
},
StartTime = 1000,
},
new BananaShower
{
StartTime = 5000,
Duration = 5000
}
}
};
private static void assertEffectivePositionsMirrored(IBeatmap original, IBeatmap mirrored)
{
if (original.HitObjects.Count != mirrored.HitObjects.Count)
Assert.Fail($"Top-level object count mismatch (original: {original.HitObjects.Count}, mirrored: {mirrored.HitObjects.Count})");
for (int i = 0; i < original.HitObjects.Count; ++i)
{
var originalObject = (CatchHitObject)original.HitObjects[i];
var mirroredObject = (CatchHitObject)mirrored.HitObjects[i];
// banana showers themselves are exempt, as we only really care about their nested bananas' positions.
if (!effectivePositionMirrored(originalObject, mirroredObject) && !(originalObject is BananaShower))
Assert.Fail($"{originalObject.GetType().Name} at time {originalObject.StartTime} is not mirrored ({printEffectivePositions(originalObject, mirroredObject)})");
if (originalObject.NestedHitObjects.Count != mirroredObject.NestedHitObjects.Count)
Assert.Fail($"{originalObject.GetType().Name} nested object count mismatch (original: {originalObject.NestedHitObjects.Count}, mirrored: {mirroredObject.NestedHitObjects.Count})");
for (int j = 0; j < originalObject.NestedHitObjects.Count; ++j)
{
var originalNested = (CatchHitObject)originalObject.NestedHitObjects[j];
var mirroredNested = (CatchHitObject)mirroredObject.NestedHitObjects[j];
if (!effectivePositionMirrored(originalNested, mirroredNested))
Assert.Fail($"{originalObject.GetType().Name}'s nested {originalNested.GetType().Name} at time {originalObject.StartTime} is not mirrored ({printEffectivePositions(originalNested, mirroredNested)})");
}
}
}
private static string printEffectivePositions(CatchHitObject original, CatchHitObject mirrored)
=> $"original X: {original.EffectiveX}, mirrored X is: {mirrored.EffectiveX}, mirrored X should be: {CatchPlayfield.WIDTH - original.EffectiveX}";
private static bool effectivePositionMirrored(CatchHitObject original, CatchHitObject mirrored)
=> Precision.AlmostEquals(original.EffectiveX, CatchPlayfield.WIDTH - mirrored.EffectiveX);
}
}

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

@ -117,6 +117,7 @@ namespace osu.Game.Rulesets.Catch
{ {
new CatchModDifficultyAdjust(), new CatchModDifficultyAdjust(),
new CatchModClassic(), new CatchModClassic(),
new CatchModMirror(),
}; };
case ModType.Automation: case ModType.Automation:

View File

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

View File

@ -9,7 +9,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Difficulty.Skills namespace osu.Game.Rulesets.Catch.Difficulty.Skills
{ {
public class Movement : StrainSkill public class Movement : StrainDecaySkill
{ {
private const float absolute_player_positioning_error = 16f; private const float absolute_player_positioning_error = 16f;
private const float normalized_hitobject_radius = 41.0f; private const float normalized_hitobject_radius = 41.0f;

View File

@ -0,0 +1,87 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osuTK;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModMirror : ModMirror, IApplicableToBeatmap
{
public override string Description => "Fruits are flipped horizontally.";
/// <remarks>
/// <see cref="IApplicableToBeatmap"/> is used instead of <see cref="IApplicableToHitObject"/>,
/// as <see cref="CatchBeatmapProcessor"/> applies offsets in <see cref="CatchBeatmapProcessor.PostProcess"/>.
/// <see cref="IApplicableToBeatmap"/> runs after post-processing, while <see cref="IApplicableToHitObject"/> runs before it.
/// </remarks>
public void ApplyToBeatmap(IBeatmap beatmap)
{
foreach (var hitObject in beatmap.HitObjects)
applyToHitObject(hitObject);
}
private void applyToHitObject(HitObject hitObject)
{
var catchObject = (CatchHitObject)hitObject;
switch (catchObject)
{
case Fruit fruit:
mirrorEffectiveX(fruit);
break;
case JuiceStream juiceStream:
mirrorEffectiveX(juiceStream);
mirrorJuiceStreamPath(juiceStream);
break;
case BananaShower bananaShower:
mirrorBananaShower(bananaShower);
break;
}
}
/// <summary>
/// Mirrors the effective X position of <paramref name="catchObject"/> and its nested hit objects.
/// </summary>
private static void mirrorEffectiveX(CatchHitObject catchObject)
{
catchObject.OriginalX = CatchPlayfield.WIDTH - catchObject.OriginalX;
catchObject.XOffset = -catchObject.XOffset;
foreach (var nested in catchObject.NestedHitObjects.Cast<CatchHitObject>())
{
nested.OriginalX = CatchPlayfield.WIDTH - nested.OriginalX;
nested.XOffset = -nested.XOffset;
}
}
/// <summary>
/// Mirrors the path of the <paramref name="juiceStream"/>.
/// </summary>
private static void mirrorJuiceStreamPath(JuiceStream juiceStream)
{
var controlPoints = juiceStream.Path.ControlPoints.Select(p => new PathControlPoint(p.Position.Value, p.Type.Value)).ToArray();
foreach (var point in controlPoints)
point.Position.Value = new Vector2(-point.Position.Value.X, point.Position.Value.Y);
juiceStream.Path = new SliderPath(controlPoints, juiceStream.Path.ExpectedDistance.Value);
}
/// <summary>
/// Mirrors X positions of all bananas in the <paramref name="bananaShower"/>.
/// </summary>
private static void mirrorBananaShower(BananaShower bananaShower)
{
foreach (var banana in bananaShower.NestedHitObjects.OfType<Banana>())
banana.XOffset = CatchPlayfield.WIDTH - banana.XOffset;
}
}
}

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

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Difficulty.Skills namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{ {
public class Strain : StrainSkill public class Strain : StrainDecaySkill
{ {
private const double individual_decay_base = 0.125; private const double individual_decay_base = 0.125;
private const double overall_decay_base = 0.30; private const double overall_decay_base = 0.30;
@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
return individualStrain + overallStrain - CurrentStrain; return individualStrain + overallStrain - CurrentStrain;
} }
protected override double GetPeakStrain(double offset) protected override double CalculateInitialStrain(double offset)
=> applyDecay(individualStrain, offset - Previous[0].StartTime, individual_decay_base) => applyDecay(individualStrain, offset - Previous[0].StartTime, individual_decay_base)
+ applyDecay(overallStrain, offset - Previous[0].StartTime, overall_decay_base); + applyDecay(overallStrain, offset - Previous[0].StartTime, overall_decay_base);

View File

@ -10,7 +10,7 @@ using osu.Framework.Utils;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{ {
public abstract class OsuStrainSkill : StrainSkill public abstract class OsuStrainSkill : StrainDecaySkill
{ {
/// <summary> /// <summary>
/// The number of sections with the highest strains, which the peak strain reductions will apply to. /// The number of sections with the highest strains, which the peak strain reductions will apply to.

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)
{ {
@ -59,11 +64,14 @@ namespace osu.Game.Rulesets.Osu.Edit
if (hitObject is DrawableHitCircle circle) if (hitObject is DrawableHitCircle circle)
{ {
circle.ApproachCircle using (circle.BeginAbsoluteSequence(circle.HitStateUpdateTime))
.FadeOutFromOne(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION * 4) {
.Expire(); circle.ApproachCircle
.FadeOutFromOne(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION * 4)
.Expire();
circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint); circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint);
}
} }
if (hitObject is IHasMainCirclePiece mainPieceContainer) if (hitObject is IHasMainCirclePiece mainPieceContainer)

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

@ -3,9 +3,9 @@
using System; using System;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Utils;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Utils;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy namespace osu.Game.Rulesets.Osu.Skinning.Legacy
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
// Roughly matches osu!stable's slider border portions. // Roughly matches osu!stable's slider border portions.
=> base.CalculatedBorderPortion * 0.77f; => base.CalculatedBorderPortion * 0.77f;
public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, base.AccentColour.A * 0.70f); public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, 0.7f);
protected override Color4 ColourAt(float position) protected override Color4 ColourAt(float position)
{ {
@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Color4 outerColour = AccentColour.Darken(0.1f); Color4 outerColour = AccentColour.Darken(0.1f);
Color4 innerColour = lighten(AccentColour, 0.5f); Color4 innerColour = lighten(AccentColour, 0.5f);
return Interpolation.ValueAt(position / realGradientPortion, outerColour, innerColour, 0, 1); return LegacyUtils.InterpolateNonLinear(position / realGradientPortion, outerColour, innerColour, 0, 1);
} }
/// <summary> /// <summary>

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

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
/// <summary> /// <summary>
/// Calculates the colour coefficient of taiko difficulty. /// Calculates the colour coefficient of taiko difficulty.
/// </summary> /// </summary>
public class Colour : StrainSkill public class Colour : StrainDecaySkill
{ {
protected override double SkillMultiplier => 1; protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.4; protected override double StrainDecayBase => 0.4;

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
/// <summary> /// <summary>
/// Calculates the rhythm coefficient of taiko difficulty. /// Calculates the rhythm coefficient of taiko difficulty.
/// </summary> /// </summary>
public class Rhythm : StrainSkill public class Rhythm : StrainDecaySkill
{ {
protected override double SkillMultiplier => 10; protected override double SkillMultiplier => 10;
protected override double StrainDecayBase => 0; protected override double StrainDecayBase => 0;

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
/// <remarks> /// <remarks>
/// The reference play style chosen uses two hands, with full alternating (the hand changes after every hit). /// The reference play style chosen uses two hands, with full alternating (the hand changes after every hit).
/// </remarks> /// </remarks>
public class Stamina : StrainSkill public class Stamina : StrainDecaySkill
{ {
protected override double SkillMultiplier => 1; protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.4; protected override double StrainDecayBase => 0.4;

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

@ -2,11 +2,13 @@
// 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 NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Gameplay namespace osu.Game.Tests.Gameplay
@ -121,6 +123,18 @@ namespace osu.Game.Tests.Gameplay
AddAssert("Drawable lifetime is restored", () => dho.LifetimeStart == 666 && dho.LifetimeEnd == 999); AddAssert("Drawable lifetime is restored", () => dho.LifetimeStart == 666 && dho.LifetimeEnd == 999);
} }
[Test]
public void TestStateChangeBeforeLoadComplete()
{
TestDrawableHitObject dho = null;
AddStep("Add DHO and apply result", () =>
{
Child = dho = new TestDrawableHitObject(new HitObject { StartTime = Time.Current });
dho.MissForcefully();
});
AddAssert("DHO state is correct", () => dho.State.Value == ArmedState.Miss);
}
private class TestDrawableHitObject : DrawableHitObject private class TestDrawableHitObject : DrawableHitObject
{ {
public const double INITIAL_LIFETIME_OFFSET = 100; public const double INITIAL_LIFETIME_OFFSET = 100;
@ -141,6 +155,19 @@ namespace osu.Game.Tests.Gameplay
if (SetLifetimeStartOnApply) if (SetLifetimeStartOnApply)
LifetimeStart = LIFETIME_ON_APPLY; LifetimeStart = LIFETIME_ON_APPLY;
} }
public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss);
protected override void UpdateHitStateTransforms(ArmedState state)
{
if (state != ArmedState.Miss)
{
base.UpdateHitStateTransforms(state);
return;
}
this.FadeOut(1000);
}
} }
private class TestLifetimeEntry : HitObjectLifetimeEntry private class TestLifetimeEntry : HitObjectLifetimeEntry

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

@ -24,6 +24,8 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
AddRepeatStep("add some users", () => Client.AddUser(new User { Id = id++ }), 5); AddRepeatStep("add some users", () => Client.AddUser(new User { Id = id++ }), 5);
checkPlayingUserCount(0); checkPlayingUserCount(0);
AddAssert("playlist item is available", () => Client.CurrentMatchPlayingItem.Value != null);
changeState(3, MultiplayerUserState.WaitingForLoad); changeState(3, MultiplayerUserState.WaitingForLoad);
checkPlayingUserCount(3); checkPlayingUserCount(3);
@ -41,6 +43,8 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
AddStep("leave room", () => Client.LeaveRoom()); AddStep("leave room", () => Client.LeaveRoom());
checkPlayingUserCount(0); checkPlayingUserCount(0);
AddAssert("playlist item is null", () => Client.CurrentMatchPlayingItem.Value == null);
} }
[Test] [Test]

View File

@ -50,10 +50,10 @@ namespace osu.Game.Tests.Skins.IO
var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin", "skinner"), "skin2.osk")); var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin", "skinner"), "skin2.osk"));
Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID)); Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID));
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins().Count, Is.EqualTo(1)); Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins(true).Count, Is.EqualTo(1));
// the first should be overwritten by the second import. // the first should be overwritten by the second import.
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins().First().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID)); Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins(true).First().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID));
} }
finally finally
{ {
@ -76,10 +76,10 @@ namespace osu.Game.Tests.Skins.IO
var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk(string.Empty, string.Empty), "download.osk")); var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk(string.Empty, string.Empty), "download.osk"));
Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID)); Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID));
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins().Count, Is.EqualTo(2)); Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins(true).Count, Is.EqualTo(2));
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins().First().Files.First().FileInfoID, Is.EqualTo(imported.Files.First().FileInfoID)); Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins(true).First().Files.First().FileInfoID, Is.EqualTo(imported.Files.First().FileInfoID));
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins().Last().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID)); Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins(true).Last().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID));
} }
finally finally
{ {
@ -101,10 +101,10 @@ namespace osu.Game.Tests.Skins.IO
var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin v2.1", "skinner"), "skin2.osk")); var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin v2.1", "skinner"), "skin2.osk"));
Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID)); Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID));
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins().Count, Is.EqualTo(2)); Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins(true).Count, Is.EqualTo(2));
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins().First().Files.First().FileInfoID, Is.EqualTo(imported.Files.First().FileInfoID)); Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins(true).First().Files.First().FileInfoID, Is.EqualTo(imported.Files.First().FileInfoID));
Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins().Last().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID)); Assert.That(osu.Dependencies.Get<SkinManager>().GetAllUserSkins(true).Last().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID));
} }
finally finally
{ {

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,12 +12,15 @@ 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
{ {
public class TestSceneHUDOverlay : OsuManualInputManagerTestScene public class TestSceneHUDOverlay : OsuManualInputManagerTestScene
{ {
private OsuConfigManager localConfig;
private HUDOverlay hudOverlay; private HUDOverlay hudOverlay;
[Cached] [Cached]
@ -30,8 +33,14 @@ namespace osu.Game.Tests.Visual.Gameplay
private Drawable hideTarget => hudOverlay.KeyCounter; private Drawable hideTarget => hudOverlay.KeyCounter;
private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First(); private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First();
[Resolved] [BackgroundDependencyLoader]
private OsuConfigManager config { get; set; } private void load()
{
Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage));
}
[SetUp]
public void SetUp() => Schedule(() => localConfig.SetValue(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always));
[Test] [Test]
public void TestComboCounterIncrementing() public void TestComboCounterIncrementing()
@ -84,11 +93,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
createNew(); createNew();
HUDVisibilityMode originalConfigValue = HUDVisibilityMode.HideDuringGameplay; AddStep("set hud to never show", () => localConfig.SetValue(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Never));
AddStep("get original config value", () => originalConfigValue = config.Get<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode));
AddStep("set hud to never show", () => config.SetValue(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Never));
AddUntilStep("wait for fade", () => !hideTarget.IsPresent); AddUntilStep("wait for fade", () => !hideTarget.IsPresent);
@ -97,37 +102,28 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("stop trigering", () => InputManager.ReleaseKey(Key.ControlLeft)); AddStep("stop trigering", () => InputManager.ReleaseKey(Key.ControlLeft));
AddUntilStep("wait for fade", () => !hideTarget.IsPresent); AddUntilStep("wait for fade", () => !hideTarget.IsPresent);
AddStep("set original config value", () => config.SetValue(OsuSetting.HUDVisibilityMode, originalConfigValue));
} }
[Test] [Test]
public void TestExternalHideDoesntAffectConfig() public void TestExternalHideDoesntAffectConfig()
{ {
HUDVisibilityMode originalConfigValue = HUDVisibilityMode.HideDuringGameplay;
createNew(); createNew();
AddStep("get original config value", () => originalConfigValue = config.Get<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode));
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
AddAssert("config unchanged", () => originalConfigValue == config.Get<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode)); AddAssert("config unchanged", () => localConfig.GetBindable<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode).IsDefault);
AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true); AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true);
AddAssert("config unchanged", () => originalConfigValue == config.Get<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode)); AddAssert("config unchanged", () => localConfig.GetBindable<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode).IsDefault);
} }
[Test] [Test]
public void TestChangeHUDVisibilityOnHiddenKeyCounter() public void TestChangeHUDVisibilityOnHiddenKeyCounter()
{ {
bool keyCounterVisibleValue = false;
createNew(); createNew();
AddStep("save keycounter visible value", () => keyCounterVisibleValue = config.Get<bool>(OsuSetting.KeyOverlay));
AddStep("set keycounter visible false", () => AddStep("hide key overlay", () =>
{ {
config.SetValue(OsuSetting.KeyOverlay, false); localConfig.SetValue(OsuSetting.KeyOverlay, false);
hudOverlay.KeyCounter.AlwaysVisible.Value = false; hudOverlay.KeyCounter.AlwaysVisible.Value = false;
}); });
@ -138,8 +134,16 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true); AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true);
AddUntilStep("hidetarget is visible", () => hideTarget.IsPresent); AddUntilStep("hidetarget is visible", () => hideTarget.IsPresent);
AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent); AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent);
}
AddStep("return value", () => config.SetValue(OsuSetting.KeyOverlay, keyCounterVisibleValue)); [Test]
public void TestHiddenHUDDoesntBlockSkinnableComponentsLoad()
{
AddStep("set hud to never show", () => localConfig.SetValue(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Never));
createNew();
AddUntilStep("wait for hud load", () => hudOverlay.IsLoaded);
AddUntilStep("skinnable components loaded", () => hudOverlay.ChildrenOfType<SkinnableTargetContainer>().Single().ComponentsLoaded);
} }
private void createNew(Action<HUDOverlay> action = null) private void createNew(Action<HUDOverlay> action = null)
@ -158,5 +162,11 @@ namespace osu.Game.Tests.Visual.Gameplay
Child = hudOverlay; Child = hudOverlay;
}); });
} }
protected override void Dispose(bool isDisposing)
{
localConfig?.Dispose();
base.Dispose(isDisposing);
}
} }
} }

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

@ -14,7 +14,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing.Panels;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
@ -215,7 +215,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
assertDownloadButtonVisible(false); assertDownloadButtonVisible(false);
void assertDownloadButtonVisible(bool visible) => AddUntilStep($"download button {(visible ? "shown" : "hidden")}", void assertDownloadButtonVisible(bool visible) => AddUntilStep($"download button {(visible ? "shown" : "hidden")}",
() => playlist.ChildrenOfType<BeatmapDownloadTrackingComposite>().Single().Alpha == (visible ? 1 : 0)); () => playlist.ChildrenOfType<BeatmapPanelDownloadButton>().Single().Alpha == (visible ? 1 : 0));
} }
[Test] [Test]
@ -229,7 +229,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
createPlaylist(byOnlineId, byChecksum); createPlaylist(byOnlineId, byChecksum);
AddAssert("download buttons shown", () => playlist.ChildrenOfType<BeatmapDownloadTrackingComposite>().All(d => d.IsPresent)); AddAssert("download buttons shown", () => playlist.ChildrenOfType<BeatmapPanelDownloadButton>().All(d => d.IsPresent));
} }
[Test] [Test]

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

@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
var room = RoomManager.Rooms[1]; var room = RoomManager.Rooms[1];
RoomManager.RemoveRoom(room); RoomManager.RemoveRoom(room);
RoomManager.AddRoom(room); RoomManager.AddOrUpdateRoom(room);
}); });
AddAssert("no selection", () => checkRoomSelected(null)); AddAssert("no selection", () => checkRoomSelected(null));
@ -115,11 +115,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4); AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4);
AddStep("filter one room", () => container.Filter(new FilterCriteria { SearchString = "1" })); AddStep("filter one room", () => container.Filter.Value = new FilterCriteria { SearchString = "1" });
AddUntilStep("1 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 1); AddUntilStep("1 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 1);
AddStep("remove filter", () => container.Filter(null)); AddStep("remove filter", () => container.Filter.Value = null);
AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4); AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4);
} }
@ -131,13 +131,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("add rooms", () => RoomManager.AddRooms(3, new CatchRuleset().RulesetInfo)); AddStep("add rooms", () => RoomManager.AddRooms(3, new CatchRuleset().RulesetInfo));
// Todo: What even is this case...? // Todo: What even is this case...?
AddStep("set empty filter criteria", () => container.Filter(null)); AddStep("set empty filter criteria", () => container.Filter.Value = null);
AddUntilStep("5 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 5); AddUntilStep("5 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 5);
AddStep("filter osu! rooms", () => container.Filter(new FilterCriteria { Ruleset = new OsuRuleset().RulesetInfo })); AddStep("filter osu! rooms", () => container.Filter.Value = new FilterCriteria { Ruleset = new OsuRuleset().RulesetInfo });
AddUntilStep("2 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 2); AddUntilStep("2 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 2);
AddStep("filter catch rooms", () => container.Filter(new FilterCriteria { Ruleset = new CatchRuleset().RulesetInfo })); AddStep("filter catch rooms", () => container.Filter.Value = new FilterCriteria { Ruleset = new CatchRuleset().RulesetInfo });
AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3); AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3);
} }

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

@ -6,12 +6,19 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration;
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;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
@ -20,12 +27,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Resolved] [Resolved]
private OsuGameBase game { get; set; } private OsuGameBase game { get; set; }
[Resolved]
private OsuConfigManager config { get; set; }
[Resolved] [Resolved]
private BeatmapManager beatmapManager { get; set; } private BeatmapManager beatmapManager { get; set; }
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;
@ -40,17 +50,18 @@ 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()
{ {
AddStep("start players silently", () => AddStep("start players silently", () =>
{ {
Client.CurrentMatchPlayingUserIds.Add(PLAYER_1_ID); OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_1_ID }, true);
Client.CurrentMatchPlayingUserIds.Add(PLAYER_2_ID); OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_2_ID }, true);
playingUserIds.Add(PLAYER_1_ID);
playingUserIds.Add(PLAYER_2_ID); playingUsers.Add(new MultiplayerRoomUser(PLAYER_1_ID));
playingUsers.Add(new MultiplayerRoomUser(PLAYER_2_ID));
}); });
loadSpectateScreen(false); loadSpectateScreen(false);
@ -76,6 +87,64 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddWaitStep("wait a bit", 20); AddWaitStep("wait a bit", 20);
} }
[Test]
public void TestSpectatorPlayerInteractiveElementsHidden()
{
HUDVisibilityMode originalConfigValue = default;
AddStep("get original config hud visibility", () => originalConfigValue = config.Get<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode));
AddStep("set config hud visibility to always", () => config.SetValue(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always));
start(new[] { PLAYER_1_ID, PLAYER_2_ID });
loadSpectateScreen(false);
AddUntilStep("wait for player loaders", () => this.ChildrenOfType<PlayerLoader>().Count() == 2);
AddAssert("all player loader settings hidden", () => this.ChildrenOfType<PlayerLoader>().All(l => !l.ChildrenOfType<FillFlowContainer<PlayerSettingsGroup>>().Any()));
AddUntilStep("wait for players to load", () => spectatorScreen.AllPlayersLoaded);
// components wrapped in skinnable target containers load asynchronously, potentially taking more than one frame to load.
// therefore use until step rather than direct assert to account for that.
AddUntilStep("all interactive elements removed", () => this.ChildrenOfType<Player>().All(p =>
!p.ChildrenOfType<PlayerSettingsOverlay>().Any() &&
!p.ChildrenOfType<HoldForMenuButton>().Any() &&
p.ChildrenOfType<SongProgressBar>().SingleOrDefault()?.ShowHandle == false));
AddStep("restore config hud visibility", () => config.SetValue(OsuSetting.HUDVisibilityMode, originalConfigValue));
}
[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()
{ {
@ -252,7 +321,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));
@ -264,9 +333,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
foreach (int id in userIds) foreach (int id in userIds)
{ {
Client.CurrentMatchPlayingUserIds.Add(id); 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;
@ -44,6 +44,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
private TestMultiplayer multiplayerScreen; private TestMultiplayer multiplayerScreen;
private TestMultiplayerClient client; private TestMultiplayerClient client;
private TestRequestHandlingMultiplayerRoomManager roomManager => multiplayerScreen.RoomManager;
[Cached(typeof(UserLookupCache))] [Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache(); private UserLookupCache lookupCache = new TestUserLookupCache();
@ -68,7 +70,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("load dependencies", () => AddStep("load dependencies", () =>
{ {
client = new TestMultiplayerClient(multiplayerScreen.RoomManager); client = new TestMultiplayerClient(roomManager);
// The screen gets suspended so it stops receiving updates. // The screen gets suspended so it stops receiving updates.
Child = client; Child = client;
@ -87,6 +89,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]
@ -131,39 +134,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestExitMidJoin() public void TestExitMidJoin()
{ {
Room room = null;
AddStep("create room", () => AddStep("create room", () =>
{ {
room = new Room roomManager.AddServerSideRoom(new Room
{
Name = { Value = "Test Room" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
};
});
AddStep("refresh rooms", () => multiplayerScreen.RoomManager.Filter.Value = new FilterCriteria());
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room and immediately exit", () =>
{
multiplayerScreen.ChildrenOfType<LoungeSubScreen>().Single().Open(room);
Schedule(() => Stack.CurrentScreen.Exit());
});
}
[Test]
public void TestJoinRoomWithoutPassword()
{
AddStep("create room", () =>
{
multiplayerScreen.RoomManager.AddRoom(new Room
{ {
Name = { Value = "Test Room" }, Name = { Value = "Test Room" },
Playlist = Playlist =
@ -177,7 +150,39 @@ namespace osu.Game.Tests.Visual.Multiplayer
}); });
}); });
AddStep("refresh rooms", () => multiplayerScreen.RoomManager.Filter.Value = new FilterCriteria()); AddStep("refresh rooms", () => this.ChildrenOfType<LoungeSubScreen>().Single().UpdateFilter());
AddUntilStep("wait for room", () => this.ChildrenOfType<DrawableRoom>().Any());
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room and immediately exit select", () =>
{
InputManager.Key(Key.Enter);
Schedule(() => Stack.CurrentScreen.Exit());
});
}
[Test]
public void TestJoinRoomWithoutPassword()
{
AddStep("create room", () =>
{
roomManager.AddServerSideRoom(new Room
{
Name = { Value = "Test Room" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
});
AddStep("refresh rooms", () => this.ChildrenOfType<LoungeSubScreen>().Single().UpdateFilter());
AddUntilStep("wait for room", () => this.ChildrenOfType<DrawableRoom>().Any());
AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room", () => InputManager.Key(Key.Enter)); AddStep("join room", () => InputManager.Key(Key.Enter));
@ -210,7 +215,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
AddStep("create room", () => AddStep("create room", () =>
{ {
multiplayerScreen.RoomManager.AddRoom(new Room roomManager.AddServerSideRoom(new Room
{ {
Name = { Value = "Test Room" }, Name = { Value = "Test Room" },
Password = { Value = "password" }, Password = { Value = "password" },
@ -225,7 +230,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
}); });
}); });
AddStep("refresh rooms", () => multiplayerScreen.RoomManager.Filter.Value = new FilterCriteria()); AddStep("refresh rooms", () => this.ChildrenOfType<LoungeSubScreen>().Single().UpdateFilter());
AddUntilStep("wait for room", () => this.ChildrenOfType<DrawableRoom>().Any());
AddStep("select room", () => InputManager.Key(Key.Down)); AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room", () => InputManager.Key(Key.Enter)); AddStep("join room", () => InputManager.Key(Key.Enter));
@ -312,6 +319,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 +357,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 +407,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 +415,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 +432,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;
@ -20,6 +21,7 @@ using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual.OnlinePlay; using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Tests.Visual.Spectator; using osu.Game.Tests.Visual.Spectator;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
@ -50,22 +52,23 @@ 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);
multiplayerUsers.Add(OnlinePlayDependencies.Client.AddUser(new User { Id = user }, true));
// Todo: This is REALLY bad. }
Client.CurrentMatchPlayingUserIds.AddRange(users);
Children = new Drawable[] Children = new Drawable[]
{ {
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

@ -1,157 +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.Testing;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual.OnlinePlay;
namespace osu.Game.Tests.Visual.Multiplayer
{
[HeadlessTest]
public class TestSceneMultiplayerRoomManager : MultiplayerTestScene
{
protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies();
public TestSceneMultiplayerRoomManager()
: base(false)
{
}
[Test]
public void TestPollsInitially()
{
AddStep("create room manager with a few rooms", () =>
{
RoomManager.CreateRoom(createRoom(r => r.Name.Value = "1"));
RoomManager.PartRoom();
RoomManager.CreateRoom(createRoom(r => r.Name.Value = "2"));
RoomManager.PartRoom();
RoomManager.ClearRooms();
});
AddAssert("manager polled for rooms", () => ((RoomManager)RoomManager).Rooms.Count == 2);
AddAssert("initial rooms received", () => RoomManager.InitialRoomsReceived.Value);
}
[Test]
public void TestRoomsClearedOnDisconnection()
{
AddStep("create room manager with a few rooms", () =>
{
RoomManager.CreateRoom(createRoom());
RoomManager.PartRoom();
RoomManager.CreateRoom(createRoom());
RoomManager.PartRoom();
});
AddStep("disconnect", () => Client.Disconnect());
AddAssert("rooms cleared", () => ((RoomManager)RoomManager).Rooms.Count == 0);
AddAssert("initial rooms not received", () => !RoomManager.InitialRoomsReceived.Value);
}
[Test]
public void TestRoomsPolledOnReconnect()
{
AddStep("create room manager with a few rooms", () =>
{
RoomManager.CreateRoom(createRoom());
RoomManager.PartRoom();
RoomManager.CreateRoom(createRoom());
RoomManager.PartRoom();
});
AddStep("disconnect", () => Client.Disconnect());
AddStep("connect", () => Client.Connect());
AddAssert("manager polled for rooms", () => ((RoomManager)RoomManager).Rooms.Count == 2);
AddAssert("initial rooms received", () => RoomManager.InitialRoomsReceived.Value);
}
[Test]
public void TestRoomsNotPolledWhenJoined()
{
AddStep("create room manager with a room", () =>
{
RoomManager.CreateRoom(createRoom());
RoomManager.ClearRooms();
});
AddAssert("manager not polled for rooms", () => ((RoomManager)RoomManager).Rooms.Count == 0);
AddAssert("initial rooms not received", () => !RoomManager.InitialRoomsReceived.Value);
}
[Test]
public void TestMultiplayerRoomJoinedWhenCreated()
{
AddStep("create room manager with a room", () =>
{
RoomManager.CreateRoom(createRoom());
});
AddUntilStep("multiplayer room joined", () => Client.Room != null);
}
[Test]
public void TestMultiplayerRoomPartedWhenAPIRoomParted()
{
AddStep("create room manager with a room", () =>
{
RoomManager.CreateRoom(createRoom());
RoomManager.PartRoom();
});
AddAssert("multiplayer room parted", () => Client.Room == null);
}
[Test]
public void TestMultiplayerRoomJoinedWhenAPIRoomJoined()
{
AddStep("create room manager with a room", () =>
{
var r = createRoom();
RoomManager.CreateRoom(r);
RoomManager.PartRoom();
RoomManager.JoinRoom(r);
});
AddUntilStep("multiplayer room joined", () => Client.Room != null);
}
private Room createRoom(Action<Room> initFunc = null)
{
var room = new Room
{
Name =
{
Value = "test room"
},
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo },
Ruleset = { Value = Ruleset.Value }
}
}
};
initFunc?.Invoke(room);
return room;
}
private class TestDependencies : MultiplayerTestSceneDependencies
{
public TestDependencies()
{
// Need to set these values as early as possible.
RoomManager.TimeBetweenListingPolls.Value = 1;
RoomManager.TimeBetweenSelectionPolls.Value = 1;
}
}
}
}

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

@ -10,7 +10,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -30,10 +29,9 @@ using osu.Game.Skinning;
using osu.Game.Utils; using osu.Game.Utils;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Tests.Visual namespace osu.Game.Tests.Visual.Navigation
{ {
[TestFixture] [TestFixture]
[HeadlessTest]
public class TestSceneOsuGame : OsuTestScene public class TestSceneOsuGame : OsuTestScene
{ {
private IReadOnlyList<Type> requiredGameDependencies => new[] private IReadOnlyList<Type> requiredGameDependencies => new[]

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

@ -141,6 +141,12 @@ namespace osu.Game.Tests.Visual.Playlists
public IBindableList<Room> Rooms => null; public IBindableList<Room> Rooms => null;
public void AddOrUpdateRoom(Room room) => throw new NotImplementedException();
public void RemoveRoom(Room room) => throw new NotImplementedException();
public void ClearRooms() => throw new NotImplementedException();
public void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null) public void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
{ {
if (CreateRequested == null) if (CreateRequested == null)

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,6 +1,8 @@
// 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 NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -68,13 +70,40 @@ namespace osu.Game.Tests.Visual.UserInterface
); );
} }
private class MyContextMenuContainer : Container, IHasContextMenu private static MenuItem[] makeMenu()
{ {
public MenuItem[] ContextMenuItems => new MenuItem[] return new MenuItem[]
{ {
new OsuMenuItem(@"Some option"), new OsuMenuItem(@"Some option"),
new OsuMenuItem(@"Highlighted option", MenuItemType.Highlighted), new OsuMenuItem(@"Highlighted option", MenuItemType.Highlighted),
new OsuMenuItem(@"Another option"), new OsuMenuItem(@"Another option"),
new OsuMenuItem(@"Nested option >")
{
Items = new MenuItem[]
{
new OsuMenuItem(@"Sub-One"),
new OsuMenuItem(@"Sub-Two"),
new OsuMenuItem(@"Sub-Three"),
new OsuMenuItem(@"Sub-Nested option >")
{
Items = new MenuItem[]
{
new OsuMenuItem(@"Double Sub-One"),
new OsuMenuItem(@"Double Sub-Two"),
new OsuMenuItem(@"Double Sub-Three"),
new OsuMenuItem(@"Sub-Sub-Nested option >")
{
Items = new MenuItem[]
{
new OsuMenuItem(@"Too Deep One"),
new OsuMenuItem(@"Too Deep Two"),
new OsuMenuItem(@"Too Deep Three"),
}
}
}
}
}
},
new OsuMenuItem(@"Choose me please"), new OsuMenuItem(@"Choose me please"),
new OsuMenuItem(@"And me too"), new OsuMenuItem(@"And me too"),
new OsuMenuItem(@"Trying to fill"), new OsuMenuItem(@"Trying to fill"),
@ -82,17 +111,29 @@ namespace osu.Game.Tests.Visual.UserInterface
}; };
} }
private class MyContextMenuContainer : Container, IHasContextMenu
{
public MenuItem[] ContextMenuItems => makeMenu();
}
private class AnotherContextMenuContainer : Container, IHasContextMenu private class AnotherContextMenuContainer : Container, IHasContextMenu
{ {
public MenuItem[] ContextMenuItems => new MenuItem[] public MenuItem[] ContextMenuItems
{ {
new OsuMenuItem(@"Simple option"), get
new OsuMenuItem(@"Simple very very long option"), {
new OsuMenuItem(@"Change width", MenuItemType.Highlighted, () => this.ResizeWidthTo(Width * 2, 100, Easing.OutQuint)), List<MenuItem> items = makeMenu().ToList();
new OsuMenuItem(@"Change height", MenuItemType.Highlighted, () => this.ResizeHeightTo(Height * 2, 100, Easing.OutQuint)), items.AddRange(new MenuItem[]
new OsuMenuItem(@"Change width back", MenuItemType.Destructive, () => this.ResizeWidthTo(Width / 2, 100, Easing.OutQuint)), {
new OsuMenuItem(@"Change height back", MenuItemType.Destructive, () => this.ResizeHeightTo(Height / 2, 100, Easing.OutQuint)), new OsuMenuItem(@"Change width", MenuItemType.Highlighted, () => this.ResizeWidthTo(Width * 2, 100, Easing.OutQuint)),
}; new OsuMenuItem(@"Change height", MenuItemType.Highlighted, () => this.ResizeHeightTo(Height * 2, 100, Easing.OutQuint)),
new OsuMenuItem(@"Change width back", MenuItemType.Destructive, () => this.ResizeWidthTo(Width / 2, 100, Easing.OutQuint)),
new OsuMenuItem(@"Change height back", MenuItemType.Destructive, () => this.ResizeHeightTo(Height / 2, 100, Easing.OutQuint)),
});
return items.ToArray();
}
}
} }
} }
} }

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

@ -13,8 +13,16 @@ namespace osu.Game.Database
public interface IModelManager<TModel> public interface IModelManager<TModel>
where TModel : class where TModel : class
{ {
/// <summary>
/// A bindable which contains a weak reference to the last item that was updated.
/// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
/// </summary>
IBindable<WeakReference<TModel>> ItemUpdated { get; } IBindable<WeakReference<TModel>> ItemUpdated { get; }
/// <summary>
/// A bindable which contains a weak reference to the last item that was removed.
/// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
/// </summary>
IBindable<WeakReference<TModel>> ItemRemoved { get; } IBindable<WeakReference<TModel>> ItemRemoved { get; }
} }
} }

View File

@ -36,6 +36,11 @@ namespace osu.Game.Database
/// </summary> /// </summary>
public IQueryable<T> ConsumableItems => AddIncludesForConsumption(ContextFactory.Get().Set<T>()); public IQueryable<T> ConsumableItems => AddIncludesForConsumption(ContextFactory.Get().Set<T>());
/// <summary>
/// Access barebones items with no includes.
/// </summary>
public IQueryable<T> Items => ContextFactory.Get().Set<T>();
/// <summary> /// <summary>
/// Add a <typeparamref name="T"/> to the database. /// Add a <typeparamref name="T"/> to the database.
/// </summary> /// </summary>

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.Allocation;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -9,6 +10,14 @@ namespace osu.Game.Graphics.Cursor
{ {
public class OsuContextMenuContainer : ContextMenuContainer public class OsuContextMenuContainer : ContextMenuContainer
{ {
protected override Menu CreateMenu() => new OsuContextMenu(); [Cached]
private OsuContextMenuSamples samples = new OsuContextMenuSamples();
public OsuContextMenuContainer()
{
AddInternal(samples);
}
protected override Menu CreateMenu() => new OsuContextMenu(true);
} }
} }

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

@ -27,7 +27,7 @@ namespace osu.Game.Graphics.UserInterface
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Children = new Drawable[] AddRange(new Drawable[]
{ {
Background = new Box Background = new Box
{ {
@ -42,7 +42,7 @@ namespace osu.Game.Graphics.UserInterface
Size = new Vector2(13), Size = new Vector2(13),
Icon = icon, Icon = icon,
}, },
}; });
} }
} }
} }

View File

@ -21,6 +21,9 @@ namespace osu.Game.Graphics.UserInterface
private const float border_width = 3; private const float border_width = 3;
private const double animate_in_duration = 150;
private const double animate_out_duration = 500;
public Nub() public Nub()
{ {
Box fill; Box fill;
@ -77,20 +80,26 @@ namespace osu.Game.Graphics.UserInterface
if (value) if (value)
{ {
this.FadeColour(GlowingAccentColour, 500, Easing.OutQuint); this.FadeColour(GlowingAccentColour, animate_in_duration, Easing.OutQuint);
FadeEdgeEffectTo(1, 500, Easing.OutQuint); FadeEdgeEffectTo(1, animate_in_duration, Easing.OutQuint);
} }
else else
{ {
FadeEdgeEffectTo(0, 500); FadeEdgeEffectTo(0, animate_out_duration);
this.FadeColour(AccentColour, 500); this.FadeColour(AccentColour, animate_out_duration);
} }
} }
} }
public bool Expanded public bool Expanded
{ {
set => this.ResizeTo(new Vector2(value ? EXPANDED_SIZE : COLLAPSED_SIZE, 12), 500, Easing.OutQuint); set
{
if (value)
this.ResizeTo(new Vector2(EXPANDED_SIZE, 12), animate_in_duration, Easing.OutQuint);
else
this.ResizeTo(new Vector2(COLLAPSED_SIZE, 12), animate_out_duration, Easing.OutQuint);
}
} }
private readonly Bindable<bool> current = new Bindable<bool>(); private readonly Bindable<bool> current = new Bindable<bool>();

View File

@ -3,6 +3,7 @@
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
@ -14,7 +15,14 @@ namespace osu.Game.Graphics.UserInterface
{ {
private const int fade_duration = 250; private const int fade_duration = 250;
public OsuContextMenu() [Resolved]
private OsuContextMenuSamples samples { get; set; }
// todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed.
private bool wasOpened;
private readonly bool playClickSample;
public OsuContextMenu(bool playClickSample = false)
: base(Direction.Vertical) : base(Direction.Vertical)
{ {
MaskingContainer.CornerRadius = 5; MaskingContainer.CornerRadius = 5;
@ -28,16 +36,38 @@ namespace osu.Game.Graphics.UserInterface
ItemsContainer.Padding = new MarginPadding { Vertical = DrawableOsuMenuItem.MARGIN_VERTICAL }; ItemsContainer.Padding = new MarginPadding { Vertical = DrawableOsuMenuItem.MARGIN_VERTICAL };
MaxHeight = 250; MaxHeight = 250;
this.playClickSample = playClickSample;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours, AudioManager audio)
{ {
BackgroundColour = colours.ContextMenuGray; BackgroundColour = colours.ContextMenuGray;
} }
protected override void AnimateOpen() => this.FadeIn(fade_duration, Easing.OutQuint); protected override void AnimateOpen()
protected override void AnimateClose() => this.FadeOut(fade_duration, Easing.OutQuint); {
this.FadeIn(fade_duration, Easing.OutQuint);
if (playClickSample)
samples.PlayClickSample();
if (!wasOpened)
samples.PlayOpenSample();
wasOpened = true;
}
protected override void AnimateClose()
{
this.FadeOut(fade_duration, Easing.OutQuint);
if (wasOpened)
samples.PlayCloseSample();
wasOpened = false;
}
protected override Menu CreateSubMenu() => new OsuContextMenu(); protected override Menu CreateSubMenu() => new OsuContextMenu();
} }

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.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
namespace osu.Game.Graphics.UserInterface
{
public class OsuContextMenuSamples : Component
{
private Sample sampleClick;
private Sample sampleOpen;
private Sample sampleClose;
[BackgroundDependencyLoader]
private void load(OsuColour colours, AudioManager audio)
{
sampleClick = audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select");
sampleOpen = audio.Samples.Get(@"UI/dropdown-open");
sampleClose = audio.Samples.Get(@"UI/dropdown-close");
}
public void PlayClickSample() => Scheduler.AddOnce(playClickSample);
private void playClickSample() => sampleClick.Play();
public void PlayOpenSample() => Scheduler.AddOnce(playOpenSample);
private void playOpenSample() => sampleOpen.Play();
public void PlayCloseSample() => Scheduler.AddOnce(playCloseSample);
private void playCloseSample() => sampleClose.Play();
}
}

View File

@ -288,7 +288,7 @@ namespace osu.Game.Graphics.UserInterface
}, },
}; };
AddInternal(new HoverSounds()); AddInternal(new HoverClickSounds());
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]

View File

@ -1,6 +1,9 @@
// 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.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -13,6 +16,12 @@ namespace osu.Game.Graphics.UserInterface
{ {
public class OsuMenu : Menu public class OsuMenu : Menu
{ {
private Sample sampleOpen;
private Sample sampleClose;
// todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed.
private bool wasOpened;
public OsuMenu(Direction direction, bool topLevelMenu = false) public OsuMenu(Direction direction, bool topLevelMenu = false)
: base(direction, topLevelMenu) : base(direction, topLevelMenu)
{ {
@ -22,8 +31,30 @@ namespace osu.Game.Graphics.UserInterface
ItemsContainer.Padding = new MarginPadding(5); ItemsContainer.Padding = new MarginPadding(5);
} }
protected override void AnimateOpen() => this.FadeIn(300, Easing.OutQuint); [BackgroundDependencyLoader]
protected override void AnimateClose() => this.FadeOut(300, Easing.OutQuint); private void load(AudioManager audio)
{
sampleOpen = audio.Samples.Get(@"UI/dropdown-open");
sampleClose = audio.Samples.Get(@"UI/dropdown-close");
}
protected override void AnimateOpen()
{
if (!TopLevelMenu && !wasOpened)
sampleOpen?.Play();
this.FadeIn(300, Easing.OutQuint);
wasOpened = true;
}
protected override void AnimateClose()
{
if (!TopLevelMenu && wasOpened)
sampleClose?.Play();
this.FadeOut(300, Easing.OutQuint);
wasOpened = false;
}
protected override void UpdateSize(Vector2 newSize) protected override void UpdateSize(Vector2 newSize)
{ {

View File

@ -7,6 +7,7 @@ using osu.Framework.Configuration;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Screens.Play;
namespace osu.Game.Input namespace osu.Game.Input
{ {
@ -24,14 +25,14 @@ namespace osu.Game.Input
private IBindable<bool> localUserPlaying; private IBindable<bool> localUserPlaying;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuGame game, FrameworkConfigManager frameworkConfigManager, OsuConfigManager osuConfigManager) private void load(ILocalUserPlayInfo localUserInfo, FrameworkConfigManager frameworkConfigManager, OsuConfigManager osuConfigManager)
{ {
frameworkConfineMode = frameworkConfigManager.GetBindable<ConfineMouseMode>(FrameworkSetting.ConfineMouseMode); frameworkConfineMode = frameworkConfigManager.GetBindable<ConfineMouseMode>(FrameworkSetting.ConfineMouseMode);
frameworkWindowMode = frameworkConfigManager.GetBindable<WindowMode>(FrameworkSetting.WindowMode); frameworkWindowMode = frameworkConfigManager.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
frameworkWindowMode.BindValueChanged(_ => updateConfineMode()); frameworkWindowMode.BindValueChanged(_ => updateConfineMode());
osuConfineMode = osuConfigManager.GetBindable<OsuConfineMouseMode>(OsuSetting.ConfineMouseMode); osuConfineMode = osuConfigManager.GetBindable<OsuConfineMouseMode>(OsuSetting.ConfineMouseMode);
localUserPlaying = game.LocalUserPlaying.GetBoundCopy(); localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy();
osuConfineMode.ValueChanged += _ => updateConfineMode(); osuConfineMode.ValueChanged += _ => updateConfineMode();
localUserPlaying.BindValueChanged(_ => updateConfineMode(), true); localUserPlaying.BindValueChanged(_ => updateConfineMode(), true);

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}";
}
}

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