Merge branch 'update-framework' into settings-filter-fixes

This commit is contained in:
Dean Herbert
2022-04-22 00:55:15 +09:00
226 changed files with 2400 additions and 1378 deletions

View File

@ -27,10 +27,10 @@
] ]
}, },
"ppy.localisationanalyser.tools": { "ppy.localisationanalyser.tools": {
"version": "2022.320.0", "version": "2022.417.0",
"commands": [ "commands": [
"localisation" "localisation"
] ]
} }
} }
} }

View File

@ -2,6 +2,60 @@ on: [push, pull_request]
name: Continuous Integration name: Continuous Integration
jobs: jobs:
inspect-code:
name: Code Quality
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
# FIXME: Tools won't run in .NET 6.0 unless you install 3.1.x LTS side by side.
# https://itnext.io/how-to-support-multiple-net-sdks-in-github-actions-workflows-b988daa884e
- name: Install .NET 3.1.x LTS
uses: actions/setup-dotnet@v1
with:
dotnet-version: "3.1.x"
- name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1
with:
dotnet-version: "6.0.x"
- name: Restore Tools
run: dotnet tool restore
- name: Restore Packages
run: dotnet restore
- name: Restore inspectcode cache
uses: actions/cache@v3
with:
path: ${{ github.workspace }}/inspectcode
key: inspectcode-${{ hashFiles('.config/dotnet-tools.json') }}-${{ hashFiles('.github/workflows/ci.yml' ) }}
- name: CodeFileSanity
run: |
# TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
# FIXME: Suppress warnings from templates project
exit_code=0
while read -r line; do
if [[ ! -z "$line" ]]; then
echo "::error::$line"
exit_code=1
fi
done <<< $(dotnet codefilesanity)
exit $exit_code
# Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
# - name: .NET Format (Dry Run)
# run: dotnet format --dry-run --check
- name: InspectCode
run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
- name: NVika
run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors
test: test:
name: Test name: Test
runs-on: ${{matrix.os.fullname}} runs-on: ${{matrix.os.fullname}}
@ -93,58 +147,4 @@ jobs:
# cannot accept .sln(f) files as arguments. # cannot accept .sln(f) files as arguments.
# Build just the main game for now. # Build just the main game for now.
- name: Build - name: Build
run: msbuild osu.iOS/osu.iOS.csproj /restore /p:Configuration=Debug run: msbuild osu.iOS/osu.iOS.csproj /restore /p:Configuration=Debug
inspect-code:
name: Code Quality
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
# FIXME: Tools won't run in .NET 6.0 unless you install 3.1.x LTS side by side.
# https://itnext.io/how-to-support-multiple-net-sdks-in-github-actions-workflows-b988daa884e
- name: Install .NET 3.1.x LTS
uses: actions/setup-dotnet@v1
with:
dotnet-version: "3.1.x"
- name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1
with:
dotnet-version: "6.0.x"
- name: Restore Tools
run: dotnet tool restore
- name: Restore Packages
run: dotnet restore
- name: Restore inspectcode cache
uses: actions/cache@v3
with:
path: ${{ github.workspace }}/inspectcode
key: inspectcode-${{ hashFiles('.config/dotnet-tools.json') }}-${{ hashFiles('.github/workflows/ci.yml' ) }}
- name: CodeFileSanity
run: |
# TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
# FIXME: Suppress warnings from templates project
exit_code=0
while read -r line; do
if [[ ! -z "$line" ]]; then
echo "::error::$line"
exit_code=1
fi
done <<< $(dotnet codefilesanity)
exit $exit_code
# Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
# - name: .NET Format (Dry Run)
# run: dotnet format --dry-run --check
- name: InspectCode
run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
- name: NVika
run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors

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="2022.407.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.417.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.408.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.421.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

@ -19,7 +19,7 @@ namespace osu.Desktop.Security
public class ElevatedPrivilegesChecker : Component public class ElevatedPrivilegesChecker : Component
{ {
[Resolved] [Resolved]
private NotificationOverlay notifications { get; set; } private INotificationOverlay notifications { get; set; }
private bool elevated; private bool elevated;

View File

@ -25,7 +25,7 @@ namespace osu.Desktop.Updater
public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager
{ {
private UpdateManager updateManager; private UpdateManager updateManager;
private NotificationOverlay notificationOverlay; private INotificationOverlay notificationOverlay;
public Task PrepareUpdateAsync() => UpdateManager.RestartAppWhenExited(); public Task PrepareUpdateAsync() => UpdateManager.RestartAppWhenExited();
@ -39,9 +39,9 @@ namespace osu.Desktop.Updater
private readonly SquirrelLogger squirrelLogger = new SquirrelLogger(); private readonly SquirrelLogger squirrelLogger = new SquirrelLogger();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(NotificationOverlay notification) private void load(INotificationOverlay notifications)
{ {
notificationOverlay = notification; notificationOverlay = notifications;
SquirrelLocator.CurrentMutable.Register(() => squirrelLogger, typeof(ILogger)); SquirrelLocator.CurrentMutable.Register(() => squirrelLogger, typeof(ILogger));
} }

View File

@ -24,7 +24,7 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Clowd.Squirrel" Version="2.8.28-pre" /> <PackageReference Include="Clowd.Squirrel" Version="2.9.23-gc8da1a" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" /> <PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageReference Include="System.IO.Packaging" Version="6.0.0" /> <PackageReference Include="System.IO.Packaging" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />

View File

@ -0,0 +1,108 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using System.Diagnostics;
using System.Linq;
using Moq;
using NUnit.Framework;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Game.Rulesets.Osu.Skinning.Legacy;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
[HeadlessTest]
public class LegacyMainCirclePieceTest : OsuTestScene
{
private static readonly object?[][] texture_priority_cases =
{
// default priority lookup
new object?[]
{
// available textures
new[] { @"hitcircle", @"hitcircleoverlay" },
// priority lookup prefix
null,
// expected circle and overlay
@"hitcircle", @"hitcircleoverlay",
},
// custom priority lookup
new object?[]
{
new[] { @"hitcircle", @"hitcircleoverlay", @"sliderstartcircle", @"sliderstartcircleoverlay" },
@"sliderstartcircle",
@"sliderstartcircle", @"sliderstartcircleoverlay",
},
// when no sprites are available for the specified prefix, fall back to "hitcircle"/"hitcircleoverlay".
new object?[]
{
new[] { @"hitcircle", @"hitcircleoverlay" },
@"sliderstartcircle",
@"hitcircle", @"hitcircleoverlay",
},
// when a circle is available for the specified prefix but no overlay exists, no overlay is displayed.
new object?[]
{
new[] { @"hitcircle", @"hitcircleoverlay", @"sliderstartcircle" },
@"sliderstartcircle",
@"sliderstartcircle", null
},
// when no circle is available for the specified prefix but an overlay exists, the overlay is ignored.
new object?[]
{
new[] { @"hitcircle", @"hitcircleoverlay", @"sliderstartcircleoverlay" },
@"sliderstartcircle",
@"hitcircle", @"hitcircleoverlay",
}
};
[TestCaseSource(nameof(texture_priority_cases))]
public void TestTexturePriorities(string[] textureFilenames, string priorityLookup, string? expectedCircle, string? expectedOverlay)
{
TestLegacyMainCirclePiece piece = null!;
AddStep("load circle piece", () =>
{
var skin = new Mock<ISkinSource>();
// shouldn't be required as GetTexture(string) calls GetTexture(string, WrapMode, WrapMode) by default,
// but moq doesn't handle that well, therefore explicitly requiring to use `CallBase`:
// https://github.com/moq/moq4/issues/972
skin.Setup(s => s.GetTexture(It.IsAny<string>())).CallBase();
skin.Setup(s => s.GetTexture(It.IsIn(textureFilenames), It.IsAny<WrapMode>(), It.IsAny<WrapMode>()))
.Returns((string componentName, WrapMode _, WrapMode __) => new Texture(1, 1) { AssetName = componentName });
Child = new DependencyProvidingContainer
{
CachedDependencies = new (Type, object)[] { (typeof(ISkinSource), skin.Object) },
Child = piece = new TestLegacyMainCirclePiece(priorityLookup),
};
var sprites = this.ChildrenOfType<Sprite>().Where(s => s.Texture.AssetName != null).DistinctBy(s => s.Texture.AssetName).ToArray();
Debug.Assert(sprites.Length <= 2);
});
AddAssert("check circle sprite", () => piece.CircleSprite?.Texture?.AssetName == expectedCircle);
AddAssert("check overlay sprite", () => piece.OverlaySprite?.Texture?.AssetName == expectedOverlay);
}
private class TestLegacyMainCirclePiece : LegacyMainCirclePiece
{
public new Sprite? CircleSprite => base.CircleSprite.ChildrenOfType<Sprite>().DistinctBy(s => s.Texture.AssetName).SingleOrDefault();
public new Sprite? OverlaySprite => base.OverlaySprite.ChildrenOfType<Sprite>().DistinctBy(s => s.Texture.AssetName).SingleOrDefault();
public TestLegacyMainCirclePiece(string? priorityLookupPrefix)
: base(priorityLookupPrefix, false)
{
}
}
}
}

View File

@ -4,6 +4,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Beatmaps namespace osu.Game.Rulesets.Osu.Beatmaps
@ -20,13 +21,13 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{ {
new BeatmapStatistic new BeatmapStatistic
{ {
Name = @"Circle Count", Name = BeatmapsetsStrings.ShowStatsCountCircles,
Content = circles.ToString(), Content = circles.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
}, },
new BeatmapStatistic new BeatmapStatistic
{ {
Name = @"Slider Count", Name = BeatmapsetsStrings.ShowStatsCountSliders,
Content = sliders.ToString(), Content = sliders.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
}, },

View File

@ -3,10 +3,10 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -16,63 +16,61 @@ using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
#nullable enable
namespace osu.Game.Rulesets.Osu.Skinning.Legacy namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
public class LegacyMainCirclePiece : CompositeDrawable public class LegacyMainCirclePiece : CompositeDrawable
{ {
public override bool RemoveCompletedTransforms => false; public override bool RemoveCompletedTransforms => false;
private readonly string priorityLookup; /// <summary>
/// A prioritised prefix to perform texture lookups with.
/// </summary>
private readonly string? priorityLookupPrefix;
private readonly bool hasNumber; private readonly bool hasNumber;
public LegacyMainCirclePiece(string priorityLookup = null, bool hasNumber = true) protected Drawable CircleSprite = null!;
protected Drawable OverlaySprite = null!;
protected Container OverlayLayer { get; private set; } = null!;
private SkinnableSpriteText hitCircleText = null!;
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
[Resolved(canBeNull: true)]
private DrawableHitObject? drawableObject { get; set; }
[Resolved]
private ISkinSource skin { get; set; } = null!;
public LegacyMainCirclePiece(string? priorityLookupPrefix = null, bool hasNumber = true)
{ {
this.priorityLookup = priorityLookup; this.priorityLookupPrefix = priorityLookupPrefix;
this.hasNumber = hasNumber; this.hasNumber = hasNumber;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
} }
private Drawable hitCircleSprite;
protected Container OverlayLayer { get; private set; }
private Drawable hitCircleOverlay;
private SkinnableSpriteText hitCircleText;
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
[Resolved]
private DrawableHitObject drawableObject { get; set; }
[Resolved]
private ISkinSource skin { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
var drawableOsuObject = (DrawableOsuHitObject)drawableObject; var drawableOsuObject = (DrawableOsuHitObject?)drawableObject;
bool allowFallback = false; // if a base texture for the specified prefix exists, continue using it for subsequent lookups.
// otherwise fall back to the default prefix "hitcircle".
// attempt lookup using priority specification string circleName = (priorityLookupPrefix != null && skin.GetTexture(priorityLookupPrefix) != null) ? priorityLookupPrefix : @"hitcircle";
Texture baseTexture = getTextureWithFallback(string.Empty);
// if the base texture was not found without a fallback, switch on fallback mode and re-perform the lookup.
if (baseTexture == null)
{
allowFallback = true;
baseTexture = getTextureWithFallback(string.Empty);
}
// at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it. // at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it.
// the flow above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist. // the conditional above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist.
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png (potentially from the default/fall-through skin). // expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png.
InternalChildren = new[] InternalChildren = new[]
{ {
hitCircleSprite = new KiaiFlashingDrawable(() => new Sprite { Texture = baseTexture }) CircleSprite = new KiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) })
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -81,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Child = hitCircleOverlay = new KiaiFlashingDrawable(() => getAnimationWithFallback(@"overlay", 1000 / 2d)) Child = OverlaySprite = new KiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d))
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -105,39 +103,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
bool overlayAboveNumber = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true; bool overlayAboveNumber = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
if (overlayAboveNumber) if (overlayAboveNumber)
OverlayLayer.ChangeChildDepth(hitCircleOverlay, float.MinValue); OverlayLayer.ChangeChildDepth(OverlaySprite, float.MinValue);
accentColour.BindTo(drawableObject.AccentColour); if (drawableOsuObject != null)
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
Texture getTextureWithFallback(string name)
{ {
Texture tex = null; accentColour.BindTo(drawableOsuObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
if (!string.IsNullOrEmpty(priorityLookup))
{
tex = skin.GetTexture($"{priorityLookup}{name}");
if (!allowFallback)
return tex;
}
return tex ?? skin.GetTexture($"hitcircle{name}");
}
Drawable getAnimationWithFallback(string name, double frameLength)
{
Drawable animation = null;
if (!string.IsNullOrEmpty(priorityLookup))
{
animation = skin.GetAnimation($"{priorityLookup}{name}", true, true, frameLength: frameLength);
if (!allowFallback)
return animation;
}
return animation ?? skin.GetAnimation($"hitcircle{name}", true, true, frameLength: frameLength);
} }
} }
@ -145,28 +116,31 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
base.LoadComplete(); base.LoadComplete();
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); accentColour.BindValueChanged(colour => CircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
if (hasNumber) if (hasNumber)
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
drawableObject.ApplyCustomUpdateState += updateStateTransforms; if (drawableObject != null)
updateStateTransforms(drawableObject, drawableObject.State.Value); {
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableObject, drawableObject.State.Value);
}
} }
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{ {
const double legacy_fade_duration = 240; const double legacy_fade_duration = 240;
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime)) using (BeginAbsoluteSequence(drawableObject.AsNonNull().HitStateUpdateTime))
{ {
switch (state) switch (state)
{ {
case ArmedState.Hit: case ArmedState.Hit:
hitCircleSprite.FadeOut(legacy_fade_duration, Easing.Out); CircleSprite.FadeOut(legacy_fade_duration, Easing.Out);
hitCircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); CircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
hitCircleOverlay.FadeOut(legacy_fade_duration, Easing.Out); OverlaySprite.FadeOut(legacy_fade_duration, Easing.Out);
hitCircleOverlay.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); OverlaySprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
if (hasNumber) if (hasNumber)
{ {

View File

@ -26,6 +26,12 @@ namespace osu.Game.Tests.Gameplay
Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage)); Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage));
} }
[SetUpSteps]
public void SetUpSteps()
{
AddStep("reset audio offset", () => localConfig.SetValue(OsuSetting.AudioOffset, 0.0));
}
[Test] [Test]
public void TestStartThenElapsedTime() public void TestStartThenElapsedTime()
{ {
@ -36,7 +42,7 @@ namespace osu.Game.Tests.Gameplay
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack(); working.LoadTrack();
Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0)); Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
}); });
AddStep("start clock", () => gameplayClockContainer.Start()); AddStep("start clock", () => gameplayClockContainer.Start());
@ -53,7 +59,7 @@ namespace osu.Game.Tests.Gameplay
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack(); working.LoadTrack();
Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0)); Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
}); });
AddStep("start clock", () => gameplayClockContainer.Start()); AddStep("start clock", () => gameplayClockContainer.Start());
@ -73,26 +79,29 @@ namespace osu.Game.Tests.Gameplay
public void TestSeekPerformsInGameplayTime( public void TestSeekPerformsInGameplayTime(
[Values(1.0, 0.5, 2.0)] double clockRate, [Values(1.0, 0.5, 2.0)] double clockRate,
[Values(0.0, 200.0, -200.0)] double userOffset, [Values(0.0, 200.0, -200.0)] double userOffset,
[Values(false, true)] bool whileStopped) [Values(false, true)] bool whileStopped,
[Values(false, true)] bool setAudioOffsetBeforeConstruction)
{ {
ClockBackedTestWorkingBeatmap working = null; ClockBackedTestWorkingBeatmap working = null;
GameplayClockContainer gameplayClockContainer = null; GameplayClockContainer gameplayClockContainer = null;
if (setAudioOffsetBeforeConstruction)
AddStep($"preset audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
AddStep("create container", () => AddStep("create container", () =>
{ {
working = new ClockBackedTestWorkingBeatmap(new OsuRuleset().RulesetInfo, new FramedClock(new ManualClock()), Audio); working = new ClockBackedTestWorkingBeatmap(new OsuRuleset().RulesetInfo, new FramedClock(new ManualClock()), Audio);
working.LoadTrack(); working.LoadTrack();
Add(gameplayClockContainer = new MasterGameplayClockContainer(working, 0)); Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
if (whileStopped) gameplayClockContainer.Reset(startClock: !whileStopped);
gameplayClockContainer.Stop();
gameplayClockContainer.Reset();
}); });
AddStep($"set clock rate to {clockRate}", () => working.Track.AddAdjustment(AdjustableProperty.Frequency, new BindableDouble(clockRate))); AddStep($"set clock rate to {clockRate}", () => working.Track.AddAdjustment(AdjustableProperty.Frequency, new BindableDouble(clockRate)));
AddStep($"set audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
if (!setAudioOffsetBeforeConstruction)
AddStep($"set audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
AddStep("seek to 2500", () => gameplayClockContainer.Seek(2500)); AddStep("seek to 2500", () => gameplayClockContainer.Seek(2500));
AddAssert("gameplay clock time = 2500", () => Precision.AlmostEquals(gameplayClockContainer.CurrentTime, 2500, 10f)); AddAssert("gameplay clock time = 2500", () => Precision.AlmostEquals(gameplayClockContainer.CurrentTime, 2500, 10f));

View File

@ -88,7 +88,7 @@ namespace osu.Game.Tests.Gameplay
[Test] [Test]
public void TestSampleHasLifetimeEndWithInitialClockTime() public void TestSampleHasLifetimeEndWithInitialClockTime()
{ {
GameplayClockContainer gameplayContainer = null; MasterGameplayClockContainer gameplayContainer = null;
DrawableStoryboardSample sample = null; DrawableStoryboardSample sample = null;
AddStep("create container", () => AddStep("create container", () =>
@ -96,8 +96,11 @@ namespace osu.Game.Tests.Gameplay
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack(); working.LoadTrack();
Add(gameplayContainer = new MasterGameplayClockContainer(working, 1000, true) const double start_time = 1000;
Add(gameplayContainer = new MasterGameplayClockContainer(working, start_time)
{ {
StartTime = start_time,
IsPaused = { Value = true }, IsPaused = { Value = true },
Child = new FrameStabilityContainer Child = new FrameStabilityContainer
{ {

View File

@ -359,9 +359,9 @@ namespace osu.Game.Tests.Visual.Background
protected override BackgroundScreen CreateBackground() => protected override BackgroundScreen CreateBackground() =>
new FadeAccessibleBackground(Beatmap.Value); new FadeAccessibleBackground(Beatmap.Value);
public override void OnEntering(IScreen last) public override void OnEntering(ScreenTransitionEvent e)
{ {
base.OnEntering(last); base.OnEntering(e);
ApplyToBackground(b => ReplacesBackground.BindTo(b.StoryboardReplacesBackground)); ApplyToBackground(b => ReplacesBackground.BindTo(b.StoryboardReplacesBackground));
} }

View File

@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Collections
}); });
Dependencies.Cache(manager); Dependencies.Cache(manager);
Dependencies.Cache(dialogOverlay); Dependencies.CacheAs<IDialogOverlay>(dialogOverlay);
} }
[SetUp] [SetUp]

View File

@ -1,44 +1,71 @@
// 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.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose;
using osu.Game.Skinning;
namespace osu.Game.Tests.Visual.Editing namespace osu.Game.Tests.Visual.Editing
{ {
[TestFixture] [TestFixture]
public class TestSceneComposeScreen : EditorClockTestScene public class TestSceneComposeScreen : EditorClockTestScene
{ {
[Cached(typeof(EditorBeatmap))] private EditorBeatmap editorBeatmap;
[Cached(typeof(IBeatSnapProvider))]
private readonly EditorBeatmap editorBeatmap =
new EditorBeatmap(new OsuBeatmap
{
BeatmapInfo =
{
Ruleset = new OsuRuleset().RulesetInfo
}
});
[Cached] [Cached]
private EditorClipboard clipboard = new EditorClipboard(); private EditorClipboard clipboard = new EditorClipboard();
protected override void LoadComplete() [SetUpSteps]
public void SetUpSteps()
{ {
base.LoadComplete(); AddStep("setup compose screen", () =>
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
Child = new ComposeScreen
{ {
State = { Value = Visibility.Visible }, var beatmap = new OsuBeatmap
}; {
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }
};
editorBeatmap = new EditorBeatmap(beatmap, new LegacyBeatmapSkin(beatmap.BeatmapInfo, null));
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[]
{
(typeof(EditorBeatmap), editorBeatmap),
(typeof(IBeatSnapProvider), editorBeatmap),
},
Child = new ComposeScreen { State = { Value = Visibility.Visible } },
};
});
AddUntilStep("wait for composer", () => this.ChildrenOfType<HitObjectComposer>().SingleOrDefault()?.IsLoaded == true);
}
/// <summary>
/// Ensures that the skin of the edited beatmap is properly wrapped in a <see cref="LegacySkinTransformer"/>.
/// </summary>
[Test]
public void TestLegacyBeatmapSkinHasTransformer()
{
AddAssert("legacy beatmap skin has transformer", () =>
{
var sources = this.ChildrenOfType<BeatmapSkinProvidingContainer>().First().AllSources;
return sources.OfType<LegacySkinTransformer>().Count(t => t.Skin == editorBeatmap.BeatmapSkin.AsNonNull().Skin) == 1;
});
} }
} }
} }

View File

@ -56,10 +56,11 @@ namespace osu.Game.Tests.Visual.Gameplay
private double lastFrequency = double.MaxValue; private double lastFrequency = double.MaxValue;
protected override void Update() protected override void UpdateAfterChildren()
{ {
base.Update(); base.UpdateAfterChildren();
// This must be done in UpdateAfterChildren to allow the gameplay clock to have updated before checking values.
double freq = Beatmap.Value.Track.AggregateFrequency.Value; double freq = Beatmap.Value.Track.AggregateFrequency.Value;
FrequencyIncreased |= freq > lastFrequency; FrequencyIncreased |= freq > lastFrequency;

View File

@ -1,12 +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.Diagnostics;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
@ -36,10 +34,10 @@ namespace osu.Game.Tests.Visual.Gameplay
BeatmapInfo = { AudioLeadIn = leadIn } BeatmapInfo = { AudioLeadIn = leadIn }
}); });
AddAssert($"first frame is {expectedStartTime}", () => AddStep("check first frame time", () =>
{ {
Debug.Assert(player.FirstFrameClockTime != null); Assert.That(player.FirstFrameClockTime, Is.Not.Null);
return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms); Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms));
}); });
} }
@ -59,10 +57,10 @@ namespace osu.Game.Tests.Visual.Gameplay
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard); loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard);
AddAssert($"first frame is {expectedStartTime}", () => AddStep("check first frame time", () =>
{ {
Debug.Assert(player.FirstFrameClockTime != null); Assert.That(player.FirstFrameClockTime, Is.Not.Null);
return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms); Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms));
}); });
} }
@ -97,10 +95,10 @@ namespace osu.Game.Tests.Visual.Gameplay
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard); loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard);
AddAssert($"first frame is {expectedStartTime}", () => AddStep("check first frame time", () =>
{ {
Debug.Assert(player.FirstFrameClockTime != null); Assert.That(player.FirstFrameClockTime, Is.Not.Null);
return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms); Assert.That(player.FirstFrameClockTime.Value, Is.EqualTo(expectedStartTime).Within(lenience_ms));
}); });
} }

View File

@ -389,9 +389,9 @@ namespace osu.Game.Tests.Visual.Gameplay
public void ExitViaQuickExit() => PerformExit(false); public void ExitViaQuickExit() => PerformExit(false);
public override void OnEntering(IScreen last) public override void OnEntering(ScreenTransitionEvent e)
{ {
base.OnEntering(last); base.OnEntering(e);
GameplayClockContainer.Stop(); GameplayClockContainer.Stop();
} }
} }

View File

@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved] [Resolved]
private SessionStatics sessionStatics { get; set; } private SessionStatics sessionStatics { get; set; }
[Cached] [Cached(typeof(INotificationOverlay))]
private readonly NotificationOverlay notificationOverlay; private readonly NotificationOverlay notificationOverlay;
[Cached] [Cached]

View File

@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Menus
private IntroScreen intro; private IntroScreen intro;
[Cached] [Cached(typeof(INotificationOverlay))]
private NotificationOverlay notifications; private NotificationOverlay notifications;
private ScheduledDelegate trackResetDelegate; private ScheduledDelegate trackResetDelegate;

View File

@ -2,6 +2,7 @@
// 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.Linq; using System.Linq;
using Moq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -22,6 +23,17 @@ namespace osu.Game.Tests.Visual.Menus
[Resolved] [Resolved]
private IRulesetStore rulesets { get; set; } private IRulesetStore rulesets { get; set; }
private readonly Mock<INotificationOverlay> notifications = new Mock<INotificationOverlay>();
private readonly BindableInt unreadNotificationCount = new BindableInt();
[BackgroundDependencyLoader]
private void load()
{
Dependencies.CacheAs(notifications.Object);
notifications.SetupGet(n => n.UnreadCount).Returns(unreadNotificationCount);
}
[SetUp] [SetUp]
public void SetUp() => Schedule(() => public void SetUp() => Schedule(() =>
{ {
@ -31,10 +43,6 @@ namespace osu.Game.Tests.Visual.Menus
[Test] [Test]
public void TestNotificationCounter() public void TestNotificationCounter()
{ {
ToolbarNotificationButton notificationButton = null;
AddStep("retrieve notification button", () => notificationButton = toolbar.ChildrenOfType<ToolbarNotificationButton>().Single());
setNotifications(1); setNotifications(1);
setNotifications(2); setNotifications(2);
setNotifications(3); setNotifications(3);
@ -43,7 +51,7 @@ namespace osu.Game.Tests.Visual.Menus
void setNotifications(int count) void setNotifications(int count)
=> AddStep($"set notification count to {count}", => AddStep($"set notification count to {count}",
() => notificationButton.NotificationCount.Value = count); () => unreadNotificationCount.Value = count);
} }
[TestCase(false)] [TestCase(false)]

View File

@ -0,0 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Overlays.Mods;
using osu.Game.Screens.OnlinePlay;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneFreeModSelectScreen : MultiplayerTestScene
{
[Test]
public void TestFreeModSelect()
{
FreeModSelectScreen freeModSelectScreen = null;
AddStep("create free mod select screen", () => Child = freeModSelectScreen = new FreeModSelectScreen
{
State = { Value = Visibility.Visible }
});
AddAssert("all visible mods are playable",
() => this.ChildrenOfType<ModPanel>()
.Where(panel => panel.IsPresent)
.All(panel => panel.Mod.HasImplementation && panel.Mod.UserPlayable));
AddToggleStep("toggle visibility", visible =>
{
if (freeModSelectScreen != null)
freeModSelectScreen.State.Value = visible ? Visibility.Visible : Visibility.Hidden;
});
}
}
}

View File

@ -3,80 +3,155 @@
using System; using System;
using System.Linq; using System.Linq;
using Moq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Platform; using osu.Framework.Logging;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.Countdown; using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets; using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Tests.Resources;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
public class TestSceneMatchStartControl : MultiplayerTestScene public class TestSceneMatchStartControl : OsuManualInputManagerTestScene
{ {
private readonly Mock<MultiplayerClient> multiplayerClient = new Mock<MultiplayerClient>();
private readonly Mock<OnlinePlayBeatmapAvailabilityTracker> availabilityTracker = new Mock<OnlinePlayBeatmapAvailabilityTracker>();
private readonly Bindable<BeatmapAvailability> beatmapAvailability = new Bindable<BeatmapAvailability>();
private readonly Bindable<Room> room = new Bindable<Room>();
private MultiplayerRoom multiplayerRoom;
private MultiplayerRoomUser localUser;
private OngoingOperationTracker ongoingOperationTracker;
private PopoverContainer content;
private MatchStartControl control; private MatchStartControl control;
private BeatmapSetInfo importedSet;
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>(); private OsuButton readyButton => control.ChildrenOfType<OsuButton>().Single();
private BeatmapManager beatmaps; protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
private RulesetStore rulesets; new CachedModelDependencyContainer<Room>(base.CreateChildDependencies(parent)) { Model = { BindTarget = room } };
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio) private void load()
{ {
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.CacheAs(multiplayerClient.Object);
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default)); Dependencies.CacheAs(ongoingOperationTracker = new OngoingOperationTracker());
Dependencies.Cache(Realm); Dependencies.CacheAs(availabilityTracker.Object);
availabilityTracker.SetupGet(a => a.Availability).Returns(beatmapAvailability);
multiplayerClient.SetupGet(m => m.LocalUser).Returns(() => localUser);
multiplayerClient.SetupGet(m => m.Room).Returns(() => multiplayerRoom);
// By default, the local user is to be the host.
multiplayerClient.SetupGet(m => m.IsHost).Returns(() => ReferenceEquals(multiplayerRoom.Host, localUser));
// Assume all state changes are accepted by the server.
multiplayerClient.Setup(m => m.ChangeState(It.IsAny<MultiplayerUserState>()))
.Callback((MultiplayerUserState r) =>
{
Logger.Log($"Changing local user state from {localUser.State} to {r}");
localUser.State = r;
raiseRoomUpdated();
});
multiplayerClient.Setup(m => m.StartMatch())
.Callback(() =>
{
multiplayerClient.Raise(m => m.LoadRequested -= null);
// immediately "end" gameplay, as we don't care about that part of the process.
changeUserState(localUser.UserID, MultiplayerUserState.Idle);
});
multiplayerClient.Setup(m => m.SendMatchRequest(It.IsAny<MatchUserRequest>()))
.Callback((MatchUserRequest request) =>
{
switch (request)
{
case StartMatchCountdownRequest countdownStart:
setRoomCountdown(countdownStart.Duration);
break;
case StopCountdownRequest _:
multiplayerRoom.Countdown = null;
raiseRoomUpdated();
break;
}
});
Children = new Drawable[]
{
ongoingOperationTracker,
content = new PopoverContainer { RelativeSizeAxes = Axes.Both }
};
} }
[SetUp] [SetUpSteps]
public new void Setup() => Schedule(() => public void SetUpSteps()
{ {
AvailabilityTracker.SelectedItem.BindTo(selectedItem); AddStep("reset state", () =>
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo)
{ {
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID multiplayerClient.Invocations.Clear();
};
Child = new PopoverContainer beatmapAvailability.Value = BeatmapAvailability.LocallyAvailable();
var playlistItem = new PlaylistItem(Beatmap.Value.BeatmapInfo)
{
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
};
room.Value = new Room
{
Playlist = { playlistItem },
CurrentPlaylistItem = { Value = playlistItem }
};
localUser = new MultiplayerRoomUser(API.LocalUser.Value.Id) { User = API.LocalUser.Value };
multiplayerRoom = new MultiplayerRoom(0)
{
Playlist =
{
new MultiplayerPlaylistItem(playlistItem),
},
Users = { localUser },
Host = localUser,
};
});
AddStep("create control", () =>
{ {
RelativeSizeAxes = Axes.Both, content.Child = control = new MatchStartControl
Child = control = new MatchStartControl
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(250, 50), Size = new Vector2(250, 50),
} };
}; });
}); }
[Test] [Test]
public void TestStartWithCountdown() public void TestStartWithCountdown()
{ {
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true); AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerCountdownButton>(); ClickButtonWhenEnabled<MultiplayerCountdownButton>();
AddStep("click the first countdown button", () => AddStep("click the first countdown button", () =>
{ {
@ -85,8 +160,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown()); AddStep("check request received", () =>
AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad); {
multiplayerClient.Verify(m => m.SendMatchRequest(It.Is<StartMatchCountdownRequest>(req =>
req.Duration == TimeSpan.FromSeconds(10)
)), Times.Once);
});
} }
[Test] [Test]
@ -94,6 +173,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true); AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerCountdownButton>(); ClickButtonWhenEnabled<MultiplayerCountdownButton>();
AddStep("click the first countdown button", () => AddStep("click the first countdown button", () =>
{ {
@ -102,6 +182,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddStep("check request received", () =>
{
multiplayerClient.Verify(m => m.SendMatchRequest(It.Is<StartMatchCountdownRequest>(req =>
req.Duration == TimeSpan.FromSeconds(10)
)), Times.Once);
});
ClickButtonWhenEnabled<MultiplayerCountdownButton>(); ClickButtonWhenEnabled<MultiplayerCountdownButton>();
AddStep("click the cancel button", () => AddStep("click the cancel button", () =>
{ {
@ -110,41 +197,39 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown()); AddStep("check request received", () =>
AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); {
multiplayerClient.Verify(m => m.SendMatchRequest(It.IsAny<StopCountdownRequest>()), Times.Once);
});
} }
[Test] [Test]
public void TestReadyAndUnReadyDuringCountdown() public void TestReadyAndUnReadyDuringCountdown()
{ {
AddStep("add second user as host", () => AddStep("add second user as host", () => addUser(new APIUser { Id = 2, Username = "Another user" }, true));
{
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
MultiplayerClient.TransferHost(2);
});
AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(2) }).WaitSafely()); AddStep("start countdown", () => setRoomCountdown(TimeSpan.FromMinutes(1)));
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); checkLocalUserState(MultiplayerUserState.Idle);
} }
[Test] [Test]
public void TestCountdownWhileSpectating() public void TestCountdownWhileSpectating()
{ {
AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating)); AddStep("set spectating", () => changeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating); checkLocalUserState(MultiplayerUserState.Spectating);
AddAssert("countdown button is visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent); AddAssert("countdown button is visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value); AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" })); AddStep("add second user", () => addUser(new APIUser { Id = 2, Username = "Another user" }));
AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value); AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready)); AddStep("set second user ready", () => changeUserState(2, MultiplayerUserState.Ready));
AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value); AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
} }
@ -153,60 +238,54 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
AddStep("add second user as host", () => AddStep("add second user as host", () =>
{ {
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); addUser(new APIUser { Id = 2, Username = "Another user" }, true);
MultiplayerClient.TransferHost(2);
}); });
AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(1) }).WaitSafely()); AddStep("start countdown", () => multiplayerClient.Object.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(1) }).WaitSafely());
AddUntilStep("countdown started", () => MultiplayerClient.Room?.Countdown != null); AddUntilStep("countdown started", () => multiplayerRoom.Countdown != null);
AddStep("transfer host to local user", () => MultiplayerClient.TransferHost(API.LocalUser.Value.OnlineID)); AddStep("transfer host to local user", () => transferHost(localUser));
AddUntilStep("local user is host", () => MultiplayerClient.Room?.Host?.Equals(MultiplayerClient.LocalUser) == true); AddUntilStep("local user is host", () => multiplayerRoom.Host?.Equals(multiplayerClient.Object.LocalUser) == true);
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
AddAssert("countdown still active", () => MultiplayerClient.Room?.Countdown != null); AddAssert("countdown still active", () => multiplayerRoom.Countdown != null);
} }
[Test] [Test]
public void TestCountdownButtonVisibilityWithAutoStartEnablement() public void TestCountdownButtonVisibilityWithAutoStart()
{ {
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
AddUntilStep("countdown button visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent); AddUntilStep("countdown button visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
AddStep("enable auto start", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }).WaitSafely()); AddStep("enable auto start", () => changeRoomSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }));
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
AddUntilStep("countdown button not visible", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent); AddUntilStep("countdown button not visible", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
} }
[Test] [Test]
public void TestClickingReadyButtonUnReadiesDuringAutoStart() public void TestClickingReadyButtonUnReadiesDuringAutoStart()
{ {
AddStep("enable auto start", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }).WaitSafely()); AddStep("enable auto start", () => changeRoomSettings(new MultiplayerRoomSettings { AutoStartDuration = TimeSpan.FromMinutes(1) }));
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle); checkLocalUserState(MultiplayerUserState.Idle);
} }
[Test] [Test]
public void TestDeletedBeatmapDisableReady() public void TestDeletedBeatmapDisableReady()
{ {
OsuButton readyButton = null; AddUntilStep("ready button enabled", () => readyButton.Enabled.Value);
AddUntilStep("ensure ready button enabled", () => AddStep("mark beatmap not available", () => beatmapAvailability.Value = BeatmapAvailability.NotDownloaded());
{
readyButton = control.ChildrenOfType<OsuButton>().Single();
return readyButton.Enabled.Value;
});
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
AddUntilStep("ready button disabled", () => !readyButton.Enabled.Value); AddUntilStep("ready button disabled", () => !readyButton.Enabled.Value);
AddStep("undelete beatmap", () => beatmaps.Undelete(importedSet));
AddStep("mark beatmap available", () => beatmapAvailability.Value = BeatmapAvailability.LocallyAvailable());
AddUntilStep("ready button enabled back", () => readyButton.Enabled.Value); AddUntilStep("ready button enabled back", () => readyButton.Enabled.Value);
} }
@ -215,31 +294,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
AddStep("add second user as host", () => AddStep("add second user as host", () =>
{ {
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); addUser(new APIUser { Id = 2, Username = "Another user" }, true);
MultiplayerClient.TransferHost(2);
}); });
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); checkLocalUserState(MultiplayerUserState.Idle);
} }
[TestCase(true)] [TestCase(true)]
[TestCase(false)] [TestCase(false)]
public void TestToggleStateWhenHost(bool allReady) public void TestToggleStateWhenHost(bool allReady)
{ {
AddStep("setup", () => if (!allReady)
{ AddStep("add other user", () => addUser(new APIUser { Id = 2, Username = "Another user" }));
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
if (!allReady)
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
});
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
verifyGameplayStartFlow(); verifyGameplayStartFlow();
} }
@ -249,12 +322,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
AddStep("add host", () => AddStep("add host", () =>
{ {
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }); addUser(new APIUser { Id = 2, Username = "Another user" }, true);
MultiplayerClient.TransferHost(2);
}); });
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddStep("make user host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0));
AddStep("make local user host", () => transferHost(localUser));
verifyGameplayStartFlow(); verifyGameplayStartFlow();
} }
@ -264,18 +337,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
AddStep("setup", () => AddStep("setup", () =>
{ {
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0); addUser(new APIUser { Id = 2, Username = "Another user" });
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
}); });
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
AddStep("transfer host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[1].UserID ?? 0)); AddStep("transfer host", () => transferHost(multiplayerRoom.Users[1]));
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is idle (match not started)", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle); checkLocalUserState(MultiplayerUserState.Idle);
AddUntilStep("ready button enabled", () => control.ChildrenOfType<OsuButton>().Single().Enabled.Value); AddUntilStep("ready button enabled", () => readyButton.Enabled.Value);
} }
[TestCase(true)] [TestCase(true)]
@ -283,44 +355,83 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestManyUsersChangingState(bool isHost) public void TestManyUsersChangingState(bool isHost)
{ {
const int users = 10; const int users = 10;
AddStep("setup", () =>
{
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
for (int i = 0; i < users; i++)
MultiplayerClient.AddUser(new APIUser { Id = i, Username = "Another user" });
});
if (!isHost) AddStep("add many users", () =>
AddStep("transfer host", () => MultiplayerClient.TransferHost(2)); {
for (int i = 0; i < users; i++)
addUser(new APIUser { Id = i, Username = "Another user" }, !isHost && i == 2);
});
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddRepeatStep("change user ready state", () => AddRepeatStep("change user ready state", () =>
{ {
MultiplayerClient.ChangeUserState(RNG.Next(0, users), RNG.NextBool() ? MultiplayerUserState.Ready : MultiplayerUserState.Idle); changeUserState(RNG.Next(0, users), RNG.NextBool() ? MultiplayerUserState.Ready : MultiplayerUserState.Idle);
}, 20); }, 20);
AddRepeatStep("ready all users", () => AddRepeatStep("ready all users", () =>
{ {
var nextUnready = MultiplayerClient.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle); var nextUnready = multiplayerRoom.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle);
if (nextUnready != null) if (nextUnready != null)
MultiplayerClient.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready); changeUserState(nextUnready.UserID, MultiplayerUserState.Ready);
}, users); }, users);
} }
private void verifyGameplayStartFlow() private void verifyGameplayStartFlow()
{ {
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready); checkLocalUserState(MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton>(); ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user waiting for load", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
AddStep("finish gameplay", () => AddStep("check start request received", () => multiplayerClient.Verify(m => m.StartMatch(), Times.Once));
{
MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.Loaded);
MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.FinishedPlay);
});
AddUntilStep("ready button enabled", () => control.ChildrenOfType<OsuButton>().Single().Enabled.Value);
} }
private void checkLocalUserState(MultiplayerUserState state) =>
AddUntilStep($"local user is {state}", () => localUser.State == state);
private void setRoomCountdown(TimeSpan duration)
{
multiplayerRoom.Countdown = new MatchStartCountdown { TimeRemaining = duration };
raiseRoomUpdated();
}
private void changeUserState(int userId, MultiplayerUserState newState)
{
multiplayerRoom.Users.Single(u => u.UserID == userId).State = newState;
raiseRoomUpdated();
}
private void addUser(APIUser user, bool asHost = false)
{
var multiplayerRoomUser = new MultiplayerRoomUser(user.Id) { User = user };
multiplayerRoom.Users.Add(multiplayerRoomUser);
if (asHost)
transferHost(multiplayerRoomUser);
raiseRoomUpdated();
}
private void transferHost(MultiplayerRoomUser user)
{
multiplayerRoom.Host = user;
raiseRoomUpdated();
}
private void changeRoomSettings(MultiplayerRoomSettings settings)
{
multiplayerRoom.Settings = settings;
// Changing settings should reset all user ready statuses.
foreach (var user in multiplayerRoom.Users)
{
if (user.State == MultiplayerUserState.Ready)
user.State = MultiplayerUserState.Idle;
}
raiseRoomUpdated();
}
private void raiseRoomUpdated() => multiplayerClient.Raise(m => m.RoomUpdated -= null);
} }
} }

View File

@ -464,16 +464,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
private class TestMultiSpectatorScreen : MultiSpectatorScreen private class TestMultiSpectatorScreen : MultiSpectatorScreen
{ {
private readonly double? gameplayStartTime; private readonly double? startTime;
public TestMultiSpectatorScreen(Room room, MultiplayerRoomUser[] users, double? gameplayStartTime = null) public TestMultiSpectatorScreen(Room room, MultiplayerRoomUser[] users, double? startTime = null)
: base(room, users) : base(room, users)
{ {
this.gameplayStartTime = gameplayStartTime; this.startTime = startTime;
} }
protected override MasterGameplayClockContainer CreateMasterGameplayClockContainer(WorkingBeatmap beatmap) protected override MasterGameplayClockContainer CreateMasterGameplayClockContainer(WorkingBeatmap beatmap)
=> new MasterGameplayClockContainer(beatmap, gameplayStartTime ?? 0, gameplayStartTime.HasValue); => new MasterGameplayClockContainer(beatmap, 0) { StartTime = startTime ?? 0 };
} }
} }
} }

View File

@ -495,17 +495,20 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true); AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);
AddAssert("Mods match current item", () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym))); AddAssert("Mods match current item",
() => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
AddStep("Switch required mods", () => ((MultiplayerMatchSongSelect)multiplayerComponents.MultiplayerScreen.CurrentSubScreen).Mods.Value = new Mod[] { new OsuModDoubleTime() }); AddStep("Switch required mods", () => ((MultiplayerMatchSongSelect)multiplayerComponents.MultiplayerScreen.CurrentSubScreen).Mods.Value = new Mod[] { new OsuModDoubleTime() });
AddAssert("Mods don't match current item", () => !SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym))); AddAssert("Mods don't match current item",
() => !SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely()); AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player); AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
AddAssert("Mods match current item", () => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym))); AddAssert("Mods match current item",
() => SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
} }
[Test] [Test]
@ -665,6 +668,41 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen); AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen);
} }
[Test]
public void TestGameplayDoesntStartWithNonLoadedUser()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
Playlist =
{
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
}
}
});
pressReadyButton();
AddStep("join other user and ready", () =>
{
multiplayerClient.AddUser(new APIUser { Id = 1234 });
multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Ready);
});
AddStep("start match", () =>
{
multiplayerClient.StartMatch();
});
AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player);
AddWaitStep("wait some", 20);
AddAssert("ensure gameplay hasn't started", () => this.ChildrenOfType<GameplayClockContainer>().SingleOrDefault()?.IsRunning == false);
}
[Test] [Test]
public void TestRoomSettingsReQueriedWhenJoiningRoom() public void TestRoomSettingsReQueriedWhenJoiningRoom()
{ {

View File

@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Navigation
typeof(OsuLogo), typeof(OsuLogo),
typeof(IdleTracker), typeof(IdleTracker),
typeof(OnScreenDisplay), typeof(OnScreenDisplay),
typeof(NotificationOverlay), typeof(INotificationOverlay),
typeof(BeatmapListingOverlay), typeof(BeatmapListingOverlay),
typeof(DashboardOverlay), typeof(DashboardOverlay),
typeof(NewsOverlay), typeof(NewsOverlay),
@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Navigation
typeof(LoginOverlay), typeof(LoginOverlay),
typeof(MusicController), typeof(MusicController),
typeof(AccountCreationOverlay), typeof(AccountCreationOverlay),
typeof(DialogOverlay), typeof(IDialogOverlay),
typeof(ScreenshotManager) typeof(ScreenshotManager)
}; };

View File

@ -5,6 +5,7 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -113,12 +114,12 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("did not perform", () => !actionPerformed); AddAssert("did not perform", () => !actionPerformed);
AddAssert("only one exit attempt", () => blocker.ExitAttempts == 1); AddAssert("only one exit attempt", () => blocker.ExitAttempts == 1);
AddUntilStep("wait for dialog display", () => Game.Dependencies.Get<DialogOverlay>().IsLoaded); waitForDialogOverlayLoad();
if (confirmed) if (confirmed)
{ {
AddStep("accept dialog", () => InputManager.Key(Key.Number1)); AddStep("accept dialog", () => InputManager.Key(Key.Number1));
AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get<DialogOverlay>().CurrentDialog == null); AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get<IDialogOverlay>().CurrentDialog == null);
AddUntilStep("did perform", () => actionPerformed); AddUntilStep("did perform", () => actionPerformed);
} }
else else
@ -145,7 +146,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddWaitStep("wait a bit", 10); AddWaitStep("wait a bit", 10);
AddUntilStep("wait for dialog display", () => Game.Dependencies.Get<DialogOverlay>().IsLoaded); waitForDialogOverlayLoad();
AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen == blocker2); AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen == blocker2);
AddAssert("did not perform", () => !actionPerformed); AddAssert("did not perform", () => !actionPerformed);
@ -171,6 +172,48 @@ namespace osu.Game.Tests.Visual.Navigation
} }
} }
[TestCase(true)]
[TestCase(false)]
public void TestPerformBlockedByDialogSubScreen(bool confirm)
{
TestScreenWithNestedStack screenWithNestedStack = null;
PushAndConfirm(() => screenWithNestedStack = new TestScreenWithNestedStack());
AddAssert("wait for nested screen", () => screenWithNestedStack.SubScreenStack.CurrentScreen == screenWithNestedStack.Blocker);
AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true));
AddUntilStep("wait for dialog", () => screenWithNestedStack.Blocker.ExitAttempts == 1);
AddWaitStep("wait a bit", 10);
waitForDialogOverlayLoad();
AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen == screenWithNestedStack);
AddAssert("nested screen didn't change", () => screenWithNestedStack.SubScreenStack.CurrentScreen == screenWithNestedStack.Blocker);
AddAssert("did not perform", () => !actionPerformed);
AddAssert("only one exit attempt", () => screenWithNestedStack.Blocker.ExitAttempts == 1);
if (confirm)
{
AddStep("accept dialog", () => InputManager.Key(Key.Number1));
AddAssert("nested screen changed", () => screenWithNestedStack.SubScreenStack.CurrentScreen != screenWithNestedStack.Blocker);
AddUntilStep("did perform", () => actionPerformed);
}
else
{
AddStep("cancel dialog", () => InputManager.Key(Key.Number2));
AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen == screenWithNestedStack);
AddAssert("nested screen didn't change", () => screenWithNestedStack.SubScreenStack.CurrentScreen == screenWithNestedStack.Blocker);
AddAssert("did not perform", () => !actionPerformed);
}
}
private void waitForDialogOverlayLoad() => AddUntilStep("wait for dialog overlay loaded", () => ((Drawable)Game.Dependencies.Get<IDialogOverlay>()).IsLoaded);
private void importAndWaitForSongSelect() private void importAndWaitForSongSelect()
{ {
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
@ -181,13 +224,13 @@ namespace osu.Game.Tests.Visual.Navigation
public class DialogBlockingScreen : OsuScreen public class DialogBlockingScreen : OsuScreen
{ {
[Resolved] [Resolved]
private DialogOverlay dialogOverlay { get; set; } private IDialogOverlay dialogOverlay { get; set; }
private int dialogDisplayCount; private int dialogDisplayCount;
public int ExitAttempts { get; private set; } public int ExitAttempts { get; private set; }
public override bool OnExiting(IScreen next) public override bool OnExiting(ScreenExitEvent e)
{ {
ExitAttempts++; ExitAttempts++;
@ -197,7 +240,32 @@ namespace osu.Game.Tests.Visual.Navigation
return true; return true;
} }
return base.OnExiting(next); return base.OnExiting(e);
}
}
public class TestScreenWithNestedStack : OsuScreen, IHasSubScreenStack
{
public DialogBlockingScreen Blocker { get; private set; }
public ScreenStack SubScreenStack { get; } = new ScreenStack();
public TestScreenWithNestedStack()
{
AddInternal(SubScreenStack);
SubScreenStack.Push(Blocker = new DialogBlockingScreen());
}
public override bool OnExiting(ScreenExitEvent e)
{
if (SubScreenStack.CurrentScreen != null)
{
SubScreenStack.CurrentScreen.Exit();
return true;
}
return base.OnExiting(e);
} }
} }
} }

View File

@ -6,6 +6,7 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Screens; using osu.Framework.Screens;
@ -200,10 +201,10 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("choose clear all scores", () => InputManager.Key(Key.Number4)); AddStep("choose clear all scores", () => InputManager.Key(Key.Number4));
AddUntilStep("wait for dialog display", () => Game.Dependencies.Get<DialogOverlay>().IsLoaded); AddUntilStep("wait for dialog display", () => ((Drawable)Game.Dependencies.Get<IDialogOverlay>()).IsLoaded);
AddUntilStep("wait for dialog", () => Game.Dependencies.Get<DialogOverlay>().CurrentDialog != null); AddUntilStep("wait for dialog", () => Game.Dependencies.Get<IDialogOverlay>().CurrentDialog != null);
AddStep("confirm deletion", () => InputManager.Key(Key.Number1)); AddStep("confirm deletion", () => InputManager.Key(Key.Number1));
AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get<DialogOverlay>().CurrentDialog == null); AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get<IDialogOverlay>().CurrentDialog == null);
AddUntilStep("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find<ScoreInfo>(score.ID)?.DeletePending == true)); AddUntilStep("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find<ScoreInfo>(score.ID)?.DeletePending == true));
@ -246,10 +247,10 @@ namespace osu.Game.Tests.Visual.Navigation
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddUntilStep("wait for dialog display", () => Game.Dependencies.Get<DialogOverlay>().IsLoaded); AddUntilStep("wait for dialog display", () => ((Drawable)Game.Dependencies.Get<IDialogOverlay>()).IsLoaded);
AddUntilStep("wait for dialog", () => Game.Dependencies.Get<DialogOverlay>().CurrentDialog != null); AddUntilStep("wait for dialog", () => Game.Dependencies.Get<IDialogOverlay>().CurrentDialog != null);
AddStep("confirm deletion", () => InputManager.Key(Key.Number1)); AddStep("confirm deletion", () => InputManager.Key(Key.Number1));
AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get<DialogOverlay>().CurrentDialog == null); AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get<IDialogOverlay>().CurrentDialog == null);
AddUntilStep("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find<ScoreInfo>(score.ID)?.DeletePending == true)); AddUntilStep("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find<ScoreInfo>(score.ID)?.DeletePending == true));

View File

@ -0,0 +1,188 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Overlays;
using osu.Game.Overlays.Chat.ChannelList;
namespace osu.Game.Tests.Visual.Online
{
[TestFixture]
public class TestSceneChannelList : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
[Cached]
private readonly Bindable<Channel> selected = new Bindable<Channel>();
private OsuSpriteText selectorText;
private OsuSpriteText selectedText;
private OsuSpriteText leaveText;
private ChannelList channelList;
[SetUp]
public void SetUp()
{
Schedule(() =>
{
Child = new GridContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Height = 0.7f,
RowDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 20),
new Dimension(GridSizeMode.Absolute, 20),
new Dimension(GridSizeMode.Absolute, 20),
new Dimension(),
},
Content = new[]
{
new Drawable[]
{
selectorText = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
},
new Drawable[]
{
selectedText = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
},
new Drawable[]
{
leaveText = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
},
new Drawable[]
{
channelList = new ChannelList
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y,
Width = 190,
},
},
},
};
channelList.OnRequestSelect += channel =>
{
channelList.SelectorActive.Value = false;
selected.Value = channel;
};
channelList.OnRequestLeave += channel =>
{
leaveText.Text = $"OnRequestLeave: {channel.Name}";
leaveText.FadeOutFromOne(1000, Easing.InQuint);
selected.Value = null;
channelList.RemoveChannel(channel);
};
channelList.SelectorActive.BindValueChanged(change =>
{
selectorText.Text = $"Channel Selector Active: {change.NewValue}";
selected.Value = null;
}, true);
selected.BindValueChanged(change =>
{
selectedText.Text = $"Selected Channel: {change.NewValue?.Name ?? "[null]"}";
}, true);
});
}
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Add Public Channels", () =>
{
for (int i = 0; i < 10; i++)
channelList.AddChannel(createRandomPublicChannel());
});
AddStep("Add Private Channels", () =>
{
for (int i = 0; i < 10; i++)
channelList.AddChannel(createRandomPrivateChannel());
});
}
[Test]
public void TestVisual()
{
AddStep("Unread Selected", () =>
{
if (selected.Value != null)
channelList.GetItem(selected.Value).Unread.Value = true;
});
AddStep("Read Selected", () =>
{
if (selected.Value != null)
channelList.GetItem(selected.Value).Unread.Value = false;
});
AddStep("Add Mention Selected", () =>
{
if (selected.Value != null)
channelList.GetItem(selected.Value).Mentions.Value++;
});
AddStep("Add 98 Mentions Selected", () =>
{
if (selected.Value != null)
channelList.GetItem(selected.Value).Mentions.Value += 98;
});
AddStep("Clear Mentions Selected", () =>
{
if (selected.Value != null)
channelList.GetItem(selected.Value).Mentions.Value = 0;
});
}
private Channel createRandomPublicChannel()
{
int id = RNG.Next(0, 10000);
return new Channel
{
Name = $"#channel-{id}",
Type = ChannelType.Public,
Id = id,
};
}
private Channel createRandomPrivateChannel()
{
int id = RNG.Next(0, 10000);
return new Channel(new APIUser
{
Id = id,
Username = $"test user {id}",
});
}
}
}

View File

@ -1,163 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Overlays;
using osu.Game.Overlays.Chat.ChannelList;
using osuTK;
namespace osu.Game.Tests.Visual.Online
{
[TestFixture]
public class TestSceneChannelListItem : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
[Cached]
private readonly Bindable<Channel> selected = new Bindable<Channel>();
private static readonly List<Channel> channels = new List<Channel>
{
createPublicChannel("#public-channel"),
createPublicChannel("#public-channel-long-name"),
createPrivateChannel("test user", 2),
createPrivateChannel("test user long name", 3),
};
private readonly Dictionary<Channel, ChannelListItem> channelMap = new Dictionary<Channel, ChannelListItem>();
private FillFlowContainer flow;
private OsuSpriteText selectedText;
private OsuSpriteText leaveText;
[SetUp]
public void SetUp()
{
Schedule(() =>
{
foreach (var item in channelMap.Values)
item.Expire();
channelMap.Clear();
Child = new FillFlowContainer
{
Direction = FillDirection.Vertical,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(10),
Children = new Drawable[]
{
selectedText = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
leaveText = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Height = 16,
AlwaysPresent = true,
},
new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Y,
Width = 190,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background6,
},
flow = new FillFlowContainer
{
Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
},
},
},
};
selected.BindValueChanged(change =>
{
selectedText.Text = $"Selected Channel: {change.NewValue?.Name ?? "[null]"}";
}, true);
foreach (var channel in channels)
{
var item = new ChannelListItem(channel);
flow.Add(item);
channelMap.Add(channel, item);
item.OnRequestSelect += c => selected.Value = c;
item.OnRequestLeave += leaveChannel;
}
});
}
[Test]
public void TestVisual()
{
AddStep("Select second item", () => selected.Value = channels.Skip(1).First());
AddStep("Unread Selected", () =>
{
if (selected.Value != null)
channelMap[selected.Value].Unread.Value = true;
});
AddStep("Read Selected", () =>
{
if (selected.Value != null)
channelMap[selected.Value].Unread.Value = false;
});
AddStep("Add Mention Selected", () =>
{
if (selected.Value != null)
channelMap[selected.Value].Mentions.Value++;
});
AddStep("Add 98 Mentions Selected", () =>
{
if (selected.Value != null)
channelMap[selected.Value].Mentions.Value += 98;
});
AddStep("Clear Mentions Selected", () =>
{
if (selected.Value != null)
channelMap[selected.Value].Mentions.Value = 0;
});
}
private void leaveChannel(Channel channel)
{
leaveText.Text = $"OnRequestLeave: {channel.Name}";
leaveText.FadeOutFromOne(1000, Easing.InQuint);
}
private static Channel createPublicChannel(string name) =>
new Channel { Name = name, Type = ChannelType.Public, Id = 1234 };
private static Channel createPrivateChannel(string username, int id)
=> new Channel(new APIUser { Id = id, Username = username });
}
}

View File

@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Online
Dependencies.Cache(chatManager); Dependencies.Cache(chatManager);
Dependencies.Cache(new ChatOverlay()); Dependencies.Cache(new ChatOverlay());
Dependencies.Cache(dialogOverlay); Dependencies.CacheAs<IDialogOverlay>(dialogOverlay);
} }
[SetUp] [SetUp]

View File

@ -200,7 +200,7 @@ namespace osu.Game.Tests.Visual.Online
[Cached] [Cached]
public ChannelManager ChannelManager { get; } = new ChannelManager(); public ChannelManager ChannelManager { get; } = new ChannelManager();
[Cached] [Cached(typeof(INotificationOverlay))]
public NotificationOverlay NotificationOverlay { get; } = new NotificationOverlay public NotificationOverlay NotificationOverlay { get; } = new NotificationOverlay
{ {
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,

View File

@ -14,7 +14,7 @@ namespace osu.Game.Tests.Visual.Settings
{ {
public class TestSceneMigrationScreens : ScreenTestScene public class TestSceneMigrationScreens : ScreenTestScene
{ {
[Cached] [Cached(typeof(INotificationOverlay))]
private readonly NotificationOverlay notifications; private readonly NotificationOverlay notifications;
public TestSceneMigrationScreens() public TestSceneMigrationScreens()

View File

@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Settings
Depth = -1 Depth = -1
}); });
Dependencies.Cache(dialogOverlay); Dependencies.CacheAs<IDialogOverlay>(dialogOverlay);
} }
} }
} }

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
@ -55,7 +56,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("no mods selected", () => SelectedMods.Value = Array.Empty<Mod>()); AddStep("no mods selected", () => SelectedMods.Value = Array.Empty<Mod>());
AddAssert("first bar text is Circle Size", () => advancedStats.ChildrenOfType<SpriteText>().First().Text == "Circle Size"); AddAssert("first bar text is correct", () => advancedStats.ChildrenOfType<SpriteText>().First().Text == BeatmapsetsStrings.ShowStatsCs);
AddAssert("circle size bar is white", () => barIsWhite(advancedStats.FirstValue)); AddAssert("circle size bar is white", () => barIsWhite(advancedStats.FirstValue));
AddAssert("HP drain bar is white", () => barIsWhite(advancedStats.HpDrain)); AddAssert("HP drain bar is white", () => barIsWhite(advancedStats.HpDrain));
AddAssert("accuracy bar is white", () => barIsWhite(advancedStats.Accuracy)); AddAssert("accuracy bar is white", () => barIsWhite(advancedStats.Accuracy));
@ -78,7 +79,7 @@ namespace osu.Game.Tests.Visual.SongSelect
StarRating = 8 StarRating = 8
}); });
AddAssert("first bar text is Key Count", () => advancedStats.ChildrenOfType<SpriteText>().First().Text == "Key Count"); AddAssert("first bar text is correct", () => advancedStats.ChildrenOfType<SpriteText>().First().Text == BeatmapsetsStrings.ShowStatsCsMania);
} }
[Test] [Test]

View File

@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
private readonly FailableLeaderboard leaderboard; private readonly FailableLeaderboard leaderboard;
[Cached] [Cached(typeof(IDialogOverlay))]
private readonly DialogOverlay dialogOverlay; private readonly DialogOverlay dialogOverlay;
private ScoreManager scoreManager; private ScoreManager scoreManager;

View File

@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
public class TestSceneUserTopScoreContainer : OsuTestScene public class TestSceneUserTopScoreContainer : OsuTestScene
{ {
[Cached] [Cached(typeof(IDialogOverlay))]
private readonly DialogOverlay dialogOverlay; private readonly DialogOverlay dialogOverlay;
public TestSceneUserTopScoreContainer() public TestSceneUserTopScoreContainer()

View File

@ -80,10 +80,10 @@ namespace osu.Game.Tests.Visual
public override bool OnBackButton() => (screenStack.CurrentScreen as OsuScreen)?.OnBackButton() ?? base.OnBackButton(); public override bool OnBackButton() => (screenStack.CurrentScreen as OsuScreen)?.OnBackButton() ?? base.OnBackButton();
public override bool OnExiting(IScreen next) public override bool OnExiting(ScreenExitEvent e)
{ {
if (screenStack.CurrentScreen == null) if (screenStack.CurrentScreen == null)
return base.OnExiting(next); return base.OnExiting(e);
screenStack.Exit(); screenStack.Exit();
return true; return true;

View File

@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private BeatmapInfo beatmapInfo; private BeatmapInfo beatmapInfo;
[Cached] [Cached(typeof(IDialogOverlay))]
private readonly DialogOverlay dialogOverlay; private readonly DialogOverlay dialogOverlay;
public TestSceneDeleteLocalScore() public TestSceneDeleteLocalScore()

View File

@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[Resolved] [Resolved]
private RulesetStore rulesetStore { get; set; } private RulesetStore rulesetStore { get; set; }
private ModSelectScreen modSelectScreen; private UserModSelectScreen modSelectScreen;
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
@ -35,7 +35,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private void createScreen() private void createScreen()
{ {
AddStep("create screen", () => Child = modSelectScreen = new ModSelectScreen AddStep("create screen", () => Child = modSelectScreen = new UserModSelectScreen
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible }, State = { Value = Visibility.Visible },

View File

@ -10,19 +10,19 @@ using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
{ {
[TestFixture] [TestFixture]
public class TestScenePopupScreenTitle : OsuTestScene public class TestSceneShearedOverlayHeader : OsuTestScene
{ {
[Cached] [Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
[Test] [Test]
public void TestPopupScreenTitle() public void TestShearedOverlayHeader()
{ {
AddStep("create content", () => AddStep("create content", () =>
{ {
Child = new PopupScreenTitle Child = new ShearedOverlayHeader
{ {
Title = "Popup Screen Title", Title = "Sheared overlay header",
Description = string.Join(" ", Enumerable.Repeat("This is a description.", 20)), Description = string.Join(" ", Enumerable.Repeat("This is a description.", 20)),
Close = () => { } Close = () => { }
}; };
@ -34,9 +34,9 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
AddStep("create content", () => AddStep("create content", () =>
{ {
Child = new PopupScreenTitle Child = new ShearedOverlayHeader
{ {
Title = "Popup Screen Title", Title = "Sheared overlay header",
Description = "This is a description." Description = "This is a description."
}; };
}); });

View File

@ -3,6 +3,7 @@
using System; using System;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
@ -14,6 +15,6 @@ namespace osu.Game.Beatmaps
public Func<Drawable> CreateIcon; public Func<Drawable> CreateIcon;
public string Content; public string Content;
public string Name; public LocalisableString Name;
} }
} }

View File

@ -12,6 +12,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online; using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Beatmaps.Drawables namespace osu.Game.Beatmaps.Drawables
{ {
@ -104,7 +105,7 @@ namespace osu.Game.Beatmaps.Drawables
if ((beatmapSet as IBeatmapSetOnlineInfo)?.Availability.DownloadDisabled == true) if ((beatmapSet as IBeatmapSetOnlineInfo)?.Availability.DownloadDisabled == true)
{ {
button.Enabled.Value = false; button.Enabled.Value = false;
button.TooltipText = "this beatmap is currently not available for download."; button.TooltipText = BeatmapsetsStrings.AvailabilityDisabled;
} }
break; break;

View File

@ -158,7 +158,7 @@ namespace osu.Game.Collections
public Func<Vector2, bool> IsTextBoxHovered; public Func<Vector2, bool> IsTextBoxHovered;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private DialogOverlay dialogOverlay { get; set; } private IDialogOverlay dialogOverlay { get; set; }
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private CollectionManager collectionManager { get; set; } private CollectionManager collectionManager { get; set; }

View File

@ -52,7 +52,7 @@ namespace osu.Game.Database
private OsuConfigManager config { get; set; } = null!; private OsuConfigManager config { get; set; } = null!;
[Resolved] [Resolved]
private NotificationOverlay notificationOverlay { get; set; } = null!; private INotificationOverlay notificationOverlay { get; set; } = null!;
[Resolved] [Resolved]
private OsuGame game { get; set; } = null!; private OsuGame game { get; set; } = null!;

View File

@ -40,7 +40,7 @@ namespace osu.Game.Database
private OsuGame game { get; set; } private OsuGame game { get; set; }
[Resolved] [Resolved]
private DialogOverlay dialogOverlay { get; set; } private IDialogOverlay dialogOverlay { get; set; }
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private DesktopGameHost desktopGameHost { get; set; } private DesktopGameHost desktopGameHost { get; set; }

View File

@ -209,7 +209,7 @@ namespace osu.Game.Graphics.Containers
{ {
protected override bool AllowStoryboardBackground => false; protected override bool AllowStoryboardBackground => false;
public override void OnEntering(IScreen last) public override void OnEntering(ScreenTransitionEvent e)
{ {
this.FadeInFromZero(4000, Easing.OutQuint); this.FadeInFromZero(4000, Easing.OutQuint);
} }

View File

@ -43,7 +43,7 @@ namespace osu.Game.Graphics
private Storage storage; private Storage storage;
[Resolved] [Resolved]
private NotificationOverlay notificationOverlay { get; set; } private INotificationOverlay notificationOverlay { get; set; }
private Sample shutter; private Sample shutter;

View File

@ -3,6 +3,7 @@
using System; using System;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
@ -15,7 +16,7 @@ namespace osu.Game.Graphics.UserInterface
{ {
} }
public OsuMenuItem(string text, MenuItemType type, Action action) public OsuMenuItem(LocalisableString text, MenuItemType type, Action action)
: base(text, action) : base(text, action)
{ {
Type = type; Type = type;

View File

@ -5,6 +5,7 @@ using System;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Graphics.UserInterface.PageSelector namespace osu.Game.Graphics.UserInterface.PageSelector
{ {
@ -29,7 +30,7 @@ namespace osu.Game.Graphics.UserInterface.PageSelector
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Children = new Drawable[] Children = new Drawable[]
{ {
previousPageButton = new PageSelectorPrevNextButton(false, "prev") previousPageButton = new PageSelectorPrevNextButton(false, CommonStrings.PaginationPrevious)
{ {
Action = () => CurrentPage.Value -= 1, Action = () => CurrentPage.Value -= 1,
}, },
@ -38,7 +39,7 @@ namespace osu.Game.Graphics.UserInterface.PageSelector
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
}, },
nextPageButton = new PageSelectorPrevNextButton(true, "next") nextPageButton = new PageSelectorPrevNextButton(true, CommonStrings.PaginationNext)
{ {
Action = () => CurrentPage.Value += 1 Action = () => CurrentPage.Value += 1
} }

View File

@ -2,9 +2,11 @@
// 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.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osuTK; using osuTK;
@ -13,12 +15,12 @@ namespace osu.Game.Graphics.UserInterface.PageSelector
public class PageSelectorPrevNextButton : PageSelectorButton public class PageSelectorPrevNextButton : PageSelectorButton
{ {
private readonly bool rightAligned; private readonly bool rightAligned;
private readonly string text; private readonly LocalisableString text;
private SpriteIcon icon; private SpriteIcon icon;
private OsuSpriteText name; private OsuSpriteText name;
public PageSelectorPrevNextButton(bool rightAligned, string text) public PageSelectorPrevNextButton(bool rightAligned, LocalisableString text)
{ {
this.rightAligned = rightAligned; this.rightAligned = rightAligned;
this.text = text; this.text = text;

View File

@ -5,6 +5,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Resources.Localisation.Web;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -27,7 +28,7 @@ namespace osu.Game.Graphics.UserInterface
}); });
TextFlow.Padding = new MarginPadding { Right = 35 }; TextFlow.Padding = new MarginPadding { Right = 35 };
PlaceholderText = "type to search"; PlaceholderText = HomeStrings.SearchPlaceholder;
} }
public override bool OnPressed(KeyBindingPressEvent<PlatformAction> e) public override bool OnPressed(KeyBindingPressEvent<PlatformAction> e)

View File

@ -19,8 +19,10 @@ using osuTK;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
public class PopupScreenTitle : CompositeDrawable public class ShearedOverlayHeader : CompositeDrawable
{ {
public const float HEIGHT = main_area_height + 2 * corner_radius;
public LocalisableString Title public LocalisableString Title
{ {
set => titleSpriteText.Text = value; set => titleSpriteText.Text = value;
@ -48,7 +50,7 @@ namespace osu.Game.Graphics.UserInterface
private readonly OsuTextFlowContainer descriptionText; private readonly OsuTextFlowContainer descriptionText;
private readonly IconButton closeButton; private readonly IconButton closeButton;
public PopupScreenTitle() public ShearedOverlayHeader()
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
@ -67,7 +69,7 @@ namespace osu.Game.Graphics.UserInterface
underlayContainer = new Container underlayContainer = new Container
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = main_area_height + 2 * corner_radius, Height = HEIGHT,
CornerRadius = corner_radius, CornerRadius = corner_radius,
Masking = true, Masking = true,
BorderThickness = 2, BorderThickness = 2,

View File

@ -11,7 +11,9 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Overlays; using osu.Game.Overlays;
using osuTK; using osuTK;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
@ -80,7 +82,7 @@ namespace osu.Game.Graphics.UserInterface
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
Text = "show more".ToUpper(), Text = CommonStrings.ButtonsShowMore.ToUpper(),
}, },
rightIcon = new ChevronIcon rightIcon = new ChevronIcon
{ {

View File

@ -14,6 +14,7 @@ using osu.Framework.Localisation;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Resources.Localisation.Web;
using osuTK; using osuTK;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
@ -139,7 +140,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
public MenuItem[] ContextMenuItems => new MenuItem[] public MenuItem[] ContextMenuItems => new MenuItem[]
{ {
new OsuMenuItem("Delete", MenuItemType.Destructive, () => DeleteRequested?.Invoke()) new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => DeleteRequested?.Invoke())
}; };
} }
} }

View File

@ -9,16 +9,6 @@ namespace osu.Game.Localisation
{ {
private const string prefix = @"osu.Game.Resources.Localisation.Common"; private const string prefix = @"osu.Game.Resources.Localisation.Common";
/// <summary>
/// "Cancel"
/// </summary>
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>

View File

@ -0,0 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public class DebugLocalisationStore : ILocalisationStore
{
public string Get(string lookup) => $@"[[{lookup.Substring(lookup.LastIndexOf('.') + 1)}]]";
public Task<string> GetAsync(string lookup, CancellationToken cancellationToken = default) => Task.FromResult(Get(lookup));
public Stream GetStream(string name) => throw new NotImplementedException();
public IEnumerable<string> GetAvailableResources() => throw new NotImplementedException();
public CultureInfo EffectiveCulture { get; } = CultureInfo.CurrentCulture;
public void Dispose()
{
}
}
}

View File

@ -110,6 +110,11 @@ namespace osu.Game.Localisation
// zh_hk, // zh_hk,
[Description(@"繁體中文(台灣)")] [Description(@"繁體中文(台灣)")]
zh_tw zh_hant,
#if DEBUG
[Description(@"Debug (show raw keys)")]
debug
#endif
} }
} }

View File

@ -13,8 +13,8 @@ namespace osu.Game.Online.API.Requests
private readonly BeatmapSetType type; private readonly BeatmapSetType type;
public GetUserBeatmapsRequest(long userId, BeatmapSetType type, int page = 0, int itemsPerPage = 6) public GetUserBeatmapsRequest(long userId, BeatmapSetType type, PaginationParameters pagination)
: base(page, itemsPerPage) : base(pagination)
{ {
this.userId = userId; this.userId = userId;
this.type = type; this.type = type;

View File

@ -10,8 +10,8 @@ namespace osu.Game.Online.API.Requests
{ {
private readonly long userId; private readonly long userId;
public GetUserKudosuHistoryRequest(long userId, int page = 0, int itemsPerPage = 5) public GetUserKudosuHistoryRequest(long userId, PaginationParameters pagination)
: base(page, itemsPerPage) : base(pagination)
{ {
this.userId = userId; this.userId = userId;
} }

View File

@ -10,8 +10,8 @@ namespace osu.Game.Online.API.Requests
{ {
private readonly long userId; private readonly long userId;
public GetUserMostPlayedBeatmapsRequest(long userId, int page = 0, int itemsPerPage = 5) public GetUserMostPlayedBeatmapsRequest(long userId, PaginationParameters pagination)
: base(page, itemsPerPage) : base(pagination)
{ {
this.userId = userId; this.userId = userId;
} }

View File

@ -10,8 +10,8 @@ namespace osu.Game.Online.API.Requests
{ {
private readonly long userId; private readonly long userId;
public GetUserRecentActivitiesRequest(long userId, int page = 0, int itemsPerPage = 5) public GetUserRecentActivitiesRequest(long userId, PaginationParameters pagination)
: base(page, itemsPerPage) : base(pagination)
{ {
this.userId = userId; this.userId = userId;
} }

View File

@ -14,8 +14,8 @@ namespace osu.Game.Online.API.Requests
private readonly ScoreType type; private readonly ScoreType type;
private readonly RulesetInfo ruleset; private readonly RulesetInfo ruleset;
public GetUserScoresRequest(long userId, ScoreType type, int page = 0, int itemsPerPage = 5, RulesetInfo ruleset = null) public GetUserScoresRequest(long userId, ScoreType type, PaginationParameters pagination, RulesetInfo ruleset = null)
: base(page, itemsPerPage) : base(pagination)
{ {
this.userId = userId; this.userId = userId;
this.type = type; this.type = type;

View File

@ -8,21 +8,19 @@ namespace osu.Game.Online.API.Requests
{ {
public abstract class PaginatedAPIRequest<T> : APIRequest<T> where T : class public abstract class PaginatedAPIRequest<T> : APIRequest<T> where T : class
{ {
private readonly int page; private readonly PaginationParameters pagination;
private readonly int itemsPerPage;
protected PaginatedAPIRequest(int page, int itemsPerPage) protected PaginatedAPIRequest(PaginationParameters pagination)
{ {
this.page = page; this.pagination = pagination;
this.itemsPerPage = itemsPerPage;
} }
protected override WebRequest CreateWebRequest() protected override WebRequest CreateWebRequest()
{ {
var req = base.CreateWebRequest(); var req = base.CreateWebRequest();
req.AddParameter("offset", (page * itemsPerPage).ToString(CultureInfo.InvariantCulture)); req.AddParameter("offset", pagination.Offset.ToString(CultureInfo.InvariantCulture));
req.AddParameter("limit", itemsPerPage.ToString(CultureInfo.InvariantCulture)); req.AddParameter("limit", pagination.Limit.ToString(CultureInfo.InvariantCulture));
return req; return req;
} }

View File

@ -0,0 +1,38 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Online.API.Requests
{
/// <summary>
/// Represents a pagination data used for <see cref="PaginatedAPIRequest{T}"/>.
/// </summary>
public readonly struct PaginationParameters
{
/// <summary>
/// The starting point of the request.
/// </summary>
public int Offset { get; }
/// <summary>
/// The maximum number of items to return in this request.
/// </summary>
public int Limit { get; }
public PaginationParameters(int offset, int limit)
{
Offset = offset;
Limit = limit;
}
public PaginationParameters(int limit)
: this(0, limit)
{
}
/// <summary>
/// Returns a <see cref="PaginationParameters"/> of the next number of items defined by <paramref name="limit"/> after this.
/// </summary>
/// <param name="limit">The limit of the next pagination.</param>
public PaginationParameters TakeNext(int limit) => new PaginationParameters(Offset + Limit, limit);
}
}

View File

@ -1,22 +1,23 @@
// 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.ComponentModel; using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Online.API.Requests.Responses namespace osu.Game.Online.API.Requests.Responses
{ {
public enum APIPlayStyle public enum APIPlayStyle
{ {
[Description("Keyboard")] [LocalisableDescription(typeof(CommonStrings), nameof(CommonStrings.DeviceKeyboard))]
Keyboard, Keyboard,
[Description("Mouse")] [LocalisableDescription(typeof(CommonStrings), nameof(CommonStrings.DeviceMouse))]
Mouse, Mouse,
[Description("Tablet")] [LocalisableDescription(typeof(CommonStrings), nameof(CommonStrings.DeviceTablet))]
Tablet, Tablet,
[Description("Touch Screen")] [LocalisableDescription(typeof(CommonStrings), nameof(CommonStrings.DeviceTouch))]
Touch, Touch,
} }
} }

View File

@ -17,7 +17,7 @@ namespace osu.Game.Online.Chat
private GameHost host { get; set; } private GameHost host { get; set; }
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private DialogOverlay dialogOverlay { get; set; } private IDialogOverlay dialogOverlay { get; set; }
private Bindable<bool> externalLinkWarning; private Bindable<bool> externalLinkWarning;

View File

@ -24,7 +24,7 @@ namespace osu.Game.Online.Chat
public class MessageNotifier : Component public class MessageNotifier : Component
{ {
[Resolved] [Resolved]
private NotificationOverlay notifications { get; set; } private INotificationOverlay notifications { get; set; }
[Resolved] [Resolved]
private ChatOverlay chatOverlay { get; set; } private ChatOverlay chatOverlay { get; set; }
@ -170,7 +170,7 @@ namespace osu.Game.Online.Chat
public override bool IsImportant => false; public override bool IsImportant => false;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours, ChatOverlay chatOverlay, NotificationOverlay notificationOverlay) private void load(OsuColour colours, ChatOverlay chatOverlay, INotificationOverlay notificationOverlay)
{ {
IconBackground.Colour = colours.PurpleDark; IconBackground.Colour = colours.PurpleDark;

View File

@ -12,6 +12,7 @@ using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Chat; using osu.Game.Overlays.Chat;
using osu.Game.Resources.Localisation.Web;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Online.Chat namespace osu.Game.Online.Chat
@ -63,7 +64,7 @@ namespace osu.Game.Online.Chat
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = text_box_height, Height = text_box_height,
PlaceholderText = "type your message", PlaceholderText = ChatStrings.InputPlaceholder,
CornerRadius = corner_radius, CornerRadius = corner_radius,
ReleaseFocusOnCommit = false, ReleaseFocusOnCommit = false,
HoldFocus = true, HoldFocus = true,

View File

@ -30,6 +30,7 @@ using osu.Game.Users.Drawables;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Utils; using osu.Game.Utils;
namespace osu.Game.Online.Leaderboards namespace osu.Game.Online.Leaderboards
@ -64,7 +65,7 @@ namespace osu.Game.Online.Leaderboards
private List<ScoreComponentLabel> statisticsLabels; private List<ScoreComponentLabel> statisticsLabels;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private DialogOverlay dialogOverlay { get; set; } private IDialogOverlay dialogOverlay { get; set; }
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private SongSelect songSelect { get; set; } private SongSelect songSelect { get; set; }
@ -291,8 +292,8 @@ namespace osu.Game.Online.Leaderboards
protected virtual IEnumerable<LeaderboardScoreStatistic> GetStatistics(ScoreInfo model) => new[] protected virtual IEnumerable<LeaderboardScoreStatistic> GetStatistics(ScoreInfo model) => new[]
{ {
new LeaderboardScoreStatistic(FontAwesome.Solid.Link, "Max Combo", model.MaxCombo.ToString()), new LeaderboardScoreStatistic(FontAwesome.Solid.Link, BeatmapsetsStrings.ShowScoreboardHeadersCombo, model.MaxCombo.ToString()),
new LeaderboardScoreStatistic(FontAwesome.Solid.Crosshairs, "Accuracy", model.DisplayAccuracy) new LeaderboardScoreStatistic(FontAwesome.Solid.Crosshairs, BeatmapsetsStrings.ShowScoreboardHeadersAccuracy, model.DisplayAccuracy)
}; };
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
@ -403,9 +404,9 @@ namespace osu.Game.Online.Leaderboards
{ {
public IconUsage Icon; public IconUsage Icon;
public LocalisableString Value; public LocalisableString Value;
public string Name; public LocalisableString Name;
public LeaderboardScoreStatistic(IconUsage icon, string name, LocalisableString value) public LeaderboardScoreStatistic(IconUsage icon, LocalisableString name, LocalisableString value)
{ {
Icon = icon; Icon = icon;
Name = name; Name = name;
@ -426,7 +427,7 @@ namespace osu.Game.Online.Leaderboards
items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => new LegacyScoreExporter(storage).Export(Score))); items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => new LegacyScoreExporter(storage).Export(Score)));
if (!isOnlineScope) if (!isOnlineScope)
items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(Score)))); items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(Score))));
return items.ToArray(); return items.ToArray();
} }

View File

@ -67,7 +67,7 @@ namespace osu.Game.Online.Multiplayer
/// <summary> /// <summary>
/// Invoked when the multiplayer server requests the current beatmap to be loaded into play. /// Invoked when the multiplayer server requests the current beatmap to be loaded into play.
/// </summary> /// </summary>
public event Action? LoadRequested; public virtual event Action? LoadRequested;
/// <summary> /// <summary>
/// Invoked when the multiplayer server requests gameplay to be started. /// Invoked when the multiplayer server requests gameplay to be started.
@ -114,12 +114,12 @@ namespace osu.Game.Online.Multiplayer
/// <summary> /// <summary>
/// The <see cref="MultiplayerRoomUser"/> corresponding to the local player, if available. /// The <see cref="MultiplayerRoomUser"/> corresponding to the local player, if available.
/// </summary> /// </summary>
public MultiplayerRoomUser? LocalUser => Room?.Users.SingleOrDefault(u => u.User?.Id == API.LocalUser.Value.Id); public virtual MultiplayerRoomUser? LocalUser => Room?.Users.SingleOrDefault(u => u.User?.Id == API.LocalUser.Value.Id);
/// <summary> /// <summary>
/// Whether the <see cref="LocalUser"/> is the host in <see cref="Room"/>. /// Whether the <see cref="LocalUser"/> is the host in <see cref="Room"/>.
/// </summary> /// </summary>
public bool IsHost public virtual bool IsHost
{ {
get get
{ {

View File

@ -1,7 +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.ComponentModel; using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Online.Rooms namespace osu.Game.Online.Rooms
{ {
@ -11,10 +12,10 @@ namespace osu.Game.Online.Rooms
Playlists, Playlists,
[Description("Head to head")] [LocalisableDescription(typeof(MatchesStrings), nameof(MatchesStrings.MatchTeamTypesHeadToHead))]
HeadToHead, HeadToHead,
[Description("Team VS")] [LocalisableDescription(typeof(MatchesStrings), nameof(MatchesStrings.MatchTeamTypesTeamVs))]
TeamVersus, TeamVersus,
} }
} }

View File

@ -25,7 +25,7 @@ namespace osu.Game.Online.Rooms
/// This differs from a regular download tracking composite as this accounts for the /// This differs from a regular download tracking composite as this accounts for the
/// databased beatmap set's checksum, to disallow from playing with an altered version of the beatmap. /// databased beatmap set's checksum, to disallow from playing with an altered version of the beatmap.
/// </summary> /// </summary>
public sealed class OnlinePlayBeatmapAvailabilityTracker : CompositeDrawable public class OnlinePlayBeatmapAvailabilityTracker : CompositeDrawable
{ {
public readonly IBindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>(); public readonly IBindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
@ -41,7 +41,7 @@ namespace osu.Game.Online.Rooms
/// <summary> /// <summary>
/// The availability state of the currently selected playlist item. /// The availability state of the currently selected playlist item.
/// </summary> /// </summary>
public IBindable<BeatmapAvailability> Availability => availability; public virtual IBindable<BeatmapAvailability> Availability => availability;
private readonly Bindable<BeatmapAvailability> availability = new Bindable<BeatmapAvailability>(BeatmapAvailability.NotDownloaded()); private readonly Bindable<BeatmapAvailability> availability = new Bindable<BeatmapAvailability>(BeatmapAvailability.NotDownloaded());

View File

@ -63,7 +63,7 @@ namespace osu.Game
/// The full osu! experience. Builds on top of <see cref="OsuGameBase"/> to add menus and binding logic /// The full osu! experience. Builds on top of <see cref="OsuGameBase"/> to add menus and binding logic
/// for initial components that are generally retrieved via DI. /// for initial components that are generally retrieved via DI.
/// </summary> /// </summary>
public class OsuGame : OsuGameBase, IKeyBindingHandler<GlobalAction>, ILocalUserPlayInfo public class OsuGame : OsuGameBase, IKeyBindingHandler<GlobalAction>, ILocalUserPlayInfo, IPerformFromScreenRunner
{ {
/// <summary> /// <summary>
/// The amount of global offset to apply when a left/right anchored overlay is displayed (ie. settings or notifications). /// The amount of global offset to apply when a left/right anchored overlay is displayed (ie. settings or notifications).
@ -586,12 +586,6 @@ namespace osu.Game
private PerformFromMenuRunner performFromMainMenuTask; private PerformFromMenuRunner performFromMainMenuTask;
/// <summary>
/// Perform an action only after returning to a specific screen as indicated by <paramref name="validScreens"/>.
/// Eagerly tries to exit the current screen until it succeeds.
/// </summary>
/// <param name="action">The action to perform once we are in the correct state.</param>
/// <param name="validScreens">An optional collection of valid screen types. If any of these screens are already current we can perform the action immediately, else the first valid parent will be made current before performing the action. <see cref="MainMenu"/> is used if not specified.</param>
public void PerformFromScreen(Action<IScreen> action, IEnumerable<Type> validScreens = null) public void PerformFromScreen(Action<IScreen> action, IEnumerable<Type> validScreens = null)
{ {
performFromMainMenuTask?.Cancel(); performFromMainMenuTask?.Cancel();
@ -634,6 +628,14 @@ namespace osu.Game
foreach (var language in Enum.GetValues(typeof(Language)).OfType<Language>()) foreach (var language in Enum.GetValues(typeof(Language)).OfType<Language>())
{ {
#if DEBUG
if (language == Language.debug)
{
Localisation.AddLanguage(Language.debug.ToString(), new DebugLocalisationStore());
continue;
}
#endif
string cultureCode = language.ToCultureCode(); string cultureCode = language.ToCultureCode();
try try
@ -778,7 +780,7 @@ namespace osu.Game
loadComponentSingleFile(onScreenDisplay, Add, true); loadComponentSingleFile(onScreenDisplay, Add, true);
loadComponentSingleFile(Notifications.With(d => loadComponentSingleFile<INotificationOverlay>(Notifications.With(d =>
{ {
d.Anchor = Anchor.TopRight; d.Anchor = Anchor.TopRight;
d.Origin = Anchor.TopRight; d.Origin = Anchor.TopRight;
@ -825,7 +827,7 @@ namespace osu.Game
}, rightFloatingOverlayContent.Add, true); }, rightFloatingOverlayContent.Add, true);
loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true); loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true);
loadComponentSingleFile(new DialogOverlay(), topMostOverlayContent.Add, true); loadComponentSingleFile<IDialogOverlay>(new DialogOverlay(), topMostOverlayContent.Add, true);
loadComponentSingleFile(CreateHighPerformanceSession(), Add); loadComponentSingleFile(CreateHighPerformanceSession(), Add);
@ -982,12 +984,14 @@ namespace osu.Game
/// <param name="component">The component to load.</param> /// <param name="component">The component to load.</param>
/// <param name="loadCompleteAction">An action to invoke on load completion (generally to add the component to the hierarchy).</param> /// <param name="loadCompleteAction">An action to invoke on load completion (generally to add the component to the hierarchy).</param>
/// <param name="cache">Whether to cache the component as type <typeparamref name="T"/> into the game dependencies before any scheduling.</param> /// <param name="cache">Whether to cache the component as type <typeparamref name="T"/> into the game dependencies before any scheduling.</param>
private T loadComponentSingleFile<T>(T component, Action<T> loadCompleteAction, bool cache = false) private T loadComponentSingleFile<T>(T component, Action<Drawable> loadCompleteAction, bool cache = false)
where T : Drawable where T : class
{ {
if (cache) if (cache)
dependencies.CacheAs(component); dependencies.CacheAs(component);
var drawableComponent = component as Drawable ?? throw new ArgumentException($"Component must be a {nameof(Drawable)}", nameof(component));
if (component is OsuFocusedOverlayContainer overlay) if (component is OsuFocusedOverlayContainer overlay)
focusedOverlays.Add(overlay); focusedOverlays.Add(overlay);
@ -1011,7 +1015,7 @@ namespace osu.Game
// Since this is running in a separate thread, it is possible for OsuGame to be disposed after LoadComponentAsync has been called // Since this is running in a separate thread, it is possible for OsuGame to be disposed after LoadComponentAsync has been called
// throwing an exception. To avoid this, the call is scheduled on the update thread, which does not run if IsDisposed = true // throwing an exception. To avoid this, the call is scheduled on the update thread, which does not run if IsDisposed = true
Task task = null; Task task = null;
var del = new ScheduledDelegate(() => task = LoadComponentAsync(component, loadCompleteAction)); var del = new ScheduledDelegate(() => task = LoadComponentAsync(drawableComponent, loadCompleteAction));
Scheduler.Add(del); Scheduler.Add(del);
// The delegate won't complete if OsuGame has been disposed in the meantime // The delegate won't complete if OsuGame has been disposed in the meantime

View File

@ -8,21 +8,21 @@ namespace osu.Game.Overlays.AccountCreation
{ {
public abstract class AccountCreationScreen : Screen public abstract class AccountCreationScreen : Screen
{ {
public override void OnEntering(IScreen last) public override void OnEntering(ScreenTransitionEvent e)
{ {
base.OnEntering(last); base.OnEntering(e);
this.FadeOut().Delay(200).FadeIn(200); this.FadeOut().Delay(200).FadeIn(200);
} }
public override void OnResuming(IScreen last) public override void OnResuming(ScreenTransitionEvent e)
{ {
base.OnResuming(last); base.OnResuming(e);
this.FadeIn(200); this.FadeIn(200);
} }
public override void OnSuspending(IScreen next) public override void OnSuspending(ScreenTransitionEvent e)
{ {
base.OnSuspending(next); base.OnSuspending(e);
this.FadeOut(200); this.FadeOut(200);
} }
} }

View File

@ -16,6 +16,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Resources.Localisation.Web;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -68,7 +69,7 @@ namespace osu.Game.Overlays.AccountCreation
}, },
usernameTextBox = new OsuTextBox usernameTextBox = new OsuTextBox
{ {
PlaceholderText = "username", PlaceholderText = UsersStrings.LoginUsername,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
TabbableContentContainer = this TabbableContentContainer = this
}, },
@ -146,9 +147,9 @@ namespace osu.Game.Overlays.AccountCreation
d.Colour = password.Length == 0 ? Color4.White : Interpolation.ValueAt(password.Length, Color4.OrangeRed, Color4.YellowGreen, 0, 8, Easing.In); d.Colour = password.Length == 0 ? Color4.White : Interpolation.ValueAt(password.Length, Color4.OrangeRed, Color4.YellowGreen, 0, 8, Easing.In);
} }
public override void OnEntering(IScreen last) public override void OnEntering(ScreenTransitionEvent e)
{ {
base.OnEntering(last); base.OnEntering(e);
loadingLayer.Hide(); loadingLayer.Hide();
if (host?.OnScreenKeyboardOverlapsGameWindow != true) if (host?.OnScreenKeyboardOverlapsGameWindow != true)

View File

@ -31,7 +31,7 @@ namespace osu.Game.Overlays.AccountCreation
private const string help_centre_url = "/help/wiki/Help_Centre#login"; private const string help_centre_url = "/help/wiki/Help_Centre#login";
public override void OnEntering(IScreen last) public override void OnEntering(ScreenTransitionEvent e)
{ {
if (string.IsNullOrEmpty(api?.ProvidedUsername) || game?.UseDevelopmentServer == true) if (string.IsNullOrEmpty(api?.ProvidedUsername) || game?.UseDevelopmentServer == true)
{ {
@ -40,7 +40,7 @@ namespace osu.Game.Overlays.AccountCreation
return; return;
} }
base.OnEntering(last); base.OnEntering(e);
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Overlays.BeatmapSet namespace osu.Game.Overlays.BeatmapSet
@ -69,14 +70,14 @@ namespace osu.Game.Overlays.BeatmapSet
{ {
textContainer.Clear(); textContainer.Clear();
textContainer.AddParagraph(downloadDisabled textContainer.AddParagraph(downloadDisabled
? "This beatmap is currently not available for download." ? BeatmapsetsStrings.AvailabilityDisabled
: "Portions of this beatmap have been removed at the request of the creator or a third-party rights holder.", t => t.Colour = Color4.Orange); : BeatmapsetsStrings.AvailabilityPartsRemoved, t => t.Colour = Color4.Orange);
if (hasExternalLink) if (hasExternalLink)
{ {
textContainer.NewParagraph(); textContainer.NewParagraph();
textContainer.NewParagraph(); textContainer.NewParagraph();
textContainer.AddLink("Check here for more information.", BeatmapSet.Availability.ExternalLink, creationParameters: t => t.Font = OsuFont.GetFont(size: 10)); textContainer.AddLink(BeatmapsetsStrings.AvailabilityMoreInfo, BeatmapSet.Availability.ExternalLink, creationParameters: t => t.Font = OsuFont.GetFont(size: 10));
} }
} }
} }

View File

@ -41,7 +41,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(IAPIProvider api, NotificationOverlay notifications) private void load(IAPIProvider api, INotificationOverlay notifications)
{ {
SpriteIcon icon; SpriteIcon icon;

View File

@ -7,6 +7,7 @@ using osu.Game.Graphics.Sprites;
using osuTK; using osuTK;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapSet.Scores namespace osu.Game.Overlays.BeatmapSet.Scores
{ {
@ -28,7 +29,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
Text = @"You need to be an osu!supporter to access the friend and country rankings!", Text = BeatmapsetsStrings.ShowScoreboardSupporterOnly,
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold), Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold),
}, },
text = new LinkFlowContainer(t => t.Font = t.Font.With(size: 11)) text = new LinkFlowContainer(t => t.Font = t.Font.With(size: 11))

View File

@ -0,0 +1,135 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Chat;
namespace osu.Game.Overlays.Chat.ChannelList
{
public class ChannelList : Container
{
public Action<Channel>? OnRequestSelect;
public Action<Channel>? OnRequestLeave;
public readonly BindableBool SelectorActive = new BindableBool();
private readonly Dictionary<Channel, ChannelListItem> channelMap = new Dictionary<Channel, ChannelListItem>();
private ChannelListItemFlow publicChannelFlow = null!;
private ChannelListItemFlow privateChannelFlow = null!;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background6,
},
new OsuScrollContainer
{
Padding = new MarginPadding { Vertical = 7 },
RelativeSizeAxes = Axes.Both,
ScrollbarAnchor = Anchor.TopRight,
ScrollDistance = 35f,
Child = new FillFlowContainer
{
Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
publicChannelFlow = new ChannelListItemFlow("CHANNELS"),
new ChannelListSelector
{
Margin = new MarginPadding { Bottom = 10 },
SelectorActive = { BindTarget = SelectorActive },
},
privateChannelFlow = new ChannelListItemFlow("DIRECT MESSAGES"),
},
},
},
};
}
public void AddChannel(Channel channel)
{
if (channelMap.ContainsKey(channel))
return;
ChannelListItem item = new ChannelListItem(channel);
item.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan);
item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan);
item.SelectorActive.BindTarget = SelectorActive;
ChannelListItemFlow flow = getFlowForChannel(channel);
channelMap.Add(channel, item);
flow.Add(item);
}
public void RemoveChannel(Channel channel)
{
if (!channelMap.ContainsKey(channel))
return;
ChannelListItem item = channelMap[channel];
ChannelListItemFlow flow = getFlowForChannel(channel);
channelMap.Remove(channel);
flow.Remove(item);
}
public ChannelListItem GetItem(Channel channel)
{
if (!channelMap.ContainsKey(channel))
throw new ArgumentOutOfRangeException();
return channelMap[channel];
}
private ChannelListItemFlow getFlowForChannel(Channel channel)
{
switch (channel.Type)
{
case ChannelType.Public:
return publicChannelFlow;
case ChannelType.PM:
return privateChannelFlow;
default:
throw new ArgumentOutOfRangeException();
}
}
private class ChannelListItemFlow : FillFlowContainer
{
public ChannelListItemFlow(string label)
{
Direction = FillDirection.Vertical;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Add(new OsuSpriteText
{
Text = label,
Margin = new MarginPadding { Left = 18, Bottom = 5 },
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold),
});
}
}
}
}

View File

@ -29,12 +29,14 @@ namespace osu.Game.Overlays.Chat.ChannelList
public readonly BindableBool Unread = new BindableBool(); public readonly BindableBool Unread = new BindableBool();
public readonly BindableBool SelectorActive = new BindableBool();
private readonly Channel channel; private readonly Channel channel;
private Box? hoverBox; private Box hoverBox = null!;
private Box? selectBox; private Box selectBox = null!;
private OsuSpriteText? text; private OsuSpriteText text = null!;
private ChannelListItemCloseButton? close; private ChannelListItemCloseButton close = null!;
[Resolved] [Resolved]
private Bindable<Channel> selectedChannel { get; set; } = null!; private Bindable<Channel> selectedChannel { get; set; } = null!;
@ -124,31 +126,26 @@ namespace osu.Game.Overlays.Chat.ChannelList
{ {
base.LoadComplete(); base.LoadComplete();
selectedChannel.BindValueChanged(change => selectedChannel.BindValueChanged(_ => updateSelectState(), true);
{ SelectorActive.BindValueChanged(_ => updateSelectState(), true);
if (change.NewValue == channel)
selectBox?.FadeIn(300, Easing.OutQuint);
else
selectBox?.FadeOut(200, Easing.OutQuint);
}, true);
Unread.BindValueChanged(change => Unread.BindValueChanged(change =>
{ {
text!.FadeColour(change.NewValue ? colourProvider.Content1 : colourProvider.Light3, 300, Easing.OutQuint); text.FadeColour(change.NewValue ? colourProvider.Content1 : colourProvider.Light3, 300, Easing.OutQuint);
}, true); }, true);
} }
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
hoverBox?.FadeIn(300, Easing.OutQuint); hoverBox.FadeIn(300, Easing.OutQuint);
close?.FadeIn(300, Easing.OutQuint); close.FadeIn(300, Easing.OutQuint);
return base.OnHover(e); return base.OnHover(e);
} }
protected override void OnHoverLost(HoverLostEvent e) protected override void OnHoverLost(HoverLostEvent e)
{ {
hoverBox?.FadeOut(200, Easing.OutQuint); hoverBox.FadeOut(200, Easing.OutQuint);
close?.FadeOut(200, Easing.OutQuint); close.FadeOut(200, Easing.OutQuint);
base.OnHoverLost(e); base.OnHoverLost(e);
} }
@ -167,5 +164,13 @@ namespace osu.Game.Overlays.Chat.ChannelList
Masking = true, Masking = true,
}; };
} }
private void updateSelectState()
{
if (selectedChannel.Value == channel && !SelectorActive.Value)
selectBox.FadeIn(300, Easing.OutQuint);
else
selectBox.FadeOut(200, Easing.OutQuint);
}
} }
} }

View File

@ -0,0 +1,91 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Overlays.Chat.ChannelList
{
public class ChannelListSelector : OsuClickableContainer
{
public readonly BindableBool SelectorActive = new BindableBool();
private Box hoverBox = null!;
private Box selectBox = null!;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Height = 30;
RelativeSizeAxes = Axes.X;
Children = new Drawable[]
{
hoverBox = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background3,
Alpha = 0f,
},
selectBox = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
Alpha = 0f,
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = 18, Right = 10 },
Child = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = "Add More Channels",
Font = OsuFont.Torus.With(size: 17, weight: FontWeight.SemiBold),
Colour = colourProvider.Light3,
Margin = new MarginPadding { Bottom = 2 },
RelativeSizeAxes = Axes.X,
Truncate = true,
},
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
SelectorActive.BindValueChanged(selector =>
{
if (selector.NewValue)
selectBox.FadeIn(300, Easing.OutQuint);
else
selectBox.FadeOut(200, Easing.OutQuint);
}, true);
Action = () => SelectorActive.Value = true;
}
protected override bool OnHover(HoverEvent e)
{
hoverBox.FadeIn(300, Easing.OutQuint);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
hoverBox.FadeOut(200, Easing.OutQuint);
base.OnHoverLost(e);
}
}
}

View File

@ -160,7 +160,7 @@ namespace osu.Game.Overlays
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Height = 1, Height = 1,
PlaceholderText = "type your message", PlaceholderText = Resources.Localisation.Web.ChatStrings.InputPlaceholder,
ReleaseFocusOnCommit = false, ReleaseFocusOnCommit = false,
HoldFocus = true, HoldFocus = true,
} }

View File

@ -3,6 +3,7 @@
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Comments.Buttons namespace osu.Game.Overlays.Comments.Buttons
{ {
@ -25,7 +26,7 @@ namespace osu.Game.Overlays.Comments.Buttons
{ {
public ButtonContent() public ButtonContent()
{ {
Text = "load replies"; Text = CommentsStrings.LoadReplies;
} }
} }
} }

View File

@ -9,6 +9,7 @@ using osu.Game.Graphics.Sprites;
using System.Collections.Generic; using System.Collections.Generic;
using osuTK; using osuTK;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Comments.Buttons namespace osu.Game.Overlays.Comments.Buttons
{ {
@ -38,7 +39,7 @@ namespace osu.Game.Overlays.Comments.Buttons
{ {
AlwaysPresent = true, AlwaysPresent = true,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
Text = "show more" Text = CommonStrings.ButtonsShowMore
} }
}; };

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Comments namespace osu.Game.Overlays.Comments
{ {
@ -54,7 +55,7 @@ namespace osu.Game.Overlays.Comments
Origin = Anchor.Centre, Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
Margin = new MarginPadding { Horizontal = 20 }, Margin = new MarginPadding { Horizontal = 20 },
Text = @"Cancel" Text = CommonStrings.ButtonsCancel
} }
} }
}; };

View File

@ -16,6 +16,7 @@ using osu.Framework.Threading;
using System.Collections.Generic; using System.Collections.Generic;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
using APIUser = osu.Game.Online.API.Requests.Responses.APIUser; using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
namespace osu.Game.Overlays.Comments namespace osu.Game.Overlays.Comments
@ -328,7 +329,7 @@ namespace osu.Game.Overlays.Comments
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Left = 50 }, Margin = new MarginPadding { Left = 50 },
Text = @"No comments yet." Text = CommentsStrings.Empty
} }
}); });
} }

View File

@ -12,7 +12,9 @@ using osu.Game.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osuTK; using osuTK;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Comments namespace osu.Game.Overlays.Comments
{ {
@ -91,7 +93,7 @@ namespace osu.Game.Overlays.Comments
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
Text = @"Show deleted" Text = CommonStrings.ButtonsShowDeleted
} }
}, },
}); });
@ -126,9 +128,13 @@ namespace osu.Game.Overlays.Comments
public enum CommentsSortCriteria public enum CommentsSortCriteria
{ {
[System.ComponentModel.Description(@"Recent")] [LocalisableDescription(typeof(SortStrings), nameof(SortStrings.New))]
New, New,
[LocalisableDescription(typeof(SortStrings), nameof(SortStrings.Old))]
Old, Old,
[LocalisableDescription(typeof(SortStrings), nameof(SortStrings.Top))]
Top Top
} }
} }

View File

@ -2,7 +2,10 @@
// 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.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Comments namespace osu.Game.Overlays.Comments
{ {
@ -18,7 +21,8 @@ namespace osu.Game.Overlays.Comments
private void onCurrentChanged(ValueChangedEvent<int> count) private void onCurrentChanged(ValueChangedEvent<int> count)
{ {
Text = $@"Show More ({count.NewValue})".ToUpper(); Text = new TranslatableString(@"_", "{0} ({1})",
CommonStrings.ButtonsShowMore.ToUpper(), count.NewValue);
} }
} }
} }

View File

@ -150,7 +150,7 @@ namespace osu.Game.Overlays.Comments
{ {
Alpha = Comment.IsDeleted ? 1 : 0, Alpha = Comment.IsDeleted ? 1 : 0,
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold), Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold),
Text = "deleted" Text = CommentsStrings.Deleted
} }
} }
}, },

View File

@ -9,6 +9,7 @@ using osuTK;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Comments namespace osu.Game.Overlays.Comments
{ {
@ -39,7 +40,7 @@ namespace osu.Game.Overlays.Comments
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: 20, italics: true), Font = OsuFont.GetFont(size: 20, italics: true),
Colour = colourProvider.Light1, Colour = colourProvider.Light1,
Text = @"Comments" Text = CommentsStrings.Title
}, },
new CircularContainer new CircularContainer
{ {

View File

@ -14,6 +14,7 @@ using osu.Game.Database;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Spectator; using osu.Game.Online.Spectator;
using osu.Game.Screens;
using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Users; using osu.Game.Users;
@ -106,7 +107,7 @@ namespace osu.Game.Overlays.Dashboard
public readonly APIUser User; public readonly APIUser User;
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private OsuGame game { get; set; } private IPerformFromScreenRunner performer { get; set; }
public PlayingUserPanel(APIUser user) public PlayingUserPanel(APIUser user)
{ {
@ -137,10 +138,10 @@ namespace osu.Game.Overlays.Dashboard
new PurpleTriangleButton new PurpleTriangleButton
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Text = "Watch", Text = "Spectate",
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
Action = () => game?.PerformFromScreen(s => s.Push(new SoloSpectator(User))), Action = () => performer?.PerformFromScreen(s => s.Push(new SoloSpectator(User))),
Enabled = { Value = User.Id != api.LocalUser.Value.Id } Enabled = { Value = User.Id != api.LocalUser.Value.Id }
} }
} }

View File

@ -6,6 +6,7 @@ using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
@ -49,7 +50,7 @@ namespace osu.Game.Overlays.Dashboard.Home
flow.AddRange(beatmapSets.Select(CreateBeatmapPanel)); flow.AddRange(beatmapSets.Select(CreateBeatmapPanel));
} }
protected abstract string Title { get; } protected abstract LocalisableString Title { get; }
protected abstract DashboardBeatmapPanel CreateBeatmapPanel(APIBeatmapSet beatmapSet); protected abstract DashboardBeatmapPanel CreateBeatmapPanel(APIBeatmapSet beatmapSet);
} }

View File

@ -2,7 +2,9 @@
// 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.Collections.Generic;
using osu.Framework.Localisation;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Dashboard.Home namespace osu.Game.Overlays.Dashboard.Home
{ {
@ -15,6 +17,6 @@ namespace osu.Game.Overlays.Dashboard.Home
protected override DashboardBeatmapPanel CreateBeatmapPanel(APIBeatmapSet beatmapSet) => new DashboardNewBeatmapPanel(beatmapSet); protected override DashboardBeatmapPanel CreateBeatmapPanel(APIBeatmapSet beatmapSet) => new DashboardNewBeatmapPanel(beatmapSet);
protected override string Title => "New Ranked Beatmaps"; protected override LocalisableString Title => HomeStrings.UserBeatmapsNew;
} }
} }

View File

@ -2,7 +2,9 @@
// 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.Collections.Generic;
using osu.Framework.Localisation;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Dashboard.Home namespace osu.Game.Overlays.Dashboard.Home
{ {
@ -15,6 +17,6 @@ namespace osu.Game.Overlays.Dashboard.Home
protected override DashboardBeatmapPanel CreateBeatmapPanel(APIBeatmapSet beatmapSet) => new DashboardPopularBeatmapPanel(beatmapSet); protected override DashboardBeatmapPanel CreateBeatmapPanel(APIBeatmapSet beatmapSet) => new DashboardPopularBeatmapPanel(beatmapSet);
protected override string Title => "Popular Beatmaps"; protected override LocalisableString Title => HomeStrings.UserBeatmapsPopular;
} }
} }

View File

@ -6,6 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Overlays.Dashboard.Home.News namespace osu.Game.Overlays.Dashboard.Home.News
@ -35,7 +36,7 @@ namespace osu.Game.Overlays.Dashboard.Home.News
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Margin = new MarginPadding { Vertical = 20 }, Margin = new MarginPadding { Vertical = 20 },
Text = "see more" Text = CommonStrings.ButtonsSeeMore
} }
}; };

View File

@ -3,6 +3,7 @@
using System; using System;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Dialog namespace osu.Game.Overlays.Dialog
{ {
@ -33,7 +34,7 @@ namespace osu.Game.Overlays.Dialog
}, },
new PopupDialogCancelButton new PopupDialogCancelButton
{ {
Text = Localisation.CommonStrings.Cancel, Text = CommonStrings.ButtonsCancel,
Action = onCancel Action = onCancel
}, },
}; };

View File

@ -88,9 +88,13 @@ namespace osu.Game.Overlays.Dialog
if (actionInvoked) return; if (actionInvoked) return;
actionInvoked = true; actionInvoked = true;
action?.Invoke();
// Hide the dialog before running the action.
// This is important as the code which is performed may check for a dialog being present (ie. `OsuGame.PerformFromScreen`)
// and we don't want it to see the already dismissed dialog.
Hide(); Hide();
action?.Invoke();
}; };
} }
} }
@ -212,7 +216,7 @@ namespace osu.Game.Overlays.Dialog
}; };
// It's important we start in a visible state so our state fires on hide, even before load. // It's important we start in a visible state so our state fires on hide, even before load.
// This is used by the DialogOverlay to know when the dialog was dismissed. // This is used by the dialog overlay to know when the dialog was dismissed.
Show(); Show();
} }

View File

@ -14,7 +14,7 @@ using osu.Game.Audio.Effects;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
public class DialogOverlay : OsuFocusedOverlayContainer public class DialogOverlay : OsuFocusedOverlayContainer, IDialogOverlay
{ {
private readonly Container dialogContainer; private readonly Container dialogContainer;

View File

@ -0,0 +1,32 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using osu.Framework.Allocation;
using osu.Game.Overlays.Dialog;
namespace osu.Game.Overlays
{
/// <summary>
/// A global overlay that can show popup dialogs.
/// </summary>
[Cached(typeof(IDialogOverlay))]
public interface IDialogOverlay
{
/// <summary>
/// Push a new dialog for display.
/// </summary>
/// <remarks>
/// This will immediate dismiss any already displayed dialog (cancelling the action).
/// If the dialog instance provided is already displayed, it will be a noop.
/// </remarks>
/// <param name="dialog">The dialog to be presented.</param>
void Push(PopupDialog dialog);
/// <summary>
/// The currently displayed dialog, if any.
/// </summary>
PopupDialog? CurrentDialog { get; }
}
}

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