1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 10:12:54 +08:00

Merge branch 'master' into taiko-hd-mod

This commit is contained in:
Dean Herbert 2021-06-16 16:09:52 +09:00 committed by GitHub
commit 3400cbe076
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 1186 additions and 486 deletions

View File

@ -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"
]

93
.github/workflows/ci.yml vendored Normal file
View File

@ -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

31
.github/workflows/report-nunit.yml vendored Normal file
View File

@ -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

14
.vscode/launch.json vendored
View File

@ -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
}
]
}

View File

@ -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
# 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

6
InspectCode.sh Executable file
View File

@ -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

View File

@ -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:

View File

@ -1,17 +0,0 @@
<Project Sdk="Microsoft.Build.Traversal">
<ItemGroup>
<ProjectReference Include="..\osu.Desktop\osu.Desktop.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Catch.Tests\osu.Game.Rulesets.Catch.Tests.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Mania.Tests\osu.Game.Rulesets.Mania.Tests.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Osu.Tests\osu.Game.Rulesets.Osu.Tests.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Taiko.Tests\osu.Game.Rulesets.Taiko.Tests.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
<ProjectReference Include="..\osu.Game.Tests\osu.Game.Tests.csproj" />
<ProjectReference Include="..\osu.Game.Tournament.Tests\osu.Game.Tournament.Tests.csproj" />
<ProjectReference Include="..\osu.Game.Tournament\osu.Game.Tournament.csproj" />
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
</ItemGroup>
</Project>

View File

@ -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);

View File

@ -1,5 +0,0 @@
[Nuget]
Source=https://api.nuget.org/v3/index.json
UseInProcessClient=true
LoadDependencies=true

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.604.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.609.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.614.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.614.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,2 @@
[General]
// no version specified means v1

View File

@ -33,13 +33,13 @@ namespace osu.Game.Rulesets.Catch.Mods
private class MouseInputHelper : Drawable, IKeyBindingHandler<CatchAction>, 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);
}
}

View File

@ -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<CatchAction>
public class Catcher : SkinReloadableDrawable
{
/// <summary>
/// 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
/// </summary>
public const double BASE_SPEED = 1.0;
/// <summary>
/// The current speed of the catcher.
/// </summary>
public double Speed => (Dashing ? 1 : 0.5) * BASE_SPEED * hyperDashModifier;
/// <summary>
/// The amount by which caught fruit should be offset from the plate surface to make them look visually "caught".
/// </summary>
@ -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);
}
/// <summary>
/// Width of the area that can be used to attempt catches during gameplay.
/// </summary>
@ -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;
}
}
/// <summary>
/// Drop any fruit off the plate.
/// </summary>
@ -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))

View File

@ -1,8 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.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<CatchAction>
{
public const float CATCHER_SIZE = 106.75f;
public readonly Catcher MovableCatcher;
private readonly CatchComboDisplay comboDisplay;
/// <summary>
/// <c>-1</c> when only left button is pressed.
/// <c>1</c> when only right button is pressed.
/// <c>0</c> when none or both left and right buttons are pressed.
/// </summary>
private int currentDirection;
public CatcherArea(Container<CaughtObject> 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<CatchAction>)?.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<CatchAction>)?.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;
}
}
}
}

View File

@ -0,0 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Rulesets.Catch.UI
{
public enum Direction
{
Right = 1,
Left = -1
}
}

View File

@ -1,5 +1,5 @@
[General]
Version: 1.0
// no version specified means v1
[Fonts]
HitCircleOverlap: 3

View File

@ -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, Color4>(OsuSkinColour.SpinnerBackground)?.Value ?? new Color4(100, 100, 100, 255),
Scale = new Vector2(SPRITE_SCALE),
Y = SPINNER_Y_CENTRE,
},

View File

@ -7,6 +7,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
SliderTrackOverride,
SliderBorder,
SliderBall
SliderBall,
SpinnerBackground,
}
}

View File

@ -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));
}

View File

@ -1,2 +1,2 @@
[General]
Version: 1.0
// no version specified means v1

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

View File

@ -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()

View File

@ -0,0 +1,240 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
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<Message>(0));
return true;
case CreateChannelRequest createChannelRequest:
var apiChatChannel = new APIChatChannel
{
RecentMessages = new List<Message>(0),
ChannelID = (int)createChannelRequest.Channel.Id
};
createChannelRequest.TriggerSuccess(apiChatChannel);
return true;
case ListChannelsRequest listChannelsRequest:
listChannelsRequest.TriggerSuccess(new List<Channel>(1) { publicChannel });
return true;
case GetUpdatesRequest updatesRequest:
updatesRequest.TriggerSuccess(new GetUpdatesResponse
{
Messages = new List<Message>(0),
Presence = new List<Channel>(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<MessageNotifier.MentionNotification>);
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<MessageNotifier.PrivateMessageNotification>);
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<T>() where T : Notification
{
var notification = testContainer.NotificationOverlay.ChildrenOfType<T>().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<Channel>)ChannelManager.AvailableChannels).AddRange(channels);
foreach (var channel in channels)
ChannelManager.JoinChannel(channel);
}
}
}
}

View File

@ -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<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory));
return dependencies;
}
public TestSceneBeatmapLeaderboard()
{
Add(dialogOverlay = new DialogOverlay
AddRange(new Drawable[]
{
dialogOverlay = new DialogOverlay
{
Depth = -1
});
Add(leaderboard = new FailableLeaderboard
},
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;
});
AddStep(@"New Scores", newScores);
clearScores();
checkCount(0);
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> 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<LeaderboardScore>().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)

View File

@ -101,10 +101,20 @@ namespace osu.Game.Beatmaps
/// Rulesets ordered descending by their respective recommended difficulties.
/// The currently selected ruleset will always be first.
/// </returns>
private IEnumerable<RulesetInfo> orderedRulesets =>
recommendedDifficultyMapping
.OrderByDescending(pair => pair.Value).Select(pair => pair.Key).Where(r => !r.Equals(ruleset.Value))
private IEnumerable<RulesetInfo> orderedRulesets
{
get
{
if (LoadState < LoadState.Ready || ruleset.Value == null)
return Enumerable.Empty<RulesetInfo>();
return recommendedDifficultyMapping
.OrderByDescending(pair => pair.Value)
.Select(pair => pair.Key)
.Where(r => !r.Equals(ruleset.Value))
.Prepend(ruleset.Value);
}
}
private void onlineStateChanged(ValueChangedEvent<APIState> state) => Schedule(() =>
{

View File

@ -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,

View File

@ -1,20 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.ComponentModel;
namespace osu.Game.Configuration
{
public enum RankingType
{
Local,
[Description("Global")]
Top,
[Description("Selected Mods")]
SelectedMod,
Friends,
Country
}
}

View File

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

View File

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

View File

@ -107,10 +107,10 @@ namespace osu.Game.Graphics.Containers
{
}
private bool playedPopInSound;
protected override void UpdateState(ValueChangedEvent<Visibility> state)
{
bool didChange = state.NewValue != state.OldValue;
switch (state.NewValue)
{
case Visibility.Visible:
@ -121,18 +121,15 @@ namespace osu.Game.Graphics.Containers
return;
}
if (didChange)
samplePopIn?.Play();
playedPopInSound = true;
if (BlockScreenWideMouse && DimMainContent) game?.AddBlockingOverlay(this);
break;
case Visibility.Hidden:
if (playedPopInSound)
{
if (didChange)
samplePopOut?.Play();
playedPopInSound = false;
}
if (BlockScreenWideMouse) game?.RemoveBlockingOverlay(this);
break;

View File

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

View File

@ -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 <code>null</code>, the click sound will only be played on left click.
/// </param>
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");
}
}
}

View File

@ -0,0 +1,25 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.ComponentModel;
namespace osu.Game.Graphics.UserInterface
{
public enum HoverSampleSet
{
[Description("default")]
Default,
[Description("button")]
Button,
[Description("softer")]
Soft,
[Description("toolbar")]
Toolbar,
[Description("songselect")]
SongSelect
}
}

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.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
}
}

View File

@ -44,6 +44,7 @@ namespace osu.Game.Graphics.UserInterface
private readonly Box hover;
public OsuAnimatedButton()
: base(HoverSampleSet.Button)
{
base.Content.Add(content = new Container
{

View File

@ -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;

View File

@ -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";
/// <summary>
/// "chat"
/// </summary>
public static LocalisableString HeaderTitle => new TranslatableString(getKey("header_title"), "chat");
public static LocalisableString HeaderTitle => new TranslatableString(getKey(@"header_title"), @"chat");
/// <summary>
/// "join the real-time discussion"
/// </summary>
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}";
}

View File

@ -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";
/// <summary>
/// "Cancel"
/// </summary>
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}";
}

View File

@ -7,10 +7,10 @@ namespace osu.Game.Localisation
{
public enum Language
{
[Description("English")]
[Description(@"English")]
en,
[Description("日本語")]
[Description(@"日本語")]
ja
}
}

View File

@ -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";
/// <summary>
/// "notifications"
/// </summary>
public static LocalisableString HeaderTitle => new TranslatableString(getKey("header_title"), "notifications");
public static LocalisableString HeaderTitle => new TranslatableString(getKey(@"header_title"), @"notifications");
/// <summary>
/// "waiting for 'ya"
/// </summary>
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}";
}

View File

@ -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";
/// <summary>
/// "now playing"
/// </summary>
public static LocalisableString HeaderTitle => new TranslatableString(getKey("header_title"), "now playing");
public static LocalisableString HeaderTitle => new TranslatableString(getKey(@"header_title"), @"now playing");
/// <summary>
/// "manage the currently playing track"
/// </summary>
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}";
}

View File

@ -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
{

View File

@ -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";
/// <summary>
/// "settings"
/// </summary>
public static LocalisableString HeaderTitle => new TranslatableString(getKey("header_title"), "settings");
public static LocalisableString HeaderTitle => new TranslatableString(getKey(@"header_title"), @"settings");
/// <summary>
/// "change the way osu! behaves"
/// </summary>
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}";
}

View File

@ -11,11 +11,11 @@ namespace osu.Game.Online.API.Requests
{
public class CreateChannelRequest : APIRequest<APIChatChannel>
{
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;
}

View File

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

View File

@ -0,0 +1,181 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.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
{
/// <summary>
/// Component that handles creating and posting notifications for incoming messages.
/// </summary>
public class MessageNotifier : Component
{
[Resolved]
private NotificationOverlay notifications { get; set; }
[Resolved]
private ChatOverlay chatOverlay { get; set; }
[Resolved]
private ChannelManager channelManager { get; set; }
private Bindable<bool> notifyOnUsername;
private Bindable<bool> notifyOnPrivateMessage;
private readonly IBindable<User> localUser = new Bindable<User>();
private readonly IBindableList<Channel> joinedChannels = new BindableList<Channel>();
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, IAPIProvider api)
{
notifyOnUsername = config.GetBindable<bool>(OsuSetting.NotifyOnUsernameMentioned);
notifyOnPrivateMessage = config.GetBindable<bool>(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>())
channel.NewMessagesArrived += checkNewMessages;
break;
case NotifyCollectionChangedAction.Remove:
foreach (var channel in e.OldItems.Cast<Channel>())
channel.NewMessagesArrived -= checkNewMessages;
break;
}
}
private void checkNewMessages(IEnumerable<Message> 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);
}
}
/// <summary>
/// Checks whether the user enabled private message notifications and whether specified <paramref name="message"/> is a direct message.
/// </summary>
/// <param name="channel">The channel associated to the <paramref name="message"/></param>
/// <param name="message">The message to be checked</param>
/// <returns>Whether a notification was fired.</returns>
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));
}
/// <summary>
/// Checks if <paramref name="message"/> contains <paramref name="username"/>.
/// This will match against the case where underscores are used instead of spaces (which is how osu-stable handles usernames with spaces).
/// </summary>
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;
};
}
}
}
}

View File

@ -44,9 +44,9 @@ namespace osu.Game.Online.Leaderboards
protected override Container<Drawable> Content => content;
private IEnumerable<TScoreInfo> scores;
private ICollection<TScoreInfo> scores;
public IEnumerable<TScoreInfo> Scores
public ICollection<TScoreInfo> 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;
}));

View File

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

View File

@ -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<SearchGeneral>(@"General"),
generalFilter = new BeatmapSearchMultipleSelectionFilterRow<SearchGeneral>(BeatmapsStrings.ListingSearchFiltersGeneral),
modeFilter = new BeatmapSearchRulesetFilterRow(),
categoryFilter = new BeatmapSearchFilterRow<SearchCategory>(@"Categories"),
genreFilter = new BeatmapSearchFilterRow<SearchGenre>(@"Genre"),
languageFilter = new BeatmapSearchFilterRow<SearchLanguage>(@"Language"),
extraFilter = new BeatmapSearchMultipleSelectionFilterRow<SearchExtra>(@"Extra"),
categoryFilter = new BeatmapSearchFilterRow<SearchCategory>(BeatmapsStrings.ListingSearchFiltersStatus),
genreFilter = new BeatmapSearchFilterRow<SearchGenre>(BeatmapsStrings.ListingSearchFiltersGenre),
languageFilter = new BeatmapSearchFilterRow<SearchLanguage>(BeatmapsStrings.ListingSearchFiltersLanguage),
extraFilter = new BeatmapSearchMultipleSelectionFilterRow<SearchExtra>(BeatmapsStrings.ListingSearchFiltersExtra),
ranksFilter = new BeatmapSearchScoreFilterRow(),
playedFilter = new BeatmapSearchFilterRow<SearchPlayed>(@"Played"),
explicitContentFilter = new BeatmapSearchFilterRow<SearchExplicit>(@"Explicit Content"),
playedFilter = new BeatmapSearchFilterRow<SearchPlayed>(BeatmapsStrings.ListingSearchFiltersPlayed),
explicitContentFilter = new BeatmapSearchFilterRow<SearchExplicit>(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)

View File

@ -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()
}

View File

@ -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);
}

View File

@ -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<RulesetInfo>
{
public BeatmapSearchRulesetFilterRow()
: base(@"Mode")
: base(BeatmapsStrings.ListingSearchFiltersMode)
{
}

View File

@ -1,9 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
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<ScoreRank>
{
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));
}
}
}

View File

@ -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
/// <summary>
/// Returns the label text to be used for the supplied <paramref name="value"/>.
/// </summary>
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()
{

View File

@ -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,
}
}
});

View File

@ -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<APIChangelogBuild> Current = new Bindable<APIChangelogBuild>();
private Sample sampleBack;
private List<APIChangelogBuild> builds;
protected List<APIUpdateStream> 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;

View File

@ -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
{

View File

@ -0,0 +1,32 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
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<bool>(OsuSetting.NotifyOnUsernameMentioned)
},
new SettingsCheckbox
{
LabelText = "Show a notification when you receive a private message",
Current = config.GetBindable<bool>(OsuSetting.NotifyOnPrivateMessage)
},
};
}
}
}

View File

@ -21,6 +21,7 @@ namespace osu.Game.Overlays.Settings.Sections
Children = new Drawable[]
{
new WebSettings(),
new AlertsAndPrivacySettings(),
new IntegrationSettings()
};
}

View File

@ -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<string>
{
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);
}
}
}

View File

@ -1,18 +1,60 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osuTK.Graphics;
namespace osu.Game.Overlays.Settings
{
public class SettingsTextBox : SettingsItem<string>
{
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;
}
}
}
}

View File

@ -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

View File

@ -716,6 +716,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
if (HitObject != null)
HitObject.DefaultsApplied -= onDefaultsApplied;
if (CurrentSkin != null)
CurrentSkin.SourceChanged -= skinSourceChanged;
}
}

View File

@ -19,6 +19,20 @@ namespace osu.Game.Rulesets.UI.Scrolling
private readonly IBindable<double> timeRange = new BindableDouble();
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
/// <summary>
/// Whether the scrolling direction is horizontal or vertical.
/// </summary>
private Direction scrollingAxis => direction.Value == ScrollingDirection.Left || direction.Value == ScrollingDirection.Right ? Direction.Horizontal : Direction.Vertical;
/// <summary>
/// The scrolling axis is inverted if objects temporally farther in the future have a smaller position value across the scrolling axis.
/// </summary>
/// <example>
/// <see cref="ScrollingDirection.Down"/> 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.
/// </example>
private bool axisInverted => direction.Value == ScrollingDirection.Down || direction.Value == ScrollingDirection.Right;
/// <summary>
/// A set of top-level <see cref="DrawableHitObject"/>s which have an up-to-date layout.
/// </summary>
@ -48,99 +62,64 @@ namespace osu.Game.Rulesets.UI.Scrolling
}
/// <summary>
/// Given a position in screen space, return the time within this column.
/// Given a position at <paramref name="currentTime"/>, return the time of the object corresponding to the position.
/// </summary>
public double TimeAtScreenSpacePosition(Vector2 screenSpacePosition)
/// <remarks>
/// If there are multiple valid time values, one arbitrary time is returned.
/// </remarks>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// If there are multiple valid time values, one arbitrary time is returned.
/// </remarks>
public double TimeAtScreenSpacePosition(Vector2 screenSpacePosition)
{
Vector2 localPosition = ToLocalSpace(screenSpacePosition);
return TimeAtPosition(scrollingAxis == Direction.Horizontal ? localPosition.X : localPosition.Y, Time.Current);
}
/// <summary>
/// Given a time, return the position along the scrolling axis within this <see cref="HitObjectContainer"/> at time <paramref name="currentTime"/>.
/// </summary>
public float PositionAtTime(double time, double currentTime)
{
float scrollPosition = scrollingInfo.Algorithm.PositionAt(time, currentTime, timeRange.Value, scrollLength);
return axisInverted ? scrollLength - scrollPosition : scrollPosition;
}
/// <summary>
/// Given a time, return the position along the scrolling axis within this <see cref="HitObjectContainer"/> at the current time.
/// </summary>
public float PositionAtTime(double time) => PositionAtTime(time, Time.Current);
/// <summary>
/// Given a time, return the screen space position within this <see cref="HitObjectContainer"/>.
/// In the non-scrolling axis, the center of this <see cref="HitObjectContainer"/> is returned.
/// </summary>
public Vector2 ScreenSpacePositionAtTime(double time)
{
var pos = scrollingInfo.Algorithm.PositionAt(time, Time.Current, scrollingInfo.TimeRange.Value, scrollLength);
float localPosition = PositionAtTime(time, Time.Current);
return scrollingAxis == Direction.Horizontal
? ToScreenSpace(new Vector2(localPosition, DrawHeight / 2))
: ToScreenSpace(new Vector2(DrawWidth / 2, localPosition));
}
flipPositionIfRequired(ref pos);
switch (scrollingInfo.Direction.Value)
/// <summary>
/// Given a start time and end time of a scrolling object, return the length of the object along the scrolling axis.
/// </summary>
public float LengthAtTime(double startTime, double endTime)
{
case ScrollingDirection.Up:
case ScrollingDirection.Down:
return ToScreenSpace(new Vector2(getBreadth() / 2, pos));
default:
return ToScreenSpace(new Vector2(pos, getBreadth() / 2));
}
return scrollingInfo.Algorithm.GetLength(startTime, endTime, timeRange.Value, scrollLength);
}
private float scrollLength
{
get
{
switch (scrollingInfo.Direction.Value)
{
case ScrollingDirection.Left:
case ScrollingDirection.Right:
return DrawWidth;
default:
return DrawHeight;
}
}
}
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;
}
}
}

View File

@ -44,6 +44,8 @@ namespace osu.Game.Screens.Select.Leaderboards
private IBindable<WeakReference<ScoreInfo>> itemRemoved;
private IBindable<WeakReference<ScoreInfo>> itemAdded;
/// <summary>
/// Whether to apply the game's currently selected mods as a filter when retrieving scores.
/// </summary>
@ -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<WeakReference<ScoreInfo>> score) => Schedule(RefreshScores);
private void onScoreRemoved(ValueChangedEvent<WeakReference<ScoreInfo>> score) =>
scoreStoreChanged(score);
private void onScoreAdded(ValueChangedEvent<WeakReference<ScoreInfo>> score) =>
scoreStoreChanged(score);
private void scoreStoreChanged(ValueChangedEvent<WeakReference<ScoreInfo>> 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;

View File

@ -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()

View File

@ -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
{

View File

@ -30,12 +30,12 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.LocalisationAnalyser" Version="2021.525.0">
<PackageReference Include="ppy.LocalisationAnalyser" Version="2021.608.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="ppy.osu.Framework" Version="2021.609.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.604.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.614.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.614.0" />
<PackageReference Include="Sentry" Version="3.4.0" />
<PackageReference Include="SharpCompress" Version="0.28.2" />
<PackageReference Include="NUnit" Version="3.13.2" />

View File

@ -70,8 +70,8 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.609.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.604.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.614.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.614.0" />
</ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
<PropertyGroup>
@ -93,7 +93,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2021.609.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.614.0" />
<PackageReference Include="SharpCompress" Version="0.28.2" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="SharpRaven" Version="2.4.0" />