diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index b51ecb4f7e..b3f7c67c51 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -2,12 +2,6 @@
"version": 1,
"isRoot": true,
"tools": {
- "cake.tool": {
- "version": "0.35.0",
- "commands": [
- "dotnet-cake"
- ]
- },
"dotnet-format": {
"version": "3.1.37601",
"commands": [
@@ -20,20 +14,20 @@
"jb"
]
},
- "nvika": {
- "version": "2.0.0",
+ "smoogipoo.nvika": {
+ "version": "1.0.1",
"commands": [
"nvika"
]
},
"codefilesanity": {
- "version": "15.0.0",
+ "version": "0.0.36",
"commands": [
"CodeFileSanity"
]
},
"ppy.localisationanalyser.tools": {
- "version": "2021.524.0",
+ "version": "2021.608.0",
"commands": [
"localisation"
]
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000000..ed3e99cb61
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,93 @@
+on: [push, pull_request]
+name: Continuous Integration
+
+jobs:
+ test:
+ name: Test
+ runs-on: ${{matrix.os.fullname}}
+ env:
+ OSU_EXECUTION_MODE: ${{matrix.threadingMode}}
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - { prettyname: Windows, fullname: windows-latest }
+ - { prettyname: macOS, fullname: macos-latest }
+ - { prettyname: Linux, fullname: ubuntu-latest }
+ threadingMode: ['SingleThread', 'MultiThreaded']
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Install .NET 5.0.x
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: "5.0.x"
+
+ # FIXME: libavformat is not included in Ubuntu. Let's fix that.
+ # https://github.com/ppy/osu-framework/issues/4349
+ # Remove this once https://github.com/actions/virtual-environments/issues/3306 has been resolved.
+ - name: Install libavformat-dev
+ if: ${{matrix.os.fullname == 'ubuntu-latest'}}
+ run: |
+ sudo apt-get update && \
+ sudo apt-get -y install libavformat-dev
+
+ - name: Compile
+ run: dotnet build -c Debug -warnaserror osu.Desktop.slnf
+
+ - name: Test
+ run: dotnet test $pwd/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx"
+ shell: pwsh
+
+ # Attempt to upload results even if test fails.
+ # https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always
+ - name: Upload Test Results
+ uses: actions/upload-artifact@v2
+ if: ${{ always() }}
+ with:
+ name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
+ path: ${{github.workspace}}/TestResults/TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx
+
+ inspect-code:
+ name: Code Quality
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ # FIXME: Tools won't run in .NET 5.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 5.0.x
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: "5.0.x"
+
+ - name: Restore Tools
+ run: dotnet tool restore
+
+ - name: Restore Packages
+ run: dotnet restore
+
+ - 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
+ dotnet codefilesanity | while read -r line; do
+ echo "::warning::$line"
+ done
+
+ # 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 --output=$(pwd)/inspectcodereport.xml --cachesDir=$(pwd)/inspectcode --verbosity=WARN
+
+ - name: NVika
+ run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors
diff --git a/.github/workflows/report-nunit.yml b/.github/workflows/report-nunit.yml
new file mode 100644
index 0000000000..381d2d49c5
--- /dev/null
+++ b/.github/workflows/report-nunit.yml
@@ -0,0 +1,31 @@
+# This is a workaround to allow PRs to report their coverage. This will run inside the base repository.
+# See:
+# * https://github.com/dorny/test-reporter#recommended-setup-for-public-repositories
+# * https://docs.github.com/en/actions/reference/authentication-in-a-workflow#permissions-for-the-github_token
+name: Annotate CI run with test results
+on:
+ workflow_run:
+ workflows: ["Continuous Integration"]
+ types:
+ - completed
+jobs:
+ annotate:
+ name: Annotate CI run with test results
+ runs-on: ubuntu-latest
+ if: ${{ github.event.workflow_run.conclusion != 'cancelled' }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os:
+ - { prettyname: Windows }
+ - { prettyname: macOS }
+ - { prettyname: Linux }
+ threadingMode: ['SingleThread', 'MultiThreaded']
+ steps:
+ - name: Annotate CI run with test results
+ uses: dorny/test-reporter@v1.4.2
+ with:
+ artifact: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
+ name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}})
+ path: "*.trx"
+ reporter: dotnet-trx
diff --git a/.vscode/launch.json b/.vscode/launch.json
index afd997f91d..1b590008cd 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -113,20 +113,6 @@
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build benchmarks",
"console": "internalConsole"
- },
- {
- "name": "Cake: Debug Script",
- "type": "coreclr",
- "request": "launch",
- "program": "${workspaceRoot}/build/tools/Cake.CoreCLR/0.30.0/Cake.dll",
- "args": [
- "${workspaceRoot}/build/build.cake",
- "--debug",
- "--verbosity=diagnostic"
- ],
- "cwd": "${workspaceRoot}/build",
- "stopAtEntry": true,
- "externalConsole": false
}
]
}
diff --git a/InspectCode.ps1 b/InspectCode.ps1
index 6ed935fdbb..8316f48ff3 100644
--- a/InspectCode.ps1
+++ b/InspectCode.ps1
@@ -1,27 +1,11 @@
-[CmdletBinding()]
-Param(
- [string]$Target,
- [string]$Configuration,
- [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")]
- [string]$Verbosity,
- [switch]$ShowDescription,
- [Alias("WhatIf", "Noop")]
- [switch]$DryRun,
- [Parameter(Position = 0, Mandatory = $false, ValueFromRemainingArguments = $true)]
- [string[]]$ScriptArgs
-)
-
-# Build Cake arguments
-$cakeArguments = "";
-if ($Target) { $cakeArguments += "-target=$Target" }
-if ($Configuration) { $cakeArguments += "-configuration=$Configuration" }
-if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" }
-if ($ShowDescription) { $cakeArguments += "-showdescription" }
-if ($DryRun) { $cakeArguments += "-dryrun" }
-if ($Experimental) { $cakeArguments += "-experimental" }
-$cakeArguments += $ScriptArgs
-
dotnet tool restore
-dotnet cake ./build/InspectCode.cake --bootstrap
-dotnet cake ./build/InspectCode.cake $cakeArguments
-exit $LASTEXITCODE
\ No newline at end of file
+
+# Temporarily disabled until the tool is upgraded to 5.0.
+ # The version specified in .config/dotnet-tools.json (3.1.37601) won't run on .NET hosts >=5.0.7.
+ # - cmd: dotnet format --dry-run --check
+
+dotnet CodeFileSanity
+dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
+dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors
+
+exit $LASTEXITCODE
diff --git a/InspectCode.sh b/InspectCode.sh
new file mode 100755
index 0000000000..cf2bc18175
--- /dev/null
+++ b/InspectCode.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+dotnet tool restore
+dotnet CodeFileSanity
+dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
+dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors
diff --git a/appveyor.yml b/appveyor.yml
index a4a0cedc66..5be73f9875 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,24 +1,27 @@
clone_depth: 1
version: '{branch}-{build}'
image: Visual Studio 2019
+cache:
+ - '%LOCALAPPDATA%\NuGet\v3-cache -> appveyor.yml'
+
dotnet_csproj:
patch: true
file: 'osu.Game\osu.Game.csproj' # Use wildcard when it's able to exclude Xamarin projects
version: '0.0.{build}'
-cache:
- - '%LOCALAPPDATA%\NuGet\v3-cache -> appveyor.yml'
+
before_build:
- - ps: dotnet --info # Useful when version mismatch between CI and local
- - ps: nuget restore -verbosity quiet # Only nuget.exe knows both new (.NET Core) and old (Xamarin) projects
+ - cmd: dotnet --info # Useful when version mismatch between CI and local
+ - cmd: nuget restore -verbosity quiet # Only nuget.exe knows both new (.NET Core) and old (Xamarin) projects
+
build:
project: osu.sln
parallel: true
verbosity: minimal
publish_nuget: true
+
after_build:
- - ps: dotnet tool restore
- - ps: dotnet format --dry-run --check
- ps: .\InspectCode.ps1
+
test:
assemblies:
except:
diff --git a/build/Desktop.proj b/build/Desktop.proj
deleted file mode 100644
index b1c6b065e8..0000000000
--- a/build/Desktop.proj
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/build/InspectCode.cake b/build/InspectCode.cake
deleted file mode 100644
index 6836d9071b..0000000000
--- a/build/InspectCode.cake
+++ /dev/null
@@ -1,41 +0,0 @@
-#addin "nuget:?package=CodeFileSanity&version=0.0.36"
-
-///////////////////////////////////////////////////////////////////////////////
-// ARGUMENTS
-///////////////////////////////////////////////////////////////////////////////
-
-var target = Argument("target", "CodeAnalysis");
-var configuration = Argument("configuration", "Release");
-
-var rootDirectory = new DirectoryPath("..");
-var sln = rootDirectory.CombineWithFilePath("osu.sln");
-var desktopSlnf = rootDirectory.CombineWithFilePath("osu.Desktop.slnf");
-
-///////////////////////////////////////////////////////////////////////////////
-// TASKS
-///////////////////////////////////////////////////////////////////////////////
-
-Task("InspectCode")
- .Does(() => {
- var inspectcodereport = "inspectcodereport.xml";
- var cacheDir = "inspectcode";
- var verbosity = AppVeyor.IsRunningOnAppVeyor ? "WARN" : "INFO"; // Don't flood CI output
-
- DotNetCoreTool(rootDirectory.FullPath,
- "jb", $@"inspectcode ""{desktopSlnf}"" --output=""{inspectcodereport}"" --caches-home=""{cacheDir}"" --verbosity={verbosity}");
- DotNetCoreTool(rootDirectory.FullPath, "nvika", $@"parsereport ""{inspectcodereport}"" --treatwarningsaserrors");
- });
-
-Task("CodeFileSanity")
- .Does(() => {
- ValidateCodeSanity(new ValidateCodeSanitySettings {
- RootDirectory = rootDirectory.FullPath,
- IsAppveyorBuild = AppVeyor.IsRunningOnAppVeyor
- });
- });
-
-Task("CodeAnalysis")
- .IsDependentOn("CodeFileSanity")
- .IsDependentOn("InspectCode");
-
-RunTarget(target);
\ No newline at end of file
diff --git a/cake.config b/cake.config
deleted file mode 100644
index 187d825591..0000000000
--- a/cake.config
+++ /dev/null
@@ -1,5 +0,0 @@
-
-[Nuget]
-Source=https://api.nuget.org/v3/index.json
-UseInProcessClient=true
-LoadDependencies=true
diff --git a/osu.Android.props b/osu.Android.props
index 0d3fafd19f..490e43b5e6 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
-
+
+
diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/skin.ini b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/skin.ini
new file mode 100644
index 0000000000..94c6b5b58d
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/skin.ini
@@ -0,0 +1,2 @@
+[General]
+// no version specified means v1
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
index 1e42c6a240..73b60f51a4 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
@@ -33,13 +33,13 @@ namespace osu.Game.Rulesets.Catch.Mods
private class MouseInputHelper : Drawable, IKeyBindingHandler, IRequireHighFrequencyMousePosition
{
- private readonly Catcher catcher;
+ private readonly CatcherArea catcherArea;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
public MouseInputHelper(CatchPlayfield playfield)
{
- catcher = playfield.CatcherArea.MovableCatcher;
+ catcherArea = playfield.CatcherArea;
RelativeSizeAxes = Axes.Both;
}
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Mods
protected override bool OnMouseMove(MouseMoveEvent e)
{
- catcher.UpdatePosition(e.MousePosition.X / DrawSize.X * CatchPlayfield.WIDTH);
+ catcherArea.SetCatcherPosition(e.MousePosition.X / DrawSize.X * CatchPlayfield.WIDTH);
return base.OnMouseMove(e);
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index 4af2243ed4..ee2986c73c 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -10,7 +10,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Textures;
-using osu.Framework.Input.Bindings;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
@@ -26,7 +25,7 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.UI
{
- public class Catcher : SkinReloadableDrawable, IKeyBindingHandler
+ public class Catcher : SkinReloadableDrawable
{
///
/// The default colour used to tint hyper-dash fruit, along with the moving catcher, its trail
@@ -54,6 +53,11 @@ namespace osu.Game.Rulesets.Catch.UI
///
public const double BASE_SPEED = 1.0;
+ ///
+ /// The current speed of the catcher.
+ ///
+ public double Speed => (Dashing ? 1 : 0.5) * BASE_SPEED * hyperDashModifier;
+
///
/// The amount by which caught fruit should be offset from the plate surface to make them look visually "caught".
///
@@ -96,7 +100,7 @@ namespace osu.Game.Rulesets.Catch.UI
public bool Dashing
{
get => dashing;
- protected set
+ set
{
if (value == dashing) return;
@@ -106,6 +110,12 @@ namespace osu.Game.Rulesets.Catch.UI
}
}
+ public Direction VisualDirection
+ {
+ get => Scale.X > 0 ? Direction.Right : Direction.Left;
+ set => Scale = new Vector2((value == Direction.Right ? 1 : -1) * Math.Abs(Scale.X), Scale.Y);
+ }
+
///
/// Width of the area that can be used to attempt catches during gameplay.
///
@@ -116,8 +126,6 @@ namespace osu.Game.Rulesets.Catch.UI
private Color4 hyperDashColour = DEFAULT_HYPER_DASH_COLOUR;
private Color4 hyperDashEndGlowColour = DEFAULT_HYPER_DASH_COLOUR;
- private int currentDirection;
-
private double hyperDashModifier = 1;
private int hyperDashDirection;
private float hyperDashTargetPosition;
@@ -315,55 +323,6 @@ namespace osu.Game.Rulesets.Catch.UI
}
}
- public void UpdatePosition(float position)
- {
- position = Math.Clamp(position, 0, CatchPlayfield.WIDTH);
-
- if (position == X)
- return;
-
- Scale = new Vector2(Math.Abs(Scale.X) * (position > X ? 1 : -1), Scale.Y);
- X = position;
- }
-
- public bool OnPressed(CatchAction action)
- {
- switch (action)
- {
- case CatchAction.MoveLeft:
- currentDirection--;
- return true;
-
- case CatchAction.MoveRight:
- currentDirection++;
- return true;
-
- case CatchAction.Dash:
- Dashing = true;
- return true;
- }
-
- return false;
- }
-
- public void OnReleased(CatchAction action)
- {
- switch (action)
- {
- case CatchAction.MoveLeft:
- currentDirection++;
- break;
-
- case CatchAction.MoveRight:
- currentDirection--;
- break;
-
- case CatchAction.Dash:
- Dashing = false;
- break;
- }
- }
-
///
/// Drop any fruit off the plate.
///
@@ -405,15 +364,6 @@ namespace osu.Game.Rulesets.Catch.UI
{
base.Update();
- if (currentDirection == 0) return;
-
- var direction = Math.Sign(currentDirection);
-
- var dashModifier = Dashing ? 1 : 0.5;
- var speed = BASE_SPEED * dashModifier * hyperDashModifier;
-
- UpdatePosition((float)(X + direction * Clock.ElapsedFrameTime * speed));
-
// Correct overshooting.
if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
(hyperDashDirection < 0 && hyperDashTargetPosition > X))
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index 44adbd5512..cdb15c2b4c 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -1,8 +1,10 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Bindings;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects.Drawables;
@@ -14,13 +16,20 @@ using osuTK;
namespace osu.Game.Rulesets.Catch.UI
{
- public class CatcherArea : Container
+ public class CatcherArea : Container, IKeyBindingHandler
{
public const float CATCHER_SIZE = 106.75f;
public readonly Catcher MovableCatcher;
private readonly CatchComboDisplay comboDisplay;
+ ///
+ /// -1 when only left button is pressed.
+ /// 1 when only right button is pressed.
+ /// 0 when none or both left and right buttons are pressed.
+ ///
+ private int currentDirection;
+
public CatcherArea(Container droppedObjectContainer, BeatmapDifficulty difficulty = null)
{
Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
@@ -63,16 +72,73 @@ namespace osu.Game.Rulesets.Catch.UI
MovableCatcher.OnRevertResult(hitObject, result);
}
+ protected override void Update()
+ {
+ base.Update();
+
+ var replayState = (GetContainingInputManager().CurrentState as RulesetInputManagerInputState)?.LastReplayState as CatchFramedReplayInputHandler.CatchReplayState;
+
+ SetCatcherPosition(
+ replayState?.CatcherX ??
+ (float)(MovableCatcher.X + MovableCatcher.Speed * currentDirection * Clock.ElapsedFrameTime));
+ }
+
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
- var state = (GetContainingInputManager().CurrentState as RulesetInputManagerInputState)?.LastReplayState as CatchFramedReplayInputHandler.CatchReplayState;
-
- if (state?.CatcherX != null)
- MovableCatcher.X = state.CatcherX.Value;
-
comboDisplay.X = MovableCatcher.X;
}
+
+ public void SetCatcherPosition(float X)
+ {
+ float lastPosition = MovableCatcher.X;
+ float newPosition = Math.Clamp(X, 0, CatchPlayfield.WIDTH);
+
+ MovableCatcher.X = newPosition;
+
+ if (lastPosition < newPosition)
+ MovableCatcher.VisualDirection = Direction.Right;
+ else if (lastPosition > newPosition)
+ MovableCatcher.VisualDirection = Direction.Left;
+ }
+
+ public bool OnPressed(CatchAction action)
+ {
+ switch (action)
+ {
+ case CatchAction.MoveLeft:
+ currentDirection--;
+ return true;
+
+ case CatchAction.MoveRight:
+ currentDirection++;
+ return true;
+
+ case CatchAction.Dash:
+ MovableCatcher.Dashing = true;
+ return true;
+ }
+
+ return false;
+ }
+
+ public void OnReleased(CatchAction action)
+ {
+ switch (action)
+ {
+ case CatchAction.MoveLeft:
+ currentDirection++;
+ break;
+
+ case CatchAction.MoveRight:
+ currentDirection--;
+ break;
+
+ case CatchAction.Dash:
+ MovableCatcher.Dashing = false;
+ break;
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/Direction.cs b/osu.Game.Rulesets.Catch/UI/Direction.cs
new file mode 100644
index 0000000000..65f064b7fb
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/UI/Direction.cs
@@ -0,0 +1,11 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Rulesets.Catch.UI
+{
+ public enum Direction
+ {
+ Right = 1,
+ Left = -1
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini
index 89bcd68343..06dfa6b7be 100644
--- a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini
+++ b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/skin.ini
@@ -1,6 +1,6 @@
[General]
-Version: 1.0
+// no version specified means v1
[Fonts]
HitCircleOverlap: 3
-ScoreOverlap: 3
\ No newline at end of file
+ScoreOverlap: 3
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
index 19cb55c16e..d80e061662 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs
@@ -12,6 +12,7 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Skinning;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
@@ -40,6 +41,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-background"),
+ Colour = source.GetConfig(OsuSkinColour.SpinnerBackground)?.Value ?? new Color4(100, 100, 100, 255),
Scale = new Vector2(SPRITE_SCALE),
Y = SPINNER_Y_CENTRE,
},
diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs
index 4e6d3ef0e4..f7ba8b9fc4 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinColour.cs
@@ -7,6 +7,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
SliderTrackOverride,
SliderBorder,
- SliderBall
+ SliderBall,
+ SpinnerBackground,
}
}
diff --git a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs
index a47631a83b..8f5ebf53bd 100644
--- a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs
+++ b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs
@@ -113,7 +113,6 @@ namespace osu.Game.Tests.Collections.IO
await importCollectionsFromStream(osu, ms);
}
- Assert.That(host.UpdateThread.Running, Is.True);
Assert.That(exceptionThrown, Is.False);
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(0));
}
diff --git a/osu.Game.Tests/Resources/old-skin/skin.ini b/osu.Game.Tests/Resources/old-skin/skin.ini
index 5369de24e9..94c6b5b58d 100644
--- a/osu.Game.Tests/Resources/old-skin/skin.ini
+++ b/osu.Game.Tests/Resources/old-skin/skin.ini
@@ -1,2 +1,2 @@
[General]
-Version: 1.0
\ No newline at end of file
+// no version specified means v1
diff --git a/osu.Game.Tests/Resources/special-skin/scorebar-bg.png b/osu.Game.Tests/Resources/special-skin/scorebar-bg.png
new file mode 100644
index 0000000000..1a25274ed8
Binary files /dev/null and b/osu.Game.Tests/Resources/special-skin/scorebar-bg.png differ
diff --git a/osu.Game.Tests/Resources/special-skin/scorebar-colour-0.png b/osu.Game.Tests/Resources/special-skin/scorebar-colour-0.png
new file mode 100644
index 0000000000..3c15449b03
Binary files /dev/null and b/osu.Game.Tests/Resources/special-skin/scorebar-colour-0.png differ
diff --git a/osu.Game.Tests/Resources/special-skin/scorebar-colour-1.png b/osu.Game.Tests/Resources/special-skin/scorebar-colour-1.png
new file mode 100644
index 0000000000..a444723ef4
Binary files /dev/null and b/osu.Game.Tests/Resources/special-skin/scorebar-colour-1.png differ
diff --git a/osu.Game.Tests/Resources/special-skin/scorebar-colour-2.png b/osu.Game.Tests/Resources/special-skin/scorebar-colour-2.png
new file mode 100644
index 0000000000..e1c6b41d9b
Binary files /dev/null and b/osu.Game.Tests/Resources/special-skin/scorebar-colour-2.png differ
diff --git a/osu.Game.Tests/Resources/special-skin/scorebar-colour-3.png b/osu.Game.Tests/Resources/special-skin/scorebar-colour-3.png
new file mode 100644
index 0000000000..a3a5ca4716
Binary files /dev/null and b/osu.Game.Tests/Resources/special-skin/scorebar-colour-3.png differ
diff --git a/osu.Game.Tests/Resources/special-skin/scorebar-marker.png b/osu.Game.Tests/Resources/special-skin/scorebar-marker.png
new file mode 100644
index 0000000000..b5af0b2148
Binary files /dev/null and b/osu.Game.Tests/Resources/special-skin/scorebar-marker.png differ
diff --git a/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs b/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs
index dc5a4f4a3e..0bd1263076 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs
@@ -161,15 +161,18 @@ namespace osu.Game.Tests.Visual.Background
private void loadNextBackground()
{
+ SeasonalBackground previousBackground = null;
SeasonalBackground background = null;
AddStep("create next background", () =>
{
+ previousBackground = (SeasonalBackground)backgroundContainer.SingleOrDefault();
background = backgroundLoader.LoadNextBackground();
LoadComponentAsync(background, bg => backgroundContainer.Child = bg);
});
AddUntilStep("background loaded", () => background.IsLoaded);
+ AddAssert("background is different", () => !background.Equals(previousBackground));
}
private void assertAnyBackground()
diff --git a/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs
new file mode 100644
index 0000000000..d193856217
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneMessageNotifier.cs
@@ -0,0 +1,240 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Chat;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Notifications;
+using osu.Game.Users;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ public class TestSceneMessageNotifier : OsuManualInputManagerTestScene
+ {
+ private User friend;
+ private Channel publicChannel;
+ private Channel privateMessageChannel;
+ private TestContainer testContainer;
+
+ private int messageIdCounter;
+
+ [SetUp]
+ public void Setup()
+ {
+ if (API is DummyAPIAccess daa)
+ {
+ daa.HandleRequest = dummyAPIHandleRequest;
+ }
+
+ friend = new User { Id = 0, Username = "Friend" };
+ publicChannel = new Channel { Id = 1, Name = "osu" };
+ privateMessageChannel = new Channel(friend) { Id = 2, Name = friend.Username, Type = ChannelType.PM };
+
+ Schedule(() =>
+ {
+ Child = testContainer = new TestContainer(new[] { publicChannel, privateMessageChannel })
+ {
+ RelativeSizeAxes = Axes.Both,
+ };
+
+ testContainer.ChatOverlay.Show();
+ });
+ }
+
+ private bool dummyAPIHandleRequest(APIRequest request)
+ {
+ switch (request)
+ {
+ case GetMessagesRequest messagesRequest:
+ messagesRequest.TriggerSuccess(new List(0));
+ return true;
+
+ case CreateChannelRequest createChannelRequest:
+ var apiChatChannel = new APIChatChannel
+ {
+ RecentMessages = new List(0),
+ ChannelID = (int)createChannelRequest.Channel.Id
+ };
+ createChannelRequest.TriggerSuccess(apiChatChannel);
+ return true;
+
+ case ListChannelsRequest listChannelsRequest:
+ listChannelsRequest.TriggerSuccess(new List(1) { publicChannel });
+ return true;
+
+ case GetUpdatesRequest updatesRequest:
+ updatesRequest.TriggerSuccess(new GetUpdatesResponse
+ {
+ Messages = new List(0),
+ Presence = new List(0)
+ });
+ return true;
+
+ case JoinChannelRequest joinChannelRequest:
+ joinChannelRequest.TriggerSuccess();
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ [Test]
+ public void TestPublicChannelMention()
+ {
+ AddStep("switch to PMs", () => testContainer.ChannelManager.CurrentChannel.Value = privateMessageChannel);
+
+ AddStep("receive public message", () => receiveMessage(friend, publicChannel, "Hello everyone"));
+ AddAssert("no notifications fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 0);
+
+ AddStep("receive message containing mention", () => receiveMessage(friend, publicChannel, $"Hello {API.LocalUser.Value.Username.ToLowerInvariant()}!"));
+ AddAssert("1 notification fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 1);
+
+ AddStep("open notification overlay", () => testContainer.NotificationOverlay.Show());
+ AddStep("click notification", clickNotification);
+
+ AddAssert("chat overlay is open", () => testContainer.ChatOverlay.State.Value == Visibility.Visible);
+ AddAssert("public channel is selected", () => testContainer.ChannelManager.CurrentChannel.Value == publicChannel);
+ }
+
+ [Test]
+ public void TestPrivateMessageNotification()
+ {
+ AddStep("switch to public channel", () => testContainer.ChannelManager.CurrentChannel.Value = publicChannel);
+
+ AddStep("receive PM", () => receiveMessage(friend, privateMessageChannel, $"Hello {API.LocalUser.Value.Username}"));
+ AddAssert("1 notification fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 1);
+
+ AddStep("open notification overlay", () => testContainer.NotificationOverlay.Show());
+ AddStep("click notification", clickNotification);
+
+ AddAssert("chat overlay is open", () => testContainer.ChatOverlay.State.Value == Visibility.Visible);
+ AddAssert("PM channel is selected", () => testContainer.ChannelManager.CurrentChannel.Value == privateMessageChannel);
+ }
+
+ [Test]
+ public void TestNoNotificationWhenPMChannelOpen()
+ {
+ AddStep("switch to PMs", () => testContainer.ChannelManager.CurrentChannel.Value = privateMessageChannel);
+
+ AddStep("receive PM", () => receiveMessage(friend, privateMessageChannel, "you're reading this, right?"));
+
+ AddAssert("no notifications fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 0);
+ }
+
+ [Test]
+ public void TestNoNotificationWhenMentionedInOpenPublicChannel()
+ {
+ AddStep("switch to public channel", () => testContainer.ChannelManager.CurrentChannel.Value = publicChannel);
+
+ AddStep("receive mention", () => receiveMessage(friend, publicChannel, $"{API.LocalUser.Value.Username.ToUpperInvariant()} has been reading this"));
+
+ AddAssert("no notifications fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 0);
+ }
+
+ [Test]
+ public void TestNoNotificationOnSelfMention()
+ {
+ AddStep("switch to PM channel", () => testContainer.ChannelManager.CurrentChannel.Value = privateMessageChannel);
+
+ AddStep("receive self-mention", () => receiveMessage(API.LocalUser.Value, publicChannel, $"my name is {API.LocalUser.Value.Username}"));
+
+ AddAssert("no notifications fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 0);
+ }
+
+ [Test]
+ public void TestNoNotificationOnPMFromSelf()
+ {
+ AddStep("switch to public channel", () => testContainer.ChannelManager.CurrentChannel.Value = publicChannel);
+
+ AddStep("receive PM from self", () => receiveMessage(API.LocalUser.Value, privateMessageChannel, "hey hey"));
+
+ AddAssert("no notifications fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 0);
+ }
+
+ [Test]
+ public void TestNotificationsNotFiredTwice()
+ {
+ AddStep("switch to public channel", () => testContainer.ChannelManager.CurrentChannel.Value = publicChannel);
+
+ AddStep("receive same PM twice", () =>
+ {
+ var message = createMessage(friend, privateMessageChannel, "hey hey");
+ privateMessageChannel.AddNewMessages(message, message);
+ });
+
+ AddStep("open notification overlay", () => testContainer.NotificationOverlay.Show());
+ AddAssert("1 notification fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 1);
+ }
+
+ private void receiveMessage(User sender, Channel channel, string content) => channel.AddNewMessages(createMessage(sender, channel, content));
+
+ private Message createMessage(User sender, Channel channel, string content) => new Message(messageIdCounter++)
+ {
+ Content = content,
+ Sender = sender,
+ ChannelId = channel.Id
+ };
+
+ private void clickNotification() where T : Notification
+ {
+ var notification = testContainer.NotificationOverlay.ChildrenOfType().Single();
+
+ InputManager.MoveMouseTo(notification);
+ InputManager.Click(MouseButton.Left);
+ }
+
+ private class TestContainer : Container
+ {
+ [Cached]
+ public ChannelManager ChannelManager { get; } = new ChannelManager();
+
+ [Cached]
+ public NotificationOverlay NotificationOverlay { get; } = new NotificationOverlay
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ };
+
+ [Cached]
+ public ChatOverlay ChatOverlay { get; } = new ChatOverlay();
+
+ private readonly MessageNotifier messageNotifier = new MessageNotifier();
+
+ private readonly Channel[] channels;
+
+ public TestContainer(Channel[] channels)
+ {
+ this.channels = channels;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Children = new Drawable[]
+ {
+ ChannelManager,
+ ChatOverlay,
+ NotificationOverlay,
+ messageNotifier,
+ };
+
+ ((BindableList)ChannelManager.AvailableChannels).AddRange(channels);
+
+ foreach (var channel in channels)
+ ChannelManager.JoinChannel(channel);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
index 67cd720260..184a2e59da 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
@@ -2,15 +2,22 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
+using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Audio;
using osu.Framework.Graphics;
+using osu.Framework.Platform;
+using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays;
+using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Select.Leaderboards;
+using osu.Game.Tests.Resources;
using osu.Game.Users;
using osuTK;
@@ -23,32 +30,98 @@ namespace osu.Game.Tests.Visual.SongSelect
[Cached]
private readonly DialogOverlay dialogOverlay;
+ private ScoreManager scoreManager;
+
+ private RulesetStore rulesetStore;
+ private BeatmapManager beatmapManager;
+
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
+ {
+ var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
+
+ dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory));
+ dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default));
+ dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory));
+
+ return dependencies;
+ }
+
public TestSceneBeatmapLeaderboard()
{
- Add(dialogOverlay = new DialogOverlay
+ AddRange(new Drawable[]
{
- Depth = -1
+ dialogOverlay = new DialogOverlay
+ {
+ Depth = -1
+ },
+ leaderboard = new FailableLeaderboard
+ {
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ Size = new Vector2(550f, 450f),
+ Scope = BeatmapLeaderboardScope.Global,
+ }
+ });
+ }
+
+ [Test]
+ public void TestLocalScoresDisplay()
+ {
+ BeatmapInfo beatmapInfo = null;
+
+ AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Local);
+
+ AddStep(@"Set beatmap", () =>
+ {
+ beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
+ beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
+
+ leaderboard.Beatmap = beatmapInfo;
});
- Add(leaderboard = new FailableLeaderboard
- {
- Origin = Anchor.Centre,
- Anchor = Anchor.Centre,
- Size = new Vector2(550f, 450f),
- Scope = BeatmapLeaderboardScope.Global,
- });
+ clearScores();
+ checkCount(0);
- AddStep(@"New Scores", newScores);
+ loadMoreScores(() => beatmapInfo);
+ checkCount(10);
+
+ loadMoreScores(() => beatmapInfo);
+ checkCount(20);
+
+ clearScores();
+ checkCount(0);
+ }
+
+ [Test]
+ public void TestGlobalScoresDisplay()
+ {
+ AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Global);
+ AddStep(@"New Scores", () => leaderboard.Scores = generateSampleScores(null));
+ }
+
+ [Test]
+ public void TestPersonalBest()
+ {
AddStep(@"Show personal best", showPersonalBest);
+ AddStep("null personal best position", showPersonalBestWithNullPosition);
+ }
+
+ [Test]
+ public void TestPlaceholderStates()
+ {
AddStep(@"Empty Scores", () => leaderboard.SetRetrievalState(PlaceholderState.NoScores));
AddStep(@"Network failure", () => leaderboard.SetRetrievalState(PlaceholderState.NetworkFailure));
AddStep(@"No supporter", () => leaderboard.SetRetrievalState(PlaceholderState.NotSupporter));
AddStep(@"Not logged in", () => leaderboard.SetRetrievalState(PlaceholderState.NotLoggedIn));
AddStep(@"Unavailable", () => leaderboard.SetRetrievalState(PlaceholderState.Unavailable));
AddStep(@"None selected", () => leaderboard.SetRetrievalState(PlaceholderState.NoneSelected));
+ }
+
+ [Test]
+ public void TestBeatmapStates()
+ {
foreach (BeatmapSetOnlineStatus status in Enum.GetValues(typeof(BeatmapSetOnlineStatus)))
AddStep($"{status} beatmap", () => showBeatmapWithStatus(status));
- AddStep("null personal best position", showPersonalBestWithNullPosition);
}
private void showPersonalBestWithNullPosition()
@@ -96,9 +169,26 @@ namespace osu.Game.Tests.Visual.SongSelect
};
}
- private void newScores()
+ private void loadMoreScores(Func beatmapInfo)
{
- var scores = new[]
+ AddStep(@"Load new scores via manager", () =>
+ {
+ foreach (var score in generateSampleScores(beatmapInfo()))
+ scoreManager.Import(score).Wait();
+ });
+ }
+
+ private void clearScores()
+ {
+ AddStep("Clear all scores", () => scoreManager.Delete(scoreManager.GetAllUsableScores()));
+ }
+
+ private void checkCount(int expected) =>
+ AddUntilStep("Correct count displayed", () => leaderboard.ChildrenOfType().Count() == expected);
+
+ private static ScoreInfo[] generateSampleScores(BeatmapInfo beatmap)
+ {
+ return new[]
{
new ScoreInfo
{
@@ -107,6 +197,7 @@ namespace osu.Game.Tests.Visual.SongSelect
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
+ Beatmap = beatmap,
User = new User
{
Id = 6602580,
@@ -125,6 +216,7 @@ namespace osu.Game.Tests.Visual.SongSelect
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
+ Beatmap = beatmap,
User = new User
{
Id = 4608074,
@@ -143,6 +235,7 @@ namespace osu.Game.Tests.Visual.SongSelect
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
+ Beatmap = beatmap,
User = new User
{
Id = 1014222,
@@ -161,6 +254,7 @@ namespace osu.Game.Tests.Visual.SongSelect
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
+ Beatmap = beatmap,
User = new User
{
Id = 1541390,
@@ -179,6 +273,7 @@ namespace osu.Game.Tests.Visual.SongSelect
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
+ Beatmap = beatmap,
User = new User
{
Id = 2243452,
@@ -197,6 +292,7 @@ namespace osu.Game.Tests.Visual.SongSelect
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
+ Beatmap = beatmap,
User = new User
{
Id = 2705430,
@@ -215,6 +311,7 @@ namespace osu.Game.Tests.Visual.SongSelect
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
+ Beatmap = beatmap,
User = new User
{
Id = 7151382,
@@ -233,6 +330,7 @@ namespace osu.Game.Tests.Visual.SongSelect
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
+ Beatmap = beatmap,
User = new User
{
Id = 2051389,
@@ -251,6 +349,7 @@ namespace osu.Game.Tests.Visual.SongSelect
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
+ Beatmap = beatmap,
User = new User
{
Id = 6169483,
@@ -269,6 +368,7 @@ namespace osu.Game.Tests.Visual.SongSelect
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
+ Beatmap = beatmap,
User = new User
{
Id = 6702666,
@@ -281,8 +381,6 @@ namespace osu.Game.Tests.Visual.SongSelect
},
},
};
-
- leaderboard.Scores = scores;
}
private void showBeatmapWithStatus(BeatmapSetOnlineStatus status)
diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs
index 340c47d89b..ca910e70b8 100644
--- a/osu.Game/Beatmaps/DifficultyRecommender.cs
+++ b/osu.Game/Beatmaps/DifficultyRecommender.cs
@@ -101,10 +101,20 @@ namespace osu.Game.Beatmaps
/// Rulesets ordered descending by their respective recommended difficulties.
/// The currently selected ruleset will always be first.
///
- private IEnumerable orderedRulesets =>
- recommendedDifficultyMapping
- .OrderByDescending(pair => pair.Value).Select(pair => pair.Key).Where(r => !r.Equals(ruleset.Value))
- .Prepend(ruleset.Value);
+ private IEnumerable orderedRulesets
+ {
+ get
+ {
+ if (LoadState < LoadState.Ready || ruleset.Value == null)
+ return Enumerable.Empty();
+
+ return recommendedDifficultyMapping
+ .OrderByDescending(pair => pair.Value)
+ .Select(pair => pair.Key)
+ .Where(r => !r.Equals(ruleset.Value))
+ .Prepend(ruleset.Value);
+ }
+ }
private void onlineStateChanged(ValueChangedEvent state) => Schedule(() =>
{
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index 43bbd725c3..60a0d5a0ac 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -61,6 +61,9 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.ShowOnlineExplicitContent, false);
+ SetDefault(OsuSetting.NotifyOnUsernameMentioned, true);
+ SetDefault(OsuSetting.NotifyOnPrivateMessage, true);
+
// Audio
SetDefault(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01);
@@ -259,6 +262,8 @@ namespace osu.Game.Configuration
ScalingSizeY,
UIScale,
IntroSequence,
+ NotifyOnUsernameMentioned,
+ NotifyOnPrivateMessage,
UIHoldActivationDelay,
HitLighting,
MenuBackgroundSource,
diff --git a/osu.Game/Configuration/RankingType.cs b/osu.Game/Configuration/RankingType.cs
deleted file mode 100644
index 7701e1dd1d..0000000000
--- a/osu.Game/Configuration/RankingType.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System.ComponentModel;
-
-namespace osu.Game.Configuration
-{
- public enum RankingType
- {
- Local,
-
- [Description("Global")]
- Top,
-
- [Description("Selected Mods")]
- SelectedMod,
- Friends,
- Country
- }
-}
diff --git a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs
index a48da37804..f01a26a3a8 100644
--- a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs
+++ b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs
@@ -99,5 +99,14 @@ namespace osu.Game.Graphics.Backgrounds
// ensure we're not loading in without a transition.
this.FadeInFromZero(200, Easing.InOutSine);
}
+
+ public override bool Equals(Background other)
+ {
+ if (ReferenceEquals(null, other)) return false;
+ if (ReferenceEquals(this, other)) return true;
+
+ return other.GetType() == GetType()
+ && ((SeasonalBackground)other).url == url;
+ }
}
}
diff --git a/osu.Game/Graphics/Containers/OsuClickableContainer.cs b/osu.Game/Graphics/Containers/OsuClickableContainer.cs
index 1f31e4cdda..60ded8952d 100644
--- a/osu.Game/Graphics/Containers/OsuClickableContainer.cs
+++ b/osu.Game/Graphics/Containers/OsuClickableContainer.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Graphics.Containers
protected virtual HoverClickSounds CreateHoverClickSounds(HoverSampleSet sampleSet) => new HoverClickSounds(sampleSet);
- public OsuClickableContainer(HoverSampleSet sampleSet = HoverSampleSet.Normal)
+ public OsuClickableContainer(HoverSampleSet sampleSet = HoverSampleSet.Default)
{
this.sampleSet = sampleSet;
}
diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs
index c0518247a9..b9b098df80 100644
--- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs
+++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs
@@ -107,10 +107,10 @@ namespace osu.Game.Graphics.Containers
{
}
- private bool playedPopInSound;
-
protected override void UpdateState(ValueChangedEvent state)
{
+ bool didChange = state.NewValue != state.OldValue;
+
switch (state.NewValue)
{
case Visibility.Visible:
@@ -121,18 +121,15 @@ namespace osu.Game.Graphics.Containers
return;
}
- samplePopIn?.Play();
- playedPopInSound = true;
+ if (didChange)
+ samplePopIn?.Play();
if (BlockScreenWideMouse && DimMainContent) game?.AddBlockingOverlay(this);
break;
case Visibility.Hidden:
- if (playedPopInSound)
- {
+ if (didChange)
samplePopOut?.Play();
- playedPopInSound = false;
- }
if (BlockScreenWideMouse) game?.RemoveBlockingOverlay(this);
break;
diff --git a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs
index 8df2c1c2fd..fea84998cf 100644
--- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs
+++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs
@@ -3,7 +3,6 @@
using osu.Framework.Allocation;
using osu.Framework.Audio;
-using osu.Framework.Audio.Sample;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -23,9 +22,6 @@ namespace osu.Game.Graphics.UserInterface
private const int text_size = 17;
private const int transition_length = 80;
- private Sample sampleClick;
- private Sample sampleHover;
-
private TextContainer text;
public DrawableOsuMenuItem(MenuItem item)
@@ -36,12 +32,11 @@ namespace osu.Game.Graphics.UserInterface
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
- sampleHover = audio.Samples.Get(@"UI/generic-hover");
- sampleClick = audio.Samples.Get(@"UI/generic-select");
-
BackgroundColour = Color4.Transparent;
BackgroundColourHover = Color4Extensions.FromHex(@"172023");
+ AddInternal(new HoverClickSounds());
+
updateTextColour();
Item.Action.BindDisabledChanged(_ => updateState(), true);
@@ -84,7 +79,6 @@ namespace osu.Game.Graphics.UserInterface
if (IsHovered && !Item.Action.Disabled)
{
- sampleHover.Play();
text.BoldText.FadeIn(transition_length, Easing.OutQuint);
text.NormalText.FadeOut(transition_length, Easing.OutQuint);
}
@@ -95,12 +89,6 @@ namespace osu.Game.Graphics.UserInterface
}
}
- protected override bool OnClick(ClickEvent e)
- {
- sampleClick.Play();
- return base.OnClick(e);
- }
-
protected sealed override Drawable CreateContent() => text = CreateTextContainer();
protected virtual TextContainer CreateTextContainer() => new TextContainer();
diff --git a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs
index c1963ce62d..12819840e5 100644
--- a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs
+++ b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Graphics.UserInterface
/// Array of button codes which should trigger the click sound.
/// If this optional parameter is omitted or set to null
, the click sound will only be played on left click.
///
- public HoverClickSounds(HoverSampleSet sampleSet = HoverSampleSet.Normal, MouseButton[] buttons = null)
+ public HoverClickSounds(HoverSampleSet sampleSet = HoverSampleSet.Default, MouseButton[] buttons = null)
: base(sampleSet)
{
this.buttons = buttons ?? new[] { MouseButton.Left };
@@ -45,7 +45,8 @@ namespace osu.Game.Graphics.UserInterface
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
- sampleClick = audio.Samples.Get($@"UI/generic-select{SampleSet.GetDescription()}");
+ sampleClick = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-select")
+ ?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select");
}
}
}
diff --git a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs
new file mode 100644
index 0000000000..c74ac90a4c
--- /dev/null
+++ b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs
@@ -0,0 +1,25 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.ComponentModel;
+
+namespace osu.Game.Graphics.UserInterface
+{
+ public enum HoverSampleSet
+ {
+ [Description("default")]
+ Default,
+
+ [Description("button")]
+ Button,
+
+ [Description("softer")]
+ Soft,
+
+ [Description("toolbar")]
+ Toolbar,
+
+ [Description("songselect")]
+ SongSelect
+ }
+}
diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs
index f2e4c6d013..c0ef5cb3fc 100644
--- a/osu.Game/Graphics/UserInterface/HoverSounds.cs
+++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.ComponentModel;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@@ -22,7 +21,7 @@ namespace osu.Game.Graphics.UserInterface
protected readonly HoverSampleSet SampleSet;
- public HoverSounds(HoverSampleSet sampleSet = HoverSampleSet.Normal)
+ public HoverSounds(HoverSampleSet sampleSet = HoverSampleSet.Default)
{
SampleSet = sampleSet;
RelativeSizeAxes = Axes.Both;
@@ -31,7 +30,8 @@ namespace osu.Game.Graphics.UserInterface
[BackgroundDependencyLoader]
private void load(AudioManager audio, SessionStatics statics)
{
- sampleHover = audio.Samples.Get($@"UI/generic-hover{SampleSet.GetDescription()}");
+ sampleHover = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-hover")
+ ?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-hover");
}
public override void PlayHoverSample()
@@ -40,22 +40,4 @@ namespace osu.Game.Graphics.UserInterface
sampleHover.Play();
}
}
-
- public enum HoverSampleSet
- {
- [Description("")]
- Loud,
-
- [Description("-soft")]
- Normal,
-
- [Description("-softer")]
- Soft,
-
- [Description("-toolbar")]
- Toolbar,
-
- [Description("-songselect")]
- SongSelect
- }
}
diff --git a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs
index cfcf034d1c..70a107ca04 100644
--- a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs
+++ b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs
@@ -44,6 +44,7 @@ namespace osu.Game.Graphics.UserInterface
private readonly Box hover;
public OsuAnimatedButton()
+ : base(HoverSampleSet.Button)
{
base.Content.Add(content = new Container
{
diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs
index a22c837080..cd9ca9f87f 100644
--- a/osu.Game/Graphics/UserInterface/OsuButton.cs
+++ b/osu.Game/Graphics/UserInterface/OsuButton.cs
@@ -49,7 +49,7 @@ namespace osu.Game.Graphics.UserInterface
protected Box Background;
protected SpriteText SpriteText;
- public OsuButton(HoverSampleSet? hoverSounds = HoverSampleSet.Loud)
+ public OsuButton(HoverSampleSet? hoverSounds = HoverSampleSet.Button)
{
Height = 40;
diff --git a/osu.Game/Localisation/ChatStrings.cs b/osu.Game/Localisation/ChatStrings.cs
index daddb602ad..636351470b 100644
--- a/osu.Game/Localisation/ChatStrings.cs
+++ b/osu.Game/Localisation/ChatStrings.cs
@@ -7,17 +7,17 @@ namespace osu.Game.Localisation
{
public static class ChatStrings
{
- private const string prefix = "osu.Game.Localisation.Chat";
+ private const string prefix = @"osu.Game.Localisation.Chat";
///
/// "chat"
///
- public static LocalisableString HeaderTitle => new TranslatableString(getKey("header_title"), "chat");
+ public static LocalisableString HeaderTitle => new TranslatableString(getKey(@"header_title"), @"chat");
///
/// "join the real-time discussion"
///
- public static LocalisableString HeaderDescription => new TranslatableString(getKey("header_description"), "join the real-time discussion");
+ public static LocalisableString HeaderDescription => new TranslatableString(getKey(@"header_description"), @"join the real-time discussion");
private static string getKey(string key) => $"{prefix}:{key}";
}
diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs
index f448158191..ced0d80955 100644
--- a/osu.Game/Localisation/CommonStrings.cs
+++ b/osu.Game/Localisation/CommonStrings.cs
@@ -7,12 +7,12 @@ namespace osu.Game.Localisation
{
public static class CommonStrings
{
- private const string prefix = "osu.Game.Localisation.Common";
+ private const string prefix = @"osu.Game.Localisation.Common";
///
/// "Cancel"
///
- public static LocalisableString Cancel => new TranslatableString(getKey("cancel"), "Cancel");
+ public static LocalisableString Cancel => new TranslatableString(getKey(@"cancel"), @"Cancel");
private static string getKey(string key) => $"{prefix}:{key}";
}
diff --git a/osu.Game/Localisation/Language.cs b/osu.Game/Localisation/Language.cs
index edcf264c7f..a3e845f229 100644
--- a/osu.Game/Localisation/Language.cs
+++ b/osu.Game/Localisation/Language.cs
@@ -7,10 +7,10 @@ namespace osu.Game.Localisation
{
public enum Language
{
- [Description("English")]
+ [Description(@"English")]
en,
- [Description("日本語")]
+ [Description(@"日本語")]
ja
}
}
diff --git a/osu.Game/Localisation/NotificationsStrings.cs b/osu.Game/Localisation/NotificationsStrings.cs
index 092eec3a6b..ba28ef5560 100644
--- a/osu.Game/Localisation/NotificationsStrings.cs
+++ b/osu.Game/Localisation/NotificationsStrings.cs
@@ -7,17 +7,17 @@ namespace osu.Game.Localisation
{
public static class NotificationsStrings
{
- private const string prefix = "osu.Game.Localisation.Notifications";
+ private const string prefix = @"osu.Game.Localisation.Notifications";
///
/// "notifications"
///
- public static LocalisableString HeaderTitle => new TranslatableString(getKey("header_title"), "notifications");
+ public static LocalisableString HeaderTitle => new TranslatableString(getKey(@"header_title"), @"notifications");
///
/// "waiting for 'ya"
///
- public static LocalisableString HeaderDescription => new TranslatableString(getKey("header_description"), "waiting for 'ya");
+ public static LocalisableString HeaderDescription => new TranslatableString(getKey(@"header_description"), @"waiting for 'ya");
private static string getKey(string key) => $"{prefix}:{key}";
}
diff --git a/osu.Game/Localisation/NowPlayingStrings.cs b/osu.Game/Localisation/NowPlayingStrings.cs
index d742a56895..47646b0f68 100644
--- a/osu.Game/Localisation/NowPlayingStrings.cs
+++ b/osu.Game/Localisation/NowPlayingStrings.cs
@@ -7,17 +7,17 @@ namespace osu.Game.Localisation
{
public static class NowPlayingStrings
{
- private const string prefix = "osu.Game.Localisation.NowPlaying";
+ private const string prefix = @"osu.Game.Localisation.NowPlaying";
///
/// "now playing"
///
- public static LocalisableString HeaderTitle => new TranslatableString(getKey("header_title"), "now playing");
+ public static LocalisableString HeaderTitle => new TranslatableString(getKey(@"header_title"), @"now playing");
///
/// "manage the currently playing track"
///
- public static LocalisableString HeaderDescription => new TranslatableString(getKey("header_description"), "manage the currently playing track");
+ public static LocalisableString HeaderDescription => new TranslatableString(getKey(@"header_description"), @"manage the currently playing track");
private static string getKey(string key) => $"{prefix}:{key}";
}
diff --git a/osu.Game/Localisation/ResourceManagerLocalisationStore.cs b/osu.Game/Localisation/ResourceManagerLocalisationStore.cs
index 7b21e1af42..a35ce7a9c8 100644
--- a/osu.Game/Localisation/ResourceManagerLocalisationStore.cs
+++ b/osu.Game/Localisation/ResourceManagerLocalisationStore.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
+using System.Linq;
using System.Resources;
using System.Threading.Tasks;
using osu.Framework.Localisation;
@@ -34,7 +35,29 @@ namespace osu.Game.Localisation
lock (resourceManagers)
{
if (!resourceManagers.TryGetValue(ns, out var manager))
- resourceManagers[ns] = manager = new ResourceManager(ns, GetType().Assembly);
+ {
+ var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
+
+ // Traverse backwards through periods in the namespace to find a matching assembly.
+ string assemblyName = ns;
+
+ while (!string.IsNullOrEmpty(assemblyName))
+ {
+ var matchingAssembly = loadedAssemblies.FirstOrDefault(asm => asm.GetName().Name == assemblyName);
+
+ if (matchingAssembly != null)
+ {
+ resourceManagers[ns] = manager = new ResourceManager(ns, matchingAssembly);
+ break;
+ }
+
+ int lastIndex = Math.Max(0, assemblyName.LastIndexOf('.'));
+ assemblyName = assemblyName.Substring(0, lastIndex);
+ }
+ }
+
+ if (manager == null)
+ return null;
try
{
diff --git a/osu.Game/Localisation/SettingsStrings.cs b/osu.Game/Localisation/SettingsStrings.cs
index cfbd392691..f4b417fa28 100644
--- a/osu.Game/Localisation/SettingsStrings.cs
+++ b/osu.Game/Localisation/SettingsStrings.cs
@@ -7,17 +7,17 @@ namespace osu.Game.Localisation
{
public static class SettingsStrings
{
- private const string prefix = "osu.Game.Localisation.Settings";
+ private const string prefix = @"osu.Game.Localisation.Settings";
///
/// "settings"
///
- public static LocalisableString HeaderTitle => new TranslatableString(getKey("header_title"), "settings");
+ public static LocalisableString HeaderTitle => new TranslatableString(getKey(@"header_title"), @"settings");
///
/// "change the way osu! behaves"
///
- public static LocalisableString HeaderDescription => new TranslatableString(getKey("header_description"), "change the way osu! behaves");
+ public static LocalisableString HeaderDescription => new TranslatableString(getKey(@"header_description"), @"change the way osu! behaves");
private static string getKey(string key) => $"{prefix}:{key}";
}
diff --git a/osu.Game/Online/API/Requests/CreateChannelRequest.cs b/osu.Game/Online/API/Requests/CreateChannelRequest.cs
index 42cb201969..041ad26267 100644
--- a/osu.Game/Online/API/Requests/CreateChannelRequest.cs
+++ b/osu.Game/Online/API/Requests/CreateChannelRequest.cs
@@ -11,11 +11,11 @@ namespace osu.Game.Online.API.Requests
{
public class CreateChannelRequest : APIRequest
{
- private readonly Channel channel;
+ public readonly Channel Channel;
public CreateChannelRequest(Channel channel)
{
- this.channel = channel;
+ Channel = channel;
}
protected override WebRequest CreateWebRequest()
@@ -24,7 +24,7 @@ namespace osu.Game.Online.API.Requests
req.Method = HttpMethod.Post;
req.AddParameter("type", $"{ChannelType.PM}");
- req.AddParameter("target_id", $"{channel.Users.First().Id}");
+ req.AddParameter("target_id", $"{Channel.Users.First().Id}");
return req;
}
diff --git a/osu.Game/Online/Chat/Message.cs b/osu.Game/Online/Chat/Message.cs
index 30753b3920..4f33153e56 100644
--- a/osu.Game/Online/Chat/Message.cs
+++ b/osu.Game/Online/Chat/Message.cs
@@ -63,5 +63,7 @@ namespace osu.Game.Online.Chat
// ReSharper disable once ImpureMethodCallOnReadonlyValueField
public override int GetHashCode() => Id.GetHashCode();
+
+ public override string ToString() => $"[{ChannelId}] ({Id}) {Sender}: {Content}";
}
}
diff --git a/osu.Game/Online/Chat/MessageNotifier.cs b/osu.Game/Online/Chat/MessageNotifier.cs
new file mode 100644
index 0000000000..6840c036ff
--- /dev/null
+++ b/osu.Game/Online/Chat/MessageNotifier.cs
@@ -0,0 +1,181 @@
+// Copyright (c) ppy Pty Ltd . 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.Collections.Specialized;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Configuration;
+using osu.Game.Graphics;
+using osu.Game.Online.API;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Notifications;
+using osu.Game.Users;
+
+namespace osu.Game.Online.Chat
+{
+ ///
+ /// Component that handles creating and posting notifications for incoming messages.
+ ///
+ public class MessageNotifier : Component
+ {
+ [Resolved]
+ private NotificationOverlay notifications { get; set; }
+
+ [Resolved]
+ private ChatOverlay chatOverlay { get; set; }
+
+ [Resolved]
+ private ChannelManager channelManager { get; set; }
+
+ private Bindable notifyOnUsername;
+ private Bindable notifyOnPrivateMessage;
+
+ private readonly IBindable localUser = new Bindable();
+ private readonly IBindableList joinedChannels = new BindableList();
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config, IAPIProvider api)
+ {
+ notifyOnUsername = config.GetBindable(OsuSetting.NotifyOnUsernameMentioned);
+ notifyOnPrivateMessage = config.GetBindable(OsuSetting.NotifyOnPrivateMessage);
+
+ localUser.BindTo(api.LocalUser);
+ joinedChannels.BindTo(channelManager.JoinedChannels);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ joinedChannels.BindCollectionChanged(channelsChanged, true);
+ }
+
+ private void channelsChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ switch (e.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ foreach (var channel in e.NewItems.Cast())
+ channel.NewMessagesArrived += checkNewMessages;
+
+ break;
+
+ case NotifyCollectionChangedAction.Remove:
+ foreach (var channel in e.OldItems.Cast())
+ channel.NewMessagesArrived -= checkNewMessages;
+
+ break;
+ }
+ }
+
+ private void checkNewMessages(IEnumerable messages)
+ {
+ if (!messages.Any())
+ return;
+
+ var channel = channelManager.JoinedChannels.SingleOrDefault(c => c.Id == messages.First().ChannelId);
+
+ if (channel == null)
+ return;
+
+ // Only send notifications, if ChatOverlay and the target channel aren't visible.
+ if (chatOverlay.IsPresent && channelManager.CurrentChannel.Value == channel)
+ return;
+
+ foreach (var message in messages.OrderByDescending(m => m.Id))
+ {
+ // ignore messages that already have been read
+ if (message.Id <= channel.LastReadId)
+ return;
+
+ if (message.Sender.Id == localUser.Value.Id)
+ continue;
+
+ // check for private messages first to avoid both posting two notifications about the same message
+ if (checkForPMs(channel, message))
+ continue;
+
+ checkForMentions(channel, message);
+ }
+ }
+
+ ///
+ /// Checks whether the user enabled private message notifications and whether specified is a direct message.
+ ///
+ /// The channel associated to the
+ /// The message to be checked
+ /// Whether a notification was fired.
+ private bool checkForPMs(Channel channel, Message message)
+ {
+ if (!notifyOnPrivateMessage.Value || channel.Type != ChannelType.PM)
+ return false;
+
+ notifications.Post(new PrivateMessageNotification(message.Sender.Username, channel));
+ return true;
+ }
+
+ private void checkForMentions(Channel channel, Message message)
+ {
+ if (!notifyOnUsername.Value || !checkContainsUsername(message.Content, localUser.Value.Username)) return;
+
+ notifications.Post(new MentionNotification(message.Sender.Username, channel));
+ }
+
+ ///
+ /// Checks if contains .
+ /// This will match against the case where underscores are used instead of spaces (which is how osu-stable handles usernames with spaces).
+ ///
+ private static bool checkContainsUsername(string message, string username) => message.Contains(username, StringComparison.OrdinalIgnoreCase) || message.Contains(username.Replace(' ', '_'), StringComparison.OrdinalIgnoreCase);
+
+ public class PrivateMessageNotification : OpenChannelNotification
+ {
+ public PrivateMessageNotification(string username, Channel channel)
+ : base(channel)
+ {
+ Icon = FontAwesome.Solid.Envelope;
+ Text = $"You received a private message from '{username}'. Click to read it!";
+ }
+ }
+
+ public class MentionNotification : OpenChannelNotification
+ {
+ public MentionNotification(string username, Channel channel)
+ : base(channel)
+ {
+ Icon = FontAwesome.Solid.At;
+ Text = $"Your name was mentioned in chat by '{username}'. Click to find out why!";
+ }
+ }
+
+ public abstract class OpenChannelNotification : SimpleNotification
+ {
+ protected OpenChannelNotification(Channel channel)
+ {
+ this.channel = channel;
+ }
+
+ private readonly Channel channel;
+
+ public override bool IsImportant => false;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours, ChatOverlay chatOverlay, NotificationOverlay notificationOverlay, ChannelManager channelManager)
+ {
+ IconBackgound.Colour = colours.PurpleDark;
+
+ Activated = delegate
+ {
+ notificationOverlay.Hide();
+ chatOverlay.Show();
+ channelManager.CurrentChannel.Value = channel;
+
+ return true;
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs
index d18f189a70..70e38e421d 100644
--- a/osu.Game/Online/Leaderboards/Leaderboard.cs
+++ b/osu.Game/Online/Leaderboards/Leaderboard.cs
@@ -44,9 +44,9 @@ namespace osu.Game.Online.Leaderboards
protected override Container Content => content;
- private IEnumerable scores;
+ private ICollection scores;
- public IEnumerable Scores
+ public ICollection Scores
{
get => scores;
set
@@ -126,7 +126,7 @@ namespace osu.Game.Online.Leaderboards
return;
scope = value;
- UpdateScores();
+ RefreshScores();
}
}
@@ -154,7 +154,7 @@ namespace osu.Game.Online.Leaderboards
case PlaceholderState.NetworkFailure:
replacePlaceholder(new ClickablePlaceholder(@"Couldn't fetch scores!", FontAwesome.Solid.Sync)
{
- Action = UpdateScores,
+ Action = RefreshScores
});
break;
@@ -254,8 +254,6 @@ namespace osu.Game.Online.Leaderboards
apiState.BindValueChanged(onlineStateChanged, true);
}
- public void RefreshScores() => UpdateScores();
-
private APIRequest getScoresRequest;
protected abstract bool IsOnlineScope { get; }
@@ -267,12 +265,14 @@ namespace osu.Game.Online.Leaderboards
case APIState.Online:
case APIState.Offline:
if (IsOnlineScope)
- UpdateScores();
+ RefreshScores();
break;
}
});
+ public void RefreshScores() => Scheduler.AddOnce(UpdateScores);
+
protected void UpdateScores()
{
// don't display any scores or placeholder until the first Scores_Set has been called.
@@ -290,7 +290,7 @@ namespace osu.Game.Online.Leaderboards
getScoresRequest = FetchScores(scores => Schedule(() =>
{
- Scores = scores;
+ Scores = scores.ToArray();
PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores;
}));
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 2dca91cbf3..02e724a451 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -426,9 +426,12 @@ namespace osu.Game
{
// The given ScoreInfo may have missing properties if it was retrieved from online data. Re-retrieve it from the database
// to ensure all the required data for presenting a replay are present.
- var databasedScoreInfo = score.OnlineScoreID != null
- ? ScoreManager.Query(s => s.OnlineScoreID == score.OnlineScoreID)
- : ScoreManager.Query(s => s.Hash == score.Hash);
+ ScoreInfo databasedScoreInfo = null;
+
+ if (score.OnlineScoreID != null)
+ databasedScoreInfo = ScoreManager.Query(s => s.OnlineScoreID == score.OnlineScoreID);
+
+ databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == score.Hash);
if (databasedScoreInfo == null)
{
@@ -712,7 +715,6 @@ namespace osu.Game
PostNotification = n => notifications.Post(n),
}, Add, true);
- loadComponentSingleFile(difficultyRecommender, Add);
loadComponentSingleFile(stableImportManager, Add);
loadComponentSingleFile(screenshotManager, Add);
@@ -728,6 +730,7 @@ namespace osu.Game
var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true);
loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true);
loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true);
+ loadComponentSingleFile(new MessageNotifier(), AddInternal, true);
loadComponentSingleFile(Settings = new SettingsOverlay { GetToolbarHeight = () => ToolbarOffset }, leftFloatingOverlayContent.Add, true);
var changelogOverlay = loadComponentSingleFile(new ChangelogOverlay(), overlayContent.Add, true);
loadComponentSingleFile(userProfile = new UserProfileOverlay(), overlayContent.Add, true);
@@ -754,6 +757,7 @@ namespace osu.Game
chatOverlay.State.ValueChanged += state => channelManager.HighPollRate.Value = state.NewValue == Visibility.Visible;
+ Add(difficultyRecommender);
Add(externalLinkOpener = new ExternalLinkOpener());
Add(new MusicKeyBindingHandler());
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
index 97ccb66599..0626f236b8 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs
@@ -14,6 +14,7 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Resources.Localisation.Web;
using osuTK.Graphics;
using osu.Game.Rulesets;
using osu.Game.Scoring;
@@ -126,15 +127,15 @@ namespace osu.Game.Overlays.BeatmapListing
Padding = new MarginPadding { Horizontal = 10 },
Children = new Drawable[]
{
- generalFilter = new BeatmapSearchMultipleSelectionFilterRow(@"General"),
+ generalFilter = new BeatmapSearchMultipleSelectionFilterRow(BeatmapsStrings.ListingSearchFiltersGeneral),
modeFilter = new BeatmapSearchRulesetFilterRow(),
- categoryFilter = new BeatmapSearchFilterRow(@"Categories"),
- genreFilter = new BeatmapSearchFilterRow(@"Genre"),
- languageFilter = new BeatmapSearchFilterRow(@"Language"),
- extraFilter = new BeatmapSearchMultipleSelectionFilterRow(@"Extra"),
+ categoryFilter = new BeatmapSearchFilterRow(BeatmapsStrings.ListingSearchFiltersStatus),
+ genreFilter = new BeatmapSearchFilterRow(BeatmapsStrings.ListingSearchFiltersGenre),
+ languageFilter = new BeatmapSearchFilterRow(BeatmapsStrings.ListingSearchFiltersLanguage),
+ extraFilter = new BeatmapSearchMultipleSelectionFilterRow(BeatmapsStrings.ListingSearchFiltersExtra),
ranksFilter = new BeatmapSearchScoreFilterRow(),
- playedFilter = new BeatmapSearchFilterRow(@"Played"),
- explicitContentFilter = new BeatmapSearchFilterRow(@"Explicit Content"),
+ playedFilter = new BeatmapSearchFilterRow(BeatmapsStrings.ListingSearchFiltersPlayed),
+ explicitContentFilter = new BeatmapSearchFilterRow(BeatmapsStrings.ListingSearchFiltersNsfw),
}
}
}
@@ -172,7 +173,7 @@ namespace osu.Game.Overlays.BeatmapListing
public BeatmapSearchTextBox()
{
- PlaceholderText = @"type in keywords...";
+ PlaceholderText = BeatmapsStrings.ListingSearchPrompt;
}
protected override bool OnKeyDown(KeyDownEvent e)
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs
index 01bcbd3244..4c831543fe 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs
@@ -11,8 +11,8 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK;
-using Humanizer;
using osu.Framework.Extensions.EnumExtensions;
+using osu.Framework.Localisation;
namespace osu.Game.Overlays.BeatmapListing
{
@@ -26,7 +26,7 @@ namespace osu.Game.Overlays.BeatmapListing
set => current.Current = value;
}
- public BeatmapSearchFilterRow(string headerName)
+ public BeatmapSearchFilterRow(LocalisableString header)
{
Drawable filter;
AutoSizeAxes = Axes.Y;
@@ -53,7 +53,7 @@ namespace osu.Game.Overlays.BeatmapListing
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.GetFont(size: 13),
- Text = headerName.Titleize()
+ Text = header
},
filter = CreateFilter()
}
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs
index 5dfa8e6109..e0632ace58 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs
@@ -9,6 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
using osuTK;
namespace osu.Game.Overlays.BeatmapListing
@@ -19,8 +20,8 @@ namespace osu.Game.Overlays.BeatmapListing
private MultipleSelectionFilter filter;
- public BeatmapSearchMultipleSelectionFilterRow(string headerName)
- : base(headerName)
+ public BeatmapSearchMultipleSelectionFilterRow(LocalisableString header)
+ : base(header)
{
Current.BindTo(filter.Current);
}
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs
index a8dc088e52..c2d0eea80c 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
namespace osu.Game.Overlays.BeatmapListing
@@ -10,7 +11,7 @@ namespace osu.Game.Overlays.BeatmapListing
public class BeatmapSearchRulesetFilterRow : BeatmapSearchFilterRow
{
public BeatmapSearchRulesetFilterRow()
- : base(@"Mode")
+ : base(BeatmapsStrings.ListingSearchFiltersMode)
{
}
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs
index 804962adfb..abfffe907f 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs
@@ -1,9 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using System.Linq;
-using osu.Framework.Extensions;
+using osu.Framework.Localisation;
+using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring;
namespace osu.Game.Overlays.BeatmapListing
@@ -11,7 +13,7 @@ namespace osu.Game.Overlays.BeatmapListing
public class BeatmapSearchScoreFilterRow : BeatmapSearchMultipleSelectionFilterRow
{
public BeatmapSearchScoreFilterRow()
- : base(@"Rank Achieved")
+ : base(BeatmapsStrings.ListingSearchFiltersRank)
{
}
@@ -31,18 +33,36 @@ namespace osu.Game.Overlays.BeatmapListing
{
}
- protected override string LabelFor(ScoreRank value)
+ protected override LocalisableString LabelFor(ScoreRank value)
{
switch (value)
{
case ScoreRank.XH:
- return @"Silver SS";
+ return BeatmapsStrings.RankXH;
+
+ case ScoreRank.X:
+ return BeatmapsStrings.RankX;
case ScoreRank.SH:
- return @"Silver S";
+ return BeatmapsStrings.RankSH;
+
+ case ScoreRank.S:
+ return BeatmapsStrings.RankS;
+
+ case ScoreRank.A:
+ return BeatmapsStrings.RankA;
+
+ case ScoreRank.B:
+ return BeatmapsStrings.RankB;
+
+ case ScoreRank.C:
+ return BeatmapsStrings.RankC;
+
+ case ScoreRank.D:
+ return BeatmapsStrings.RankD;
default:
- return value.GetDescription();
+ throw new ArgumentException("Unsupported value.", nameof(value));
}
}
}
diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs
index f02b515755..d64ee59682 100644
--- a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs
+++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs
@@ -7,6 +7,7 @@ using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
+using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
@@ -66,7 +67,7 @@ namespace osu.Game.Overlays.BeatmapListing
///
/// Returns the label text to be used for the supplied .
///
- protected virtual string LabelFor(T value) => (value as Enum)?.GetDescription() ?? value.ToString();
+ protected virtual LocalisableString LabelFor(T value) => (value as Enum)?.GetDescription() ?? value.ToString();
private void updateState()
{
diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs
index 5df7a4650e..5e65cd9488 100644
--- a/osu.Game/Overlays/BeatmapListingOverlay.cs
+++ b/osu.Game/Overlays/BeatmapListingOverlay.cs
@@ -18,6 +18,7 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.BeatmapListing.Panels;
+using osu.Game.Resources.Localisation.Web;
using osuTK;
using osuTK.Graphics;
@@ -232,7 +233,7 @@ namespace osu.Game.Overlays
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Text = @"... nope, nothing found.",
+ Text = BeatmapsStrings.ListingSearchNotFoundQuote,
}
}
});
diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs
index e7d68853ad..a8f2e654d7 100644
--- a/osu.Game/Overlays/ChangelogOverlay.cs
+++ b/osu.Game/Overlays/ChangelogOverlay.cs
@@ -8,7 +8,6 @@ using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
-using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Input.Bindings;
@@ -25,8 +24,6 @@ namespace osu.Game.Overlays
public readonly Bindable Current = new Bindable();
- private Sample sampleBack;
-
private List builds;
protected List Streams;
@@ -41,8 +38,6 @@ namespace osu.Game.Overlays
{
Header.Build.BindTarget = Current;
- sampleBack = audio.Samples.Get(@"UI/generic-select-soft");
-
Current.BindValueChanged(e =>
{
if (e.NewValue != null)
@@ -108,7 +103,6 @@ namespace osu.Game.Overlays
else
{
Current.Value = null;
- sampleBack?.Play();
}
return true;
diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs
index 5f9c00b36a..41e70bbfae 100644
--- a/osu.Game/Overlays/Chat/DrawableChannel.cs
+++ b/osu.Game/Overlays/Chat/DrawableChannel.cs
@@ -6,18 +6,18 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
-using osuTK.Graphics;
+using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Game.Graphics.Containers;
-using osu.Game.Graphics.Cursor;
-using osu.Game.Online.Chat;
using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics;
-using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.Sprites;
+using osu.Game.Online.Chat;
+using osuTK.Graphics;
namespace osu.Game.Overlays.Chat
{
diff --git a/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs b/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs
new file mode 100644
index 0000000000..b0f6400d4f
--- /dev/null
+++ b/osu.Game/Overlays/Settings/Sections/Online/AlertsAndPrivacySettings.cs
@@ -0,0 +1,32 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Configuration;
+
+namespace osu.Game.Overlays.Settings.Sections.Online
+{
+ public class AlertsAndPrivacySettings : SettingsSubsection
+ {
+ protected override string Header => "Alerts and Privacy";
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ Children = new Drawable[]
+ {
+ new SettingsCheckbox
+ {
+ LabelText = "Show a notification when someone mentions your name",
+ Current = config.GetBindable(OsuSetting.NotifyOnUsernameMentioned)
+ },
+ new SettingsCheckbox
+ {
+ LabelText = "Show a notification when you receive a private message",
+ Current = config.GetBindable(OsuSetting.NotifyOnPrivateMessage)
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs b/osu.Game/Overlays/Settings/Sections/OnlineSection.cs
index 7aa4eff29a..680d11f7da 100644
--- a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/OnlineSection.cs
@@ -21,6 +21,7 @@ namespace osu.Game.Overlays.Settings.Sections
Children = new Drawable[]
{
new WebSettings(),
+ new AlertsAndPrivacySettings(),
new IntegrationSettings()
};
}
diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs
index cb7e63ae6f..ca9a8e9c08 100644
--- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs
+++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs
@@ -2,16 +2,20 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
-using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings
{
public class SettingsNumberBox : SettingsItem
{
- protected override Drawable CreateControl() => new OsuNumberBox
+ protected override Drawable CreateControl() => new NumberBox
{
Margin = new MarginPadding { Top = 5 },
RelativeSizeAxes = Axes.X,
};
+
+ public class NumberBox : SettingsTextBox.TextBox
+ {
+ protected override bool CanAddCharacter(char character) => char.IsNumber(character);
+ }
}
}
diff --git a/osu.Game/Overlays/Settings/SettingsTextBox.cs b/osu.Game/Overlays/Settings/SettingsTextBox.cs
index 5e700a1d6b..25424e85a1 100644
--- a/osu.Game/Overlays/Settings/SettingsTextBox.cs
+++ b/osu.Game/Overlays/Settings/SettingsTextBox.cs
@@ -1,18 +1,60 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
+using osuTK.Graphics;
namespace osu.Game.Overlays.Settings
{
public class SettingsTextBox : SettingsItem
{
- protected override Drawable CreateControl() => new OsuTextBox
+ protected override Drawable CreateControl() => new TextBox
{
Margin = new MarginPadding { Top = 5 },
RelativeSizeAxes = Axes.X,
CommitOnFocusLost = true,
};
+
+ public class TextBox : OsuTextBox
+ {
+ private const float border_thickness = 3;
+
+ private Color4 borderColourFocused;
+ private Color4 borderColourUnfocused;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colour)
+ {
+ borderColourUnfocused = colour.Gray4.Opacity(0.5f);
+ borderColourFocused = BorderColour;
+
+ updateBorder();
+ }
+
+ protected override void OnFocus(FocusEvent e)
+ {
+ base.OnFocus(e);
+
+ updateBorder();
+ }
+
+ protected override void OnFocusLost(FocusLostEvent e)
+ {
+ base.OnFocusLost(e);
+
+ updateBorder();
+ }
+
+ private void updateBorder()
+ {
+ BorderThickness = border_thickness;
+ BorderColour = HasFocus ? borderColourFocused : borderColourUnfocused;
+ }
+ }
}
}
diff --git a/osu.Game/Rulesets/Mods/ModRandom.cs b/osu.Game/Rulesets/Mods/ModRandom.cs
index 3f14263420..e0c3008ae8 100644
--- a/osu.Game/Rulesets/Mods/ModRandom.cs
+++ b/osu.Game/Rulesets/Mods/ModRandom.cs
@@ -8,7 +8,6 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Configuration;
using osu.Game.Graphics;
-using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
namespace osu.Game.Rulesets.Mods
@@ -50,7 +49,7 @@ namespace osu.Game.Rulesets.Mods
}
}
- private readonly OsuNumberBox seedNumberBox;
+ private readonly SettingsNumberBox.NumberBox seedNumberBox;
public SeedControl()
{
@@ -76,7 +75,7 @@ namespace osu.Game.Rulesets.Mods
{
new Drawable[]
{
- seedNumberBox = new OsuNumberBox
+ seedNumberBox = new SettingsNumberBox.NumberBox
{
RelativeSizeAxes = Axes.X,
CommitOnFocusLost = true
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index 5fd2b2493e..7fc35fc778 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -716,7 +716,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
if (HitObject != null)
HitObject.DefaultsApplied -= onDefaultsApplied;
- CurrentSkin.SourceChanged -= skinSourceChanged;
+ if (CurrentSkin != null)
+ CurrentSkin.SourceChanged -= skinSourceChanged;
}
}
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
index f478e37e3e..94cc7ed095 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
@@ -19,6 +19,20 @@ namespace osu.Game.Rulesets.UI.Scrolling
private readonly IBindable timeRange = new BindableDouble();
private readonly IBindable direction = new Bindable();
+ ///
+ /// Whether the scrolling direction is horizontal or vertical.
+ ///
+ private Direction scrollingAxis => direction.Value == ScrollingDirection.Left || direction.Value == ScrollingDirection.Right ? Direction.Horizontal : Direction.Vertical;
+
+ ///
+ /// The scrolling axis is inverted if objects temporally farther in the future have a smaller position value across the scrolling axis.
+ ///
+ ///
+ /// is inverted, because given two objects, one of which is at the current time and one of which is 1000ms in the future,
+ /// in the current time instant the future object is spatially above the current object, and therefore has a smaller value of the Y coordinate of its position.
+ ///
+ private bool axisInverted => direction.Value == ScrollingDirection.Down || direction.Value == ScrollingDirection.Right;
+
///
/// A set of top-level s which have an up-to-date layout.
///
@@ -48,99 +62,64 @@ namespace osu.Game.Rulesets.UI.Scrolling
}
///
- /// Given a position in screen space, return the time within this column.
+ /// Given a position at , return the time of the object corresponding to the position.
///
- public double TimeAtScreenSpacePosition(Vector2 screenSpacePosition)
+ ///
+ /// If there are multiple valid time values, one arbitrary time is returned.
+ ///
+ public double TimeAtPosition(float localPosition, double currentTime)
{
- // convert to local space of column so we can snap and fetch correct location.
- Vector2 localPosition = ToLocalSpace(screenSpacePosition);
-
- float position = 0;
-
- switch (scrollingInfo.Direction.Value)
- {
- case ScrollingDirection.Up:
- case ScrollingDirection.Down:
- position = localPosition.Y;
- break;
-
- case ScrollingDirection.Right:
- case ScrollingDirection.Left:
- position = localPosition.X;
- break;
- }
-
- flipPositionIfRequired(ref position);
-
- return scrollingInfo.Algorithm.TimeAt(position, Time.Current, scrollingInfo.TimeRange.Value, scrollLength);
+ float scrollPosition = axisInverted ? scrollLength - localPosition : localPosition;
+ return scrollingInfo.Algorithm.TimeAt(scrollPosition, currentTime, timeRange.Value, scrollLength);
}
///
- /// Given a time, return the screen space position within this column.
+ /// Given a position at the current time in screen space, return the time of the object corresponding the position.
+ ///
+ ///
+ /// If there are multiple valid time values, one arbitrary time is returned.
+ ///
+ public double TimeAtScreenSpacePosition(Vector2 screenSpacePosition)
+ {
+ Vector2 localPosition = ToLocalSpace(screenSpacePosition);
+ return TimeAtPosition(scrollingAxis == Direction.Horizontal ? localPosition.X : localPosition.Y, Time.Current);
+ }
+
+ ///
+ /// Given a time, return the position along the scrolling axis within this at time .
+ ///
+ public float PositionAtTime(double time, double currentTime)
+ {
+ float scrollPosition = scrollingInfo.Algorithm.PositionAt(time, currentTime, timeRange.Value, scrollLength);
+ return axisInverted ? scrollLength - scrollPosition : scrollPosition;
+ }
+
+ ///
+ /// Given a time, return the position along the scrolling axis within this at the current time.
+ ///
+ public float PositionAtTime(double time) => PositionAtTime(time, Time.Current);
+
+ ///
+ /// Given a time, return the screen space position within this .
+ /// In the non-scrolling axis, the center of this is returned.
///
public Vector2 ScreenSpacePositionAtTime(double time)
{
- var pos = scrollingInfo.Algorithm.PositionAt(time, Time.Current, scrollingInfo.TimeRange.Value, scrollLength);
-
- flipPositionIfRequired(ref pos);
-
- switch (scrollingInfo.Direction.Value)
- {
- case ScrollingDirection.Up:
- case ScrollingDirection.Down:
- return ToScreenSpace(new Vector2(getBreadth() / 2, pos));
-
- default:
- return ToScreenSpace(new Vector2(pos, getBreadth() / 2));
- }
+ float localPosition = PositionAtTime(time, Time.Current);
+ return scrollingAxis == Direction.Horizontal
+ ? ToScreenSpace(new Vector2(localPosition, DrawHeight / 2))
+ : ToScreenSpace(new Vector2(DrawWidth / 2, localPosition));
}
- private float scrollLength
+ ///
+ /// Given a start time and end time of a scrolling object, return the length of the object along the scrolling axis.
+ ///
+ public float LengthAtTime(double startTime, double endTime)
{
- get
- {
- switch (scrollingInfo.Direction.Value)
- {
- case ScrollingDirection.Left:
- case ScrollingDirection.Right:
- return DrawWidth;
-
- default:
- return DrawHeight;
- }
- }
+ return scrollingInfo.Algorithm.GetLength(startTime, endTime, timeRange.Value, scrollLength);
}
- private float getBreadth()
- {
- switch (scrollingInfo.Direction.Value)
- {
- case ScrollingDirection.Up:
- case ScrollingDirection.Down:
- return DrawWidth;
-
- default:
- return DrawHeight;
- }
- }
-
- private void flipPositionIfRequired(ref float position)
- {
- // We're dealing with screen coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time.
- // The scrolling algorithm instead assumes a top anchor meaning an increase in time corresponds to an increase in position,
- // so when scrolling downwards the coordinates need to be flipped.
-
- switch (scrollingInfo.Direction.Value)
- {
- case ScrollingDirection.Down:
- position = DrawHeight - position;
- break;
-
- case ScrollingDirection.Right:
- position = DrawWidth - position;
- break;
- }
- }
+ private float scrollLength => scrollingAxis == Direction.Horizontal ? DrawWidth : DrawHeight;
protected override void AddDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable)
{
@@ -237,18 +216,11 @@ namespace osu.Game.Rulesets.UI.Scrolling
{
if (hitObject.HitObject is IHasDuration e)
{
- switch (direction.Value)
- {
- case ScrollingDirection.Up:
- case ScrollingDirection.Down:
- hitObject.Height = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, e.EndTime, timeRange.Value, scrollLength);
- break;
-
- case ScrollingDirection.Left:
- case ScrollingDirection.Right:
- hitObject.Width = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, e.EndTime, timeRange.Value, scrollLength);
- break;
- }
+ float length = LengthAtTime(hitObject.HitObject.StartTime, e.EndTime);
+ if (scrollingAxis == Direction.Horizontal)
+ hitObject.Width = length;
+ else
+ hitObject.Height = length;
}
foreach (var obj in hitObject.NestedHitObjects)
@@ -262,24 +234,16 @@ namespace osu.Game.Rulesets.UI.Scrolling
private void updatePosition(DrawableHitObject hitObject, double currentTime)
{
- switch (direction.Value)
- {
- case ScrollingDirection.Up:
- hitObject.Y = scrollingInfo.Algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, timeRange.Value, scrollLength);
- break;
+ float position = PositionAtTime(hitObject.HitObject.StartTime, currentTime);
- case ScrollingDirection.Down:
- hitObject.Y = -scrollingInfo.Algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, timeRange.Value, scrollLength);
- break;
+ // The position returned from `PositionAtTime` is assuming the `TopLeft` anchor.
+ // A correction is needed because the hit objects are using a different anchor for each direction (e.g. `BottomCentre` for `Bottom` direction).
+ float anchorCorrection = axisInverted ? scrollLength : 0;
- case ScrollingDirection.Left:
- hitObject.X = scrollingInfo.Algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, timeRange.Value, scrollLength);
- break;
-
- case ScrollingDirection.Right:
- hitObject.X = -scrollingInfo.Algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, timeRange.Value, scrollLength);
- break;
- }
+ if (scrollingAxis == Direction.Horizontal)
+ hitObject.X = position - anchorCorrection;
+ else
+ hitObject.Y = position - anchorCorrection;
}
}
}
diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
index 8ddae67dba..a86a614a05 100644
--- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
+++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
@@ -44,6 +44,8 @@ namespace osu.Game.Screens.Select.Leaderboards
private IBindable> itemRemoved;
+ private IBindable> itemAdded;
+
///
/// Whether to apply the game's currently selected mods as a filter when retrieving scores.
///
@@ -85,6 +87,9 @@ namespace osu.Game.Screens.Select.Leaderboards
itemRemoved = scoreManager.ItemRemoved.GetBoundCopy();
itemRemoved.BindValueChanged(onScoreRemoved);
+
+ itemAdded = scoreManager.ItemUpdated.GetBoundCopy();
+ itemAdded.BindValueChanged(onScoreAdded);
}
protected override void Reset()
@@ -93,7 +98,25 @@ namespace osu.Game.Screens.Select.Leaderboards
TopScore = null;
}
- private void onScoreRemoved(ValueChangedEvent> score) => Schedule(RefreshScores);
+ private void onScoreRemoved(ValueChangedEvent> score) =>
+ scoreStoreChanged(score);
+
+ private void onScoreAdded(ValueChangedEvent> score) =>
+ scoreStoreChanged(score);
+
+ private void scoreStoreChanged(ValueChangedEvent> score)
+ {
+ if (Scope != BeatmapLeaderboardScope.Local)
+ return;
+
+ if (score.NewValue.TryGetTarget(out var scoreInfo))
+ {
+ if (Beatmap?.ID != scoreInfo.BeatmapInfoID)
+ return;
+ }
+
+ RefreshScores();
+ }
protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local;
diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs
index 9d3bafd0b1..1da80f6613 100644
--- a/osu.Game/Skinning/LegacyHealthDisplay.cs
+++ b/osu.Game/Skinning/LegacyHealthDisplay.cs
@@ -148,9 +148,9 @@ namespace osu.Game.Skinning
}
}
- internal class LegacyOldStyleFill : LegacyHealthPiece
+ internal abstract class LegacyFill : LegacyHealthPiece
{
- public LegacyOldStyleFill(ISkin skin)
+ protected LegacyFill(ISkin skin)
{
// required for sizing correctly..
var firstFrame = getTexture(skin, "colour-0");
@@ -162,27 +162,29 @@ namespace osu.Game.Skinning
}
else
{
- InternalChild = skin.GetAnimation("scorebar-colour", true, true, startAtCurrentTime: false, applyConfigFrameRate: true) ?? Drawable.Empty();
+ InternalChild = skin.GetAnimation("scorebar-colour", true, true, startAtCurrentTime: false, applyConfigFrameRate: true) ?? Empty();
Size = new Vector2(firstFrame.DisplayWidth, firstFrame.DisplayHeight);
}
- Position = new Vector2(3, 10) * 1.6f;
Masking = true;
}
}
- internal class LegacyNewStyleFill : LegacyHealthPiece
+ internal class LegacyOldStyleFill : LegacyFill
+ {
+ public LegacyOldStyleFill(ISkin skin)
+ : base(skin)
+ {
+ Position = new Vector2(3, 10) * 1.6f;
+ }
+ }
+
+ internal class LegacyNewStyleFill : LegacyFill
{
public LegacyNewStyleFill(ISkin skin)
+ : base(skin)
{
- InternalChild = new Sprite
- {
- Texture = getTexture(skin, "colour"),
- };
-
- Size = InternalChild.Size;
Position = new Vector2(7.5f, 7.8f) * 1.6f;
- Masking = true;
}
protected override void Update()
diff --git a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs
index c7edc0174a..01dd7a25c8 100644
--- a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs
+++ b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs
@@ -4,7 +4,6 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Input;
using osu.Framework.Testing.Input;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.Sprites;
@@ -49,7 +48,7 @@ namespace osu.Game.Tests.Visual
InputManager = new ManualInputManager
{
UseParentInput = true,
- Child = new PlatformActionContainer().WithChild(mainContent)
+ Child = mainContent
},
new Container
{
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 2ee8ed527f..8eeaad1127 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -30,12 +30,12 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 7832aaaf2d..db442238ce 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,8 +70,8 @@
-
-
+
+
@@ -93,7 +93,7 @@
-
+