1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-15 23:57:25 +08:00

Merge branch 'master' into log-global-statistics

This commit is contained in:
Bartłomiej Dach 2024-02-28 15:09:24 +01:00 committed by GitHub
commit 9902dad4c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
110 changed files with 1543 additions and 828 deletions

View File

@ -13,10 +13,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Install .NET 8.0.x - name: Install .NET 8.0.x
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: "8.0.x" dotnet-version: "8.0.x"
@ -27,7 +27,7 @@ jobs:
run: dotnet restore osu.Desktop.slnf run: dotnet restore osu.Desktop.slnf
- name: Restore inspectcode cache - name: Restore inspectcode cache
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: ${{ github.workspace }}/inspectcode path: ${{ github.workspace }}/inspectcode
key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu.sln*', 'osu*.slnf', '.editorconfig', '.globalconfig', 'CodeAnalysis/*', '**/*.csproj', '**/*.props') }} key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu.sln*', 'osu*.slnf', '.editorconfig', '.globalconfig', 'CodeAnalysis/*', '**/*.csproj', '**/*.props') }}
@ -70,10 +70,10 @@ jobs:
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Install .NET 8.0.x - name: Install .NET 8.0.x
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: "8.0.x" dotnet-version: "8.0.x"
@ -99,16 +99,16 @@ jobs:
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Setup JDK 11 - name: Setup JDK 11
uses: actions/setup-java@v3 uses: actions/setup-java@v4
with: with:
distribution: microsoft distribution: microsoft
java-version: 11 java-version: 11
- name: Install .NET 8.0.x - name: Install .NET 8.0.x
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: "8.0.x" dotnet-version: "8.0.x"
@ -126,10 +126,10 @@ jobs:
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Install .NET 8.0.x - name: Install .NET 8.0.x
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: "8.0.x" dotnet-version: "8.0.x"

View File

@ -140,7 +140,7 @@ jobs:
GOOGLE_CREDS_FILE: ${{ steps.set-outputs.outputs.GOOGLE_CREDS_FILE }} GOOGLE_CREDS_FILE: ${{ steps.set-outputs.outputs.GOOGLE_CREDS_FILE }}
steps: steps:
- name: Checkout diffcalc-sheet-generator - name: Checkout diffcalc-sheet-generator
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
path: ${{ env.EXECUTION_ID }} path: ${{ env.EXECUTION_ID }}
repository: 'smoogipoo/diffcalc-sheet-generator' repository: 'smoogipoo/diffcalc-sheet-generator'

View File

@ -28,7 +28,7 @@ jobs:
timeout-minutes: 5 timeout-minutes: 5
steps: steps:
- name: Annotate CI run with test results - name: Annotate CI run with test results
uses: dorny/test-reporter@v1.6.0 uses: dorny/test-reporter@v1.8.0
with: with:
artifact: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}} artifact: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}}) name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}})

View File

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0

View File

@ -13,23 +13,23 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Install .NET 8.0.x - name: Install .NET 8.0.x
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: "8.0.x" dotnet-version: "8.0.x"
- name: Checkout ppy/osu - name: Checkout ppy/osu
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
path: osu path: osu
- name: Checkout ppy/osu-tools - name: Checkout ppy/osu-tools
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
repository: ppy/osu-tools repository: ppy/osu-tools
path: osu-tools path: osu-tools
- name: Checkout ppy/osu-web - name: Checkout ppy/osu-web
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
repository: ppy/osu-web repository: ppy/osu-web
path: osu-web path: osu-web
@ -43,7 +43,7 @@ jobs:
working-directory: ./osu-tools working-directory: ./osu-tools
- name: Create pull request with changes - name: Create pull request with changes
uses: peter-evans/create-pull-request@v5 uses: peter-evans/create-pull-request@v6
with: with:
title: Update mod definitions title: Update mod definitions
body: "This PR has been auto-generated to update the mod definitions to match ppy/osu@${{ github.ref_name }}." body: "This PR has been auto-generated to update the mod definitions to match ppy/osu@${{ github.ref_name }}."

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.217.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2024.223.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged. <!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -92,9 +92,10 @@ namespace osu.Desktop
return; return;
} }
if (status.Value == UserStatus.Online && activity.Value != null) if (activity.Value != null)
{ {
bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited; bool hideIdentifiableInformation = privacyMode.Value == DiscordRichPresenceMode.Limited || status.Value == UserStatus.DoNotDisturb;
presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation)); presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation));
presence.Details = truncate(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty); presence.Details = truncate(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Reflection; using System.Reflection;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using Microsoft.Win32; using Microsoft.Win32;
using osu.Desktop.Performance;
using osu.Desktop.Security; using osu.Desktop.Security;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game; using osu.Game;
@ -15,9 +16,11 @@ using osu.Framework;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Updater; using osu.Game.Updater;
using osu.Desktop.Windows; using osu.Desktop.Windows;
using osu.Framework.Allocation;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.IPC; using osu.Game.IPC;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Performance;
using osu.Game.Utils; using osu.Game.Utils;
using SDL2; using SDL2;
@ -28,6 +31,9 @@ namespace osu.Desktop
private OsuSchemeLinkIPCChannel? osuSchemeLinkIPCChannel; private OsuSchemeLinkIPCChannel? osuSchemeLinkIPCChannel;
private ArchiveImportIPCChannel? archiveImportIPCChannel; private ArchiveImportIPCChannel? archiveImportIPCChannel;
[Cached(typeof(IHighPerformanceSessionManager))]
private readonly HighPerformanceSessionManager highPerformanceSessionManager = new HighPerformanceSessionManager();
public OsuGameDesktop(string[]? args = null) public OsuGameDesktop(string[]? args = null)
: base(args) : base(args)
{ {

View File

@ -0,0 +1,43 @@
// 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.Runtime;
using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Game.Performance;
namespace osu.Desktop.Performance
{
public class HighPerformanceSessionManager : IHighPerformanceSessionManager
{
private GCLatencyMode originalGCMode;
public IDisposable BeginSession()
{
enableHighPerformanceSession();
return new InvokeOnDisposal<HighPerformanceSessionManager>(this, static m => m.disableHighPerformanceSession());
}
private void enableHighPerformanceSession()
{
Logger.Log("Starting high performance session");
originalGCMode = GCSettings.LatencyMode;
GCSettings.LatencyMode = GCLatencyMode.LowLatency;
// Without doing this, the new GC mode won't kick in until the next GC, which could be at a more noticeable point in time.
GC.Collect(0);
}
private void disableHighPerformanceSession()
{
Logger.Log("Ending high performance session");
if (GCSettings.LatencyMode == GCLatencyMode.LowLatency)
GCSettings.LatencyMode = originalGCMode;
// No GC.Collect() as we were already collecting at a higher frequency in the old mode.
}
}
}

View File

@ -0,0 +1,48 @@
// 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 BenchmarkDotNet.Attributes;
using osu.Game.Utils;
namespace osu.Game.Benchmarks
{
public class BenchmarkStringComparison
{
private string[] strings = null!;
[GlobalSetup]
public void GlobalSetUp()
{
strings = new string[10000];
for (int i = 0; i < strings.Length; ++i)
strings[i] = Guid.NewGuid().ToString();
for (int i = 0; i < strings.Length; ++i)
{
if (i % 2 == 0)
strings[i] = strings[i].ToUpperInvariant();
}
}
[Benchmark]
public void OrdinalIgnoreCase() => compare(StringComparer.OrdinalIgnoreCase);
[Benchmark]
public void OrdinalSortByCase() => compare(OrdinalSortByCaseStringComparer.DEFAULT);
[Benchmark]
public void InvariantCulture() => compare(StringComparer.InvariantCulture);
private void compare(IComparer<string> comparer)
{
for (int i = 0; i < strings.Length; ++i)
{
for (int j = i + 1; j < strings.Length; ++j)
_ = comparer.Compare(strings[i], strings[j]);
}
}
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
@ -17,9 +18,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
[Resolved] [Resolved]
private Playfield playfield { get; set; } = null!; private Playfield playfield { get; set; } = null!;
[Resolved]
private IScrollingInfo scrollingInfo { get; set; } = null!;
protected ScrollingHitObjectContainer HitObjectContainer => ((ManiaPlayfield)playfield).GetColumn(HitObject.Column).HitObjectContainer; protected ScrollingHitObjectContainer HitObjectContainer => ((ManiaPlayfield)playfield).GetColumn(HitObject.Column).HitObjectContainer;
protected ManiaSelectionBlueprint(T hitObject) protected ManiaSelectionBlueprint(T hitObject)
@ -28,14 +26,31 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
RelativeSizeAxes = Axes.None; RelativeSizeAxes = Axes.None;
} }
protected override void Update() private readonly IBindable<ScrollingDirection> directionBindable = new Bindable<ScrollingDirection>();
{
base.Update();
var anchor = scrollingInfo.Direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; [BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
directionBindable.BindTo(scrollingInfo.Direction);
}
protected override void LoadComplete()
{
base.LoadComplete();
directionBindable.BindValueChanged(onDirectionChanged, true);
}
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
{
var anchor = direction.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
Anchor = Origin = anchor; Anchor = Origin = anchor;
foreach (var child in InternalChildren) foreach (var child in InternalChildren)
child.Anchor = child.Origin = anchor; child.Anchor = child.Origin = anchor;
}
protected override void Update()
{
base.Update();
Position = Parent!.ToLocalSpace(HitObjectContainer.ScreenSpacePositionAtTime(HitObject.StartTime)) - AnchorPosition; Position = Parent!.ToLocalSpace(HitObjectContainer.ScreenSpacePositionAtTime(HitObject.StartTime)) - AnchorPosition;
Width = HitObjectContainer.DrawWidth; Width = HitObjectContainer.DrawWidth;

View File

@ -7,7 +7,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
@ -149,7 +148,18 @@ namespace osu.Game.Rulesets.Mania.UI
/// <summary> /// <summary>
/// Retrieves the total amount of columns across all stages in this playfield. /// Retrieves the total amount of columns across all stages in this playfield.
/// </summary> /// </summary>
public int TotalColumns => stages.Sum(s => s.Columns.Length); public int TotalColumns
{
get
{
int sum = 0;
foreach (var stage in stages)
sum += stage.Columns.Length;
return sum;
}
}
private Stage getStageByColumn(int column) private Stage getStageByColumn(int column)
{ {

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Beatmaps;
@ -32,6 +33,27 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
[Test] [Test]
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true }); public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
[Test]
public void TestPlayfieldBasedSize()
{
ModFlashlight mod = new OsuModFlashlight();
CreateModTest(new ModTestData
{
Mod = mod,
PassCondition = () =>
{
var flashlightOverlay = Player.DrawableRuleset.Overlays
.OfType<ModFlashlight<OsuHitObject>.Flashlight>()
.First();
return Precision.AlmostEquals(mod.DefaultFlashlightSize * .5f, flashlightOverlay.GetSize());
}
});
AddStep("adjust playfield scale", () =>
Player.DrawableRuleset.Playfield.Scale = new Vector2(.5f));
}
[Test] [Test]
public void TestSliderDimsOnlyAfterStartTime() public void TestSliderDimsOnlyAfterStartTime()
{ {

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;

View File

@ -78,9 +78,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components
Scale = new Vector2(hitObject.Scale); Scale = new Vector2(hitObject.Scale);
if (hitObject is IHasComboInformation combo)
ring.BorderColour = combo.GetComboColour(skin);
double editorTime = editorClock.CurrentTime; double editorTime = editorClock.CurrentTime;
double hitObjectTime = hitObject.StartTime; double hitObjectTime = hitObject.StartTime;
bool hasReachedObject = editorTime >= hitObjectTime; bool hasReachedObject = editorTime >= hitObjectTime;
@ -92,6 +89,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components
ring.Scale = new Vector2(1 + 0.1f * ringScale); ring.Scale = new Vector2(1 + 0.1f * ringScale);
content.Alpha = 0.9f * (1 - alpha); content.Alpha = 0.9f * (1 - alpha);
// TODO: should only update colour on skin/combo/object change.
if (hitObject is IHasComboInformation combo && content.Alpha > 0)
ring.BorderColour = combo.GetComboColour(skin);
} }
else else
content.Alpha = 0; content.Alpha = 0;

View File

@ -416,8 +416,22 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathEndOffset) ?? BodyPiece.ToScreenSpace(BodyPiece.PathEndLocation) DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathEndOffset) ?? BodyPiece.ToScreenSpace(BodyPiece.PathEndLocation)
}; };
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
BodyPiece.ReceivePositionalInputAt(screenSpacePos) || ControlPointVisualiser?.Pieces.Any(p => p.ReceivePositionalInputAt(screenSpacePos)) == true; {
if (BodyPiece.ReceivePositionalInputAt(screenSpacePos))
return true;
if (ControlPointVisualiser == null)
return false;
foreach (var p in ControlPointVisualiser.Pieces)
{
if (p.ReceivePositionalInputAt(screenSpacePos))
return true;
}
return false;
}
protected virtual SliderCircleOverlay CreateCircleOverlay(Slider slider, SliderPosition position) => new SliderCircleOverlay(slider, position); protected virtual SliderCircleOverlay CreateCircleOverlay(Slider slider, SliderPosition position) => new SliderCircleOverlay(slider, position);
} }

View File

@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override IEnumerable<Drawable> DimmablePieces => new Drawable[] protected override IEnumerable<Drawable> DimmablePieces => new Drawable[]
{ {
HeadCircle, // HeadCircle should not be added to this list, as it handles dimming itself
TailCircle, TailCircle,
repeatContainer, repeatContainer,
Body, Body,

View File

@ -279,10 +279,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (HandleUserInput) if (HandleUserInput)
{ {
bool isValidSpinningTime = Time.Current >= HitObject.StartTime && Time.Current <= HitObject.EndTime; bool isValidSpinningTime = Time.Current >= HitObject.StartTime && Time.Current <= HitObject.EndTime;
bool correctButtonPressed = (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false);
RotationTracker.Tracking = !Result.HasResult RotationTracker.Tracking = !Result.HasResult
&& correctButtonPressed && correctButtonPressed()
&& isValidSpinningTime; && isValidSpinningTime;
} }
@ -292,11 +291,34 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// Ticks can theoretically be judged at any point in the spinner's duration. // Ticks can theoretically be judged at any point in the spinner's duration.
// A tick must be alive to correctly play back samples, // A tick must be alive to correctly play back samples,
// but for performance reasons, we only want to keep the next tick alive. // but for performance reasons, we only want to keep the next tick alive.
var next = NestedHitObjects.FirstOrDefault(h => !h.Judged); DrawableHitObject nextTick = null;
foreach (var nested in NestedHitObjects)
{
if (!nested.Judged)
{
nextTick = nested;
break;
}
}
// See default `LifetimeStart` as set in `DrawableSpinnerTick`. // See default `LifetimeStart` as set in `DrawableSpinnerTick`.
if (next?.LifetimeStart == double.MaxValue) if (nextTick?.LifetimeStart == double.MaxValue)
next.LifetimeStart = HitObject.StartTime; nextTick.LifetimeStart = HitObject.StartTime;
}
private bool correctButtonPressed()
{
if (OsuActionInputManager == null)
return false;
foreach (var action in OsuActionInputManager.PressedActions)
{
if (action == OsuAction.LeftButton || action == OsuAction.RightButton)
return true;
}
return false;
} }
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()

View File

@ -1,13 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Lists;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu
{ {
public partial class OsuInputManager : RulesetInputManager<OsuAction> public partial class OsuInputManager : RulesetInputManager<OsuAction>
{ {
public IEnumerable<OsuAction> PressedActions => KeyBindingContainer.PressedActions; public SlimReadOnlyListWrapper<OsuAction> PressedActions => KeyBindingContainer.PressedActions;
/// <summary> /// <summary>
/// Whether gameplay input buttons should be allowed. /// Whether gameplay input buttons should be allowed.

View File

@ -5,7 +5,6 @@ using System;
using System.Globalization; using System.Globalization;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -111,42 +110,21 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{ {
spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0"); spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
}, true); }, true);
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
} }
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
if (!spmContainer.IsPresent && drawableSpinner.Result?.TimeStarted != null) updateSpmAlpha();
fadeCounterOnTimeStart();
} }
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) private void updateSpmAlpha()
{
if (!(drawableHitObject is DrawableSpinner))
return;
fadeCounterOnTimeStart();
}
private void fadeCounterOnTimeStart()
{ {
if (drawableSpinner.Result?.TimeStarted is double startTime) if (drawableSpinner.Result?.TimeStarted is double startTime)
{ spmContainer.Alpha = (float)Math.Clamp((Clock.CurrentTime - startTime) / drawableSpinner.HitObject.TimeFadeIn, 0, 1);
using (BeginAbsoluteSequence(startTime)) else
spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn); spmContainer.Alpha = 0;
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableSpinner.IsNotNull())
drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms;
} }
} }
} }

View File

@ -25,8 +25,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{ {
Texture = textures.Get(@"Gameplay/osu/approachcircle").WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS * 2); Texture = textures.Get(@"Gameplay/osu/approachcircle").WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS * 2);
// account for the sprite being used for the default approach circle being taken from stable, // In triangles and argon skins, we expanded hitcircles to take up the full 128 px which are clickable,
// when hitcircles have 5px padding on each size. this should be removed if we update the sprite. // but still use the old approach circle sprite. To make it feel correct (ie. disappear as it collides
// with the hitcircle, *not when it overlaps the border*) we need to expand it slightly.
Scale = new Vector2(128 / 118f); Scale = new Vector2(128 / 118f);
} }

View File

@ -5,7 +5,6 @@ using System;
using System.Globalization; using System.Globalization;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -117,42 +116,21 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{ {
spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0"); spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
}, true); }, true);
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
} }
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
if (!spmContainer.IsPresent && drawableSpinner.Result?.TimeStarted != null) updateSpmAlpha();
fadeCounterOnTimeStart();
} }
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) private void updateSpmAlpha()
{
if (!(drawableHitObject is DrawableSpinner))
return;
fadeCounterOnTimeStart();
}
private void fadeCounterOnTimeStart()
{ {
if (drawableSpinner.Result?.TimeStarted is double startTime) if (drawableSpinner.Result?.TimeStarted is double startTime)
{ spmContainer.Alpha = (float)Math.Clamp((Clock.CurrentTime - startTime) / drawableSpinner.HitObject.TimeFadeIn, 0, 1);
using (BeginAbsoluteSequence(startTime)) else
spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn); spmContainer.Alpha = 0;
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableSpinner.IsNotNull())
drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms;
} }
} }
} }

View File

@ -44,7 +44,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
SnakingOut.BindTo(configSnakingOut); SnakingOut.BindTo(configSnakingOut);
BorderSize = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderBorderSize)?.Value ?? 1;
BorderColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBorder)?.Value ?? Color4.White; BorderColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBorder)?.Value ?? Color4.White;
} }

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
@ -33,14 +32,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
drawableSpinner.HitObjectApplied += resetState; drawableSpinner.HitObjectApplied += resetState;
} }
private RotationRecord lastRecord;
public void SetRotation(float currentRotation) public void SetRotation(float currentRotation)
{ {
// If we've gone back in time, it's fine to work with a fresh set of records for now // If we've gone back in time, it's fine to work with a fresh set of records for now
if (records.Count > 0 && Time.Current < records.Last().Time) if (records.Count > 0 && Time.Current < lastRecord.Time)
records.Clear(); records.Clear();
// Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result. // Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result.
if (records.Count > 0 && Precision.AlmostEquals(Time.Current, records.Last().Time)) if (records.Count > 0 && Precision.AlmostEquals(Time.Current, lastRecord.Time))
return; return;
if (records.Count > 0) if (records.Count > 0)
@ -52,11 +53,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
result.Value = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360; result.Value = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360;
} }
records.Enqueue(new RotationRecord { Rotation = currentRotation, Time = Time.Current }); records.Enqueue(lastRecord = new RotationRecord { Rotation = currentRotation, Time = Time.Current });
} }
private void resetState(DrawableHitObject hitObject) private void resetState(DrawableHitObject hitObject)
{ {
lastRecord = default;
result.Value = 0; result.Value = 0;
records.Clear(); records.Clear();
} }

View File

@ -8,7 +8,6 @@ using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy namespace osu.Game.Rulesets.Osu.Skinning.Legacy
@ -26,10 +25,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
var texture = skin.GetTexture(@"approachcircle"); var texture = skin.GetTexture(@"approachcircle");
Debug.Assert(texture != null); Debug.Assert(texture != null);
Texture = texture.WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS * 2); Texture = texture.WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS * 2);
// account for the sprite being used for the default approach circle being taken from stable,
// when hitcircles have 5px padding on each size. this should be removed if we update the sprite.
Scale = new Vector2(128 / 118f);
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
// Note that the scale adjust here is 2 instead of DrawableSliderBall.FOLLOW_AREA to match legacy behaviour. // Note that the scale adjust here is 2 instead of DrawableSliderBall.FOLLOW_AREA to match legacy behaviour.
// This means the actual tracking area for gameplay purposes is larger than the sprite (but skins may be accounting for this). // This means the actual tracking area for gameplay purposes is larger than the sprite (but skins may be accounting for this).
this.ScaleTo(0.5f).ScaleTo(2f, Math.Min(180f, remainingTime), Easing.Out) this.ScaleTo(1f).ScaleTo(2f, Math.Min(180f, remainingTime), Easing.Out)
.FadeTo(0).FadeTo(1f, Math.Min(60f, remainingTime)); .FadeTo(0).FadeTo(1f, Math.Min(60f, remainingTime));
} }

View File

@ -92,8 +92,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt)) using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
this.FadeOut(); this.FadeOut();
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2)) using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn))
this.FadeInFromZero(spinner.TimeFadeIn / 2); this.FadeInFromZero(spinner.TimeFadeIn);
} }
protected override void Update() protected override void Update()

View File

@ -23,29 +23,35 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private partial class LegacyDrawableSliderPath : DrawableSliderPath private partial class LegacyDrawableSliderPath : DrawableSliderPath
{ {
private const float shadow_portion = 1 - (OsuLegacySkinTransformer.LEGACY_CIRCLE_RADIUS / OsuHitObject.OBJECT_RADIUS);
protected new float CalculatedBorderPortion
// Roughly matches osu!stable's slider border portions.
=> base.CalculatedBorderPortion * 0.77f;
protected override Color4 ColourAt(float position) protected override Color4 ColourAt(float position)
{ {
float realBorderPortion = shadow_portion + CalculatedBorderPortion; // https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/Graphics/Renderers/MmSliderRendererGL.cs#L99
float realGradientPortion = 1 - realBorderPortion; // float aaWidth = Math.Min(Math.Max(0.5f / PathRadius, 3.0f / 256.0f), 1.0f / 16.0f);
// applying the aa_width constant from stable makes sliders blurry, especially on CS>5. set to zero for now.
if (position <= shadow_portion) // this might be related to SmoothPath applying AA internally, but disabling that does not seem to have much of an effect.
return new Color4(0f, 0f, 0f, 0.25f * position / shadow_portion); const float aa_width = 0f;
if (position <= realBorderPortion)
return BorderColour;
position -= realBorderPortion;
Color4 shadow = new Color4(0, 0, 0, 0.25f);
Color4 outerColour = AccentColour.Darken(0.1f); Color4 outerColour = AccentColour.Darken(0.1f);
Color4 innerColour = lighten(AccentColour, 0.5f); Color4 innerColour = lighten(AccentColour, 0.5f);
return LegacyUtils.InterpolateNonLinear(position / realGradientPortion, outerColour, innerColour, 0, 1); // https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/Graphics/Renderers/MmSliderRendererGL.cs#L59-L70
const float shadow_portion = 1 - (OsuLegacySkinTransformer.LEGACY_CIRCLE_RADIUS / OsuHitObject.OBJECT_RADIUS);
const float border_portion = 0.1875f;
if (position <= shadow_portion - aa_width)
return LegacyUtils.InterpolateNonLinear(position, Color4.Black.Opacity(0f), shadow, 0, shadow_portion - aa_width);
if (position <= shadow_portion + aa_width)
return LegacyUtils.InterpolateNonLinear(position, shadow, BorderColour, shadow_portion - aa_width, shadow_portion + aa_width);
if (position <= border_portion - aa_width)
return BorderColour;
if (position <= border_portion + aa_width)
return LegacyUtils.InterpolateNonLinear(position, BorderColour, outerColour, border_portion - aa_width, border_portion + aa_width);
return LegacyUtils.InterpolateNonLinear(position, outerColour, innerColour, border_portion + aa_width, 1);
} }
/// <summary> /// <summary>

View File

@ -5,7 +5,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
public enum OsuSkinConfiguration public enum OsuSkinConfiguration
{ {
SliderBorderSize,
SliderPathRadius, SliderPathRadius,
CursorCentre, CursorCentre,
CursorExpand, CursorExpand,

View File

@ -4,7 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -228,7 +227,9 @@ namespace osu.Game.Rulesets.Osu.Skinning
int futurePointIndex = ~Source.SmokePoints.BinarySearch(new SmokePoint { Time = CurrentTime }, new SmokePoint.UpperBoundComparer()); int futurePointIndex = ~Source.SmokePoints.BinarySearch(new SmokePoint { Time = CurrentTime }, new SmokePoint.UpperBoundComparer());
points.Clear(); points.Clear();
points.AddRange(Source.SmokePoints.Skip(firstVisiblePointIndex).Take(futurePointIndex - firstVisiblePointIndex));
for (int i = firstVisiblePointIndex; i < futurePointIndex; i++)
points.Add(Source.SmokePoints[i]);
} }
protected sealed override void Draw(IRenderer renderer) protected sealed override void Draw(IRenderer renderer)

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -17,6 +18,7 @@ using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Tests.Editing namespace osu.Game.Tests.Editing
{ {
@ -228,6 +230,28 @@ namespace osu.Game.Tests.Editing
assertSnappedDistance(400, 400); assertSnappedDistance(400, 400);
} }
[Test]
public void TestUseCurrentSnap()
{
AddStep("add objects to beatmap", () =>
{
editorBeatmap.Add(new HitCircle { StartTime = 1000 });
editorBeatmap.Add(new HitCircle { Position = new Vector2(100), StartTime = 2000 });
});
AddStep("hover use current snap button", () => InputManager.MoveMouseTo(composer.ChildrenOfType<ExpandableButton>().Single()));
AddUntilStep("use current snap expanded", () => composer.ChildrenOfType<ExpandableButton>().Single().Expanded.Value, () => Is.True);
AddStep("seek before first object", () => EditorClock.Seek(0));
AddUntilStep("use current snap not available", () => composer.ChildrenOfType<ExpandableButton>().Single().Enabled.Value, () => Is.False);
AddStep("seek to between objects", () => EditorClock.Seek(1500));
AddUntilStep("use current snap available", () => composer.ChildrenOfType<ExpandableButton>().Single().Enabled.Value, () => Is.True);
AddStep("seek after last object", () => EditorClock.Seek(2500));
AddUntilStep("use current snap not available", () => composer.ChildrenOfType<ExpandableButton>().Single().Enabled.Value, () => Is.False);
}
private void assertSnapDistance(float expectedDistance, HitObject? referenceObject, bool includeSliderVelocity) private void assertSnapDistance(float expectedDistance, HitObject? referenceObject, bool includeSliderVelocity)
=> AddAssert($"distance is {expectedDistance}", () => composer.DistanceSnapProvider.GetBeatSnapDistanceAt(referenceObject ?? new HitObject(), includeSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON)); => AddAssert($"distance is {expectedDistance}", () => composer.DistanceSnapProvider.GetBeatSnapDistanceAt(referenceObject ?? new HitObject(), includeSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));

View File

@ -0,0 +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 NUnit.Framework;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Rulesets.Osu;
using osu.Game.Tests.Beatmaps;
using osuTK;
namespace osu.Game.Tests.Visual.Beatmaps
{
public partial class TestSceneDifficultyIcon : OsuTestScene
{
private FillFlowContainer<DifficultyIcon> fill = null!;
protected override void LoadComplete()
{
base.LoadComplete();
Child = fill = new FillFlowContainer<DifficultyIcon>
{
AutoSizeAxes = Axes.Y,
Width = 300,
Direction = FillDirection.Full,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
}
[Test]
public void CreateDifficultyIcon()
{
AddRepeatStep("create difficulty icon", () =>
{
var rulesetInfo = new OsuRuleset().RulesetInfo;
var beatmapInfo = new TestBeatmap(rulesetInfo).BeatmapInfo;
beatmapInfo.Difficulty.ApproachRate = RNG.Next(0, 10);
beatmapInfo.Difficulty.CircleSize = RNG.Next(0, 10);
beatmapInfo.Difficulty.OverallDifficulty = RNG.Next(0, 10);
beatmapInfo.Difficulty.DrainRate = RNG.Next(0, 10);
beatmapInfo.StarRating = RNG.NextSingle(0, 10);
beatmapInfo.BPM = RNG.Next(60, 300);
fill.Add(new DifficultyIcon(beatmapInfo, rulesetInfo)
{
Scale = new Vector2(2),
});
}, 10);
AddStep("no tooltip", () => fill.ForEach(icon => icon.TooltipType = DifficultyIconTooltipType.None));
AddStep("basic tooltip", () => fill.ForEach(icon => icon.TooltipType = DifficultyIconTooltipType.StarRating));
AddStep("extended tooltip", () => fill.ForEach(icon => icon.TooltipType = DifficultyIconTooltipType.Extended));
}
}
}

View File

@ -8,11 +8,13 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Login; using osu.Game.Overlays.Login;
using osu.Game.Overlays.Settings;
using osu.Game.Users.Drawables; using osu.Game.Users.Drawables;
using osuTK.Input; using osuTK.Input;
@ -25,6 +27,9 @@ namespace osu.Game.Tests.Visual.Menus
private LoginOverlay loginOverlay = null!; private LoginOverlay loginOverlay = null!;
[Resolved]
private OsuConfigManager configManager { get; set; } = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
@ -156,5 +161,36 @@ namespace osu.Game.Tests.Visual.Menus
}); });
AddAssert("login overlay is hidden", () => loginOverlay.State.Value == Visibility.Hidden); AddAssert("login overlay is hidden", () => loginOverlay.State.Value == Visibility.Hidden);
} }
[Test]
public void TestUncheckingRememberUsernameClearsIt()
{
AddStep("logout", () => API.Logout());
AddStep("set username", () => configManager.SetValue(OsuSetting.Username, "test_user"));
AddStep("set remember password", () => configManager.SetValue(OsuSetting.SavePassword, true));
AddStep("uncheck remember username", () =>
{
InputManager.MoveMouseTo(loginOverlay.ChildrenOfType<SettingsCheckbox>().First());
InputManager.Click(MouseButton.Left);
});
AddAssert("remember username off", () => configManager.Get<bool>(OsuSetting.SaveUsername), () => Is.False);
AddAssert("remember password off", () => configManager.Get<bool>(OsuSetting.SavePassword), () => Is.False);
AddAssert("username cleared", () => configManager.Get<string>(OsuSetting.Username), () => Is.Empty);
}
[Test]
public void TestUncheckingRememberPasswordClearsToken()
{
AddStep("logout", () => API.Logout());
AddStep("set token", () => configManager.SetValue(OsuSetting.Token, "test_token"));
AddStep("set remember password", () => configManager.SetValue(OsuSetting.SavePassword, true));
AddStep("uncheck remember token", () =>
{
InputManager.MoveMouseTo(loginOverlay.ChildrenOfType<SettingsCheckbox>().Last());
InputManager.Click(MouseButton.Left);
});
AddAssert("remember password off", () => configManager.Get<bool>(OsuSetting.SavePassword), () => Is.False);
AddAssert("token cleared", () => configManager.Get<string>(OsuSetting.Token), () => Is.Empty);
}
} }
} }

View File

@ -28,6 +28,21 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("state is top level", () => buttons.State == ButtonSystemState.TopLevel); AddAssert("state is top level", () => buttons.State == ButtonSystemState.TopLevel);
} }
[Test]
public void TestFastShortcutKeys()
{
AddAssert("state is initial", () => buttons.State == ButtonSystemState.Initial);
AddStep("press P three times", () =>
{
InputManager.Key(Key.P);
InputManager.Key(Key.P);
InputManager.Key(Key.P);
});
AddAssert("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
}
[Test] [Test]
public void TestShortcutKeys() public void TestShortcutKeys()
{ {

View File

@ -81,16 +81,17 @@ namespace osu.Game.Tests.Visual.Online
}, },
// Taken from https://github.com/ppy/osu/issues/13993#issuecomment-885994077 // Taken from https://github.com/ppy/osu/issues/13993#issuecomment-885994077
new[] { "Problematic", @"My tablet doesn't work :( It's a Huion 420 and it's apparently incompatible with OpenTablet Driver. The warning I get is: ""DeviceInUseException: Device is currently in use by another kernel module. To fix this issue, please follow the instructions from https://github.com/OpenTabletDriver/OpenTabletDriver/wiki/Linux-FAQ#arg umentoutofrangeexception-value-0-15"" and it repeats 4 times on the notification before logging subsequent warnings. Checking the logs, it looks for other Huion tablets before sending the notification (e.g. ""2021-07-23 03:52:33 [verbose]: Detect: Searching for tablet 'Huion WH1409 V2' 20 2021-07-23 03:52:33 [error]: DeviceInUseException: Device is currently in use by another kernel module. To fix this issue, please follow the instructions from https://github.com/OpenTabletDriver/OpenTabletDriver/wiki/Linux-FAQ#arg umentoutofrangeexception-value-0-15"") I use an Arch based installation of Linux and the tablet runs perfectly with Digimend kernel driver, with area configuration, pen pressure, etc. On osu!lazer the cursor disappears until I set it to ""Borderless"" instead of ""Fullscreen"" and even after it shows up, it goes to the bottom left corner as soon as a map starts. I have honestly 0 idea of whats going on at this point.", },
new[] new[]
{ {
"Problematic", @"My tablet doesn't work :( "Code Block", @"User not found! ;_;
It's a Huion 420 and it's apparently incompatible with OpenTablet Driver. The warning I get is: ""DeviceInUseException: Device is currently in use by another kernel module. To fix this issue, please follow the instructions from https://github.com/OpenTabletDriver/OpenTabletDriver/wiki/Linux-FAQ#arg umentoutofrangeexception-value-0-15"" and it repeats 4 times on the notification before logging subsequent warnings.
Checking the logs, it looks for other Huion tablets before sending the notification (e.g. There are a few possible reasons for this:
""2021-07-23 03:52:33 [verbose]: Detect: Searching for tablet 'Huion WH1409 V2'
20 2021-07-23 03:52:33 [error]: DeviceInUseException: Device is currently in use by another kernel module. To fix this issue, please follow the instructions from https://github.com/OpenTabletDriver/OpenTabletDriver/wiki/Linux-FAQ#arg umentoutofrangeexception-value-0-15"") They may have changed their username.
I use an Arch based installation of Linux and the tablet runs perfectly with Digimend kernel driver, with area configuration, pen pressure, etc. On osu!lazer the cursor disappears until I set it to ""Borderless"" instead of ""Fullscreen"" and even after it shows up, it goes to the bottom left corner as soon as a map starts. The account may be temporarily unavailable due to security or abuse issues.
I have honestly 0 idea of whats going on at this point." You may have made a typo!"
} },
}; };
} }
} }

View File

@ -137,6 +137,11 @@ namespace osu.Game.Tests.Visual.Online
@"top_ranks", @"top_ranks",
@"medals" @"medals"
}, },
RankHighest = new APIUser.UserRankHighest
{
Rank = 1,
UpdatedAt = DateTimeOffset.Now,
},
Statistics = new UserStatistics Statistics = new UserStatistics
{ {
IsRanked = true, IsRanked = true,

View File

@ -0,0 +1,42 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Expanded.Accuracy;
using osuTK;
namespace osu.Game.Tests.Visual.Ranking
{
public partial class TestSceneGradedCircles : OsuTestScene
{
private readonly GradedCircles ring;
public TestSceneGradedCircles()
{
ScoreProcessor scoreProcessor = new OsuRuleset().CreateScoreProcessor();
double accuracyX = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.X);
double accuracyS = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.S);
double accuracyA = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.A);
double accuracyB = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.B);
double accuracyC = scoreProcessor.AccuracyCutoffFromRank(ScoreRank.C);
Add(ring = new GradedCircles(accuracyC, accuracyB, accuracyA, accuracyS, accuracyX)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(400)
});
}
protected override void LoadComplete()
{
base.LoadComplete();
AddSliderStep("Progress", 0.0, 1.0, 1.0, p => ring.Progress = p);
}
}
}

View File

@ -629,7 +629,8 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
var sets = new List<BeatmapSetInfo>(); var sets = new List<BeatmapSetInfo>();
const string zzz_string = "zzzzz"; const string zzz_lowercase = "zzzzz";
const string zzz_uppercase = "ZZZZZ";
AddStep("Populuate beatmap sets", () => AddStep("Populuate beatmap sets", () =>
{ {
@ -640,10 +641,16 @@ namespace osu.Game.Tests.Visual.SongSelect
var set = TestResources.CreateTestBeatmapSetInfo(); var set = TestResources.CreateTestBeatmapSetInfo();
if (i == 4) if (i == 4)
set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_string); set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_uppercase);
if (i == 8)
set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_lowercase);
if (i == 12)
set.Beatmaps.ForEach(b => b.Metadata.Author.Username = zzz_uppercase);
if (i == 16) if (i == 16)
set.Beatmaps.ForEach(b => b.Metadata.Author.Username = zzz_string); set.Beatmaps.ForEach(b => b.Metadata.Author.Username = zzz_lowercase);
sets.Add(set); sets.Add(set);
} }
@ -652,9 +659,11 @@ namespace osu.Game.Tests.Visual.SongSelect
loadBeatmaps(sets); loadBeatmaps(sets);
AddStep("Sort by author", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Author }, false)); AddStep("Sort by author", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Author }, false));
AddAssert($"Check {zzz_string} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Author.Username == zzz_string); AddAssert($"Check {zzz_uppercase} is last", () => carousel.BeatmapSets.Last().Metadata.Author.Username == zzz_uppercase);
AddAssert($"Check {zzz_lowercase} is second last", () => carousel.BeatmapSets.SkipLast(1).Last().Metadata.Author.Username == zzz_lowercase);
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false)); AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
AddAssert($"Check {zzz_string} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Artist == zzz_string); AddAssert($"Check {zzz_uppercase} is last", () => carousel.BeatmapSets.Last().Metadata.Artist == zzz_uppercase);
AddAssert($"Check {zzz_lowercase} is second last", () => carousel.BeatmapSets.SkipLast(1).Last().Metadata.Artist == zzz_lowercase);
} }
/// <summary> /// <summary>

View File

@ -788,7 +788,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().All(col => col.IsPresent)); AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().All(col => col.IsPresent));
AddStep("set search", () => modSelectOverlay.SearchTerm = "HD"); AddStep("set search", () => modSelectOverlay.SearchTerm = "HD");
AddAssert("one column visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 1); AddAssert("two columns visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 2);
AddStep("filter out everything", () => modSelectOverlay.SearchTerm = "Some long search term with no matches"); AddStep("filter out everything", () => modSelectOverlay.SearchTerm = "Some long search term with no matches");
AddAssert("no columns visible", () => this.ChildrenOfType<ModColumn>().All(col => !col.IsPresent)); AddAssert("no columns visible", () => this.ChildrenOfType<ModColumn>().All(col => !col.IsPresent));
@ -812,7 +812,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().All(col => col.IsPresent)); AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().All(col => col.IsPresent));
AddStep("set search", () => modSelectOverlay.SearchTerm = "fail"); AddStep("set search", () => modSelectOverlay.SearchTerm = "fail");
AddAssert("one column visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 2); AddAssert("one column visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 1);
AddStep("hide", () => modSelectOverlay.Hide()); AddStep("hide", () => modSelectOverlay.Hide());
AddStep("show", () => modSelectOverlay.Show()); AddStep("show", () => modSelectOverlay.Show());

View File

@ -17,12 +17,15 @@ namespace osu.Game.Audio.Effects
/// </summary> /// </summary>
public const int MAX_LOWPASS_CUTOFF = 22049; // nyquist - 1hz public const int MAX_LOWPASS_CUTOFF = 22049; // nyquist - 1hz
/// <summary>
/// Whether this filter is currently attached to the audio track and thus applying an adjustment.
/// </summary>
public bool IsAttached { get; private set; }
private readonly AudioMixer mixer; private readonly AudioMixer mixer;
private readonly BQFParameters filter; private readonly BQFParameters filter;
private readonly BQFType type; private readonly BQFType type;
private bool isAttached;
private readonly Cached filterApplication = new Cached(); private readonly Cached filterApplication = new Cached();
private int cutoff; private int cutoff;
@ -132,22 +135,22 @@ namespace osu.Game.Audio.Effects
private void ensureAttached() private void ensureAttached()
{ {
if (isAttached) if (IsAttached)
return; return;
Debug.Assert(!mixer.Effects.Contains(filter)); Debug.Assert(!mixer.Effects.Contains(filter));
mixer.Effects.Add(filter); mixer.Effects.Add(filter);
isAttached = true; IsAttached = true;
} }
private void ensureDetached() private void ensureDetached()
{ {
if (!isAttached) if (!IsAttached)
return; return;
Debug.Assert(mixer.Effects.Contains(filter)); Debug.Assert(mixer.Effects.Contains(filter));
mixer.Effects.Remove(filter); mixer.Effects.Remove(filter);
isAttached = false; IsAttached = false;
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

View File

@ -361,13 +361,20 @@ namespace osu.Game.Beatmaps
/// </summary> /// </summary>
public void DeleteVideos(List<BeatmapSetInfo> items, bool silent = false) public void DeleteVideos(List<BeatmapSetInfo> items, bool silent = false)
{ {
if (items.Count == 0) return; const string no_videos_message = "No videos found to delete!";
if (items.Count == 0)
{
if (!silent)
PostNotification?.Invoke(new ProgressCompletionNotification { Text = no_videos_message });
return;
}
var notification = new ProgressNotification var notification = new ProgressNotification
{ {
Progress = 0, Progress = 0,
Text = $"Preparing to delete all {HumanisedModelName} videos...", Text = $"Preparing to delete all {HumanisedModelName} videos...",
CompletionText = "No videos found to delete!", CompletionText = no_videos_message,
State = ProgressNotificationState.Active, State = ProgressNotificationState.Active,
}; };

View File

@ -14,6 +14,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -31,14 +32,16 @@ namespace osu.Game.Beatmaps.Drawables
} }
/// <summary> /// <summary>
/// Whether to display a tooltip on hover. Only works if a beatmap was provided at construction time. /// Which type of tooltip to show. Only works if a beatmap was provided at construction time.
/// </summary> /// </summary>
public bool ShowTooltip { get; set; } = true; public DifficultyIconTooltipType TooltipType { get; set; } = DifficultyIconTooltipType.StarRating;
private readonly IBeatmapInfo? beatmap; private readonly IBeatmapInfo? beatmap;
private readonly IRulesetInfo ruleset; private readonly IRulesetInfo ruleset;
private readonly Mod[]? mods;
private Drawable background = null!; private Drawable background = null!;
private readonly Container iconContainer; private readonly Container iconContainer;
@ -58,11 +61,14 @@ namespace osu.Game.Beatmaps.Drawables
/// Creates a new <see cref="DifficultyIcon"/>. Will use provided beatmap's <see cref="BeatmapInfo.StarRating"/> for initial value. /// Creates a new <see cref="DifficultyIcon"/>. Will use provided beatmap's <see cref="BeatmapInfo.StarRating"/> for initial value.
/// </summary> /// </summary>
/// <param name="beatmap">The beatmap to be displayed in the tooltip, and to be used for the initial star rating value.</param> /// <param name="beatmap">The beatmap to be displayed in the tooltip, and to be used for the initial star rating value.</param>
/// <param name="mods">An array of mods to account for in the calculations</param>
/// <param name="ruleset">An optional ruleset to be used for the icon display, in place of the beatmap's ruleset.</param> /// <param name="ruleset">An optional ruleset to be used for the icon display, in place of the beatmap's ruleset.</param>
public DifficultyIcon(IBeatmapInfo beatmap, IRulesetInfo? ruleset = null) public DifficultyIcon(IBeatmapInfo beatmap, IRulesetInfo? ruleset = null, Mod[]? mods = null)
: this(ruleset ?? beatmap.Ruleset) : this(ruleset ?? beatmap.Ruleset)
{ {
this.beatmap = beatmap; this.beatmap = beatmap;
this.mods = mods;
Current.Value = new StarDifficulty(beatmap.StarRating, 0); Current.Value = new StarDifficulty(beatmap.StarRating, 0);
} }
@ -127,6 +133,24 @@ namespace osu.Game.Beatmaps.Drawables
GetCustomTooltip() => new DifficultyIconTooltip(); GetCustomTooltip() => new DifficultyIconTooltip();
DifficultyIconTooltipContent IHasCustomTooltip<DifficultyIconTooltipContent>. DifficultyIconTooltipContent IHasCustomTooltip<DifficultyIconTooltipContent>.
TooltipContent => (ShowTooltip && beatmap != null ? new DifficultyIconTooltipContent(beatmap, Current) : null)!; TooltipContent => (TooltipType != DifficultyIconTooltipType.None && beatmap != null ? new DifficultyIconTooltipContent(beatmap, Current, ruleset, mods, TooltipType) : null)!;
}
public enum DifficultyIconTooltipType
{
/// <summary>
/// No tooltip.
/// </summary>
None,
/// <summary>
/// Star rating only.
/// </summary>
StarRating,
/// <summary>
/// Star rating, OD, HP, CS, AR, length, BPM, and max combo.
/// </summary>
Extended,
} }
} }

View File

@ -1,8 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable using System;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -11,14 +11,25 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osuTK; using osuTK;
namespace osu.Game.Beatmaps.Drawables namespace osu.Game.Beatmaps.Drawables
{ {
internal partial class DifficultyIconTooltip : VisibilityContainer, ITooltip<DifficultyIconTooltipContent> internal partial class DifficultyIconTooltip : VisibilityContainer, ITooltip<DifficultyIconTooltipContent>
{ {
private OsuSpriteText difficultyName; private OsuSpriteText difficultyName = null!;
private StarRatingDisplay starRating; private StarRatingDisplay starRating = null!;
private OsuSpriteText overallDifficulty = null!;
private OsuSpriteText drainRate = null!;
private OsuSpriteText circleSize = null!;
private OsuSpriteText approachRate = null!;
private OsuSpriteText bpm = null!;
private OsuSpriteText length = null!;
private FillFlowContainer difficultyFillFlowContainer = null!;
private FillFlowContainer miscFillFlowContainer = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
@ -31,7 +42,6 @@ namespace osu.Game.Beatmaps.Drawables
{ {
new Box new Box
{ {
Alpha = 0.9f,
Colour = colours.Gray3, Colour = colours.Gray3,
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}, },
@ -49,19 +59,49 @@ namespace osu.Game.Beatmaps.Drawables
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold), Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold)
}, },
starRating = new StarRatingDisplay(default, StarRatingDisplaySize.Small) starRating = new StarRatingDisplay(default, StarRatingDisplaySize.Small)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
},
difficultyFillFlowContainer = new FillFlowContainer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Alpha = 0,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Children = new Drawable[]
{
circleSize = new OsuSpriteText { Font = OsuFont.GetFont(size: 14) },
drainRate = new OsuSpriteText { Font = OsuFont.GetFont(size: 14) },
overallDifficulty = new OsuSpriteText { Font = OsuFont.GetFont(size: 14) },
approachRate = new OsuSpriteText { Font = OsuFont.GetFont(size: 14) },
}
},
miscFillFlowContainer = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Children = new Drawable[]
{
length = new OsuSpriteText { Font = OsuFont.GetFont(size: 14) },
bpm = new OsuSpriteText { Font = OsuFont.GetFont(size: 14) },
}
} }
} }
} }
}; };
} }
private DifficultyIconTooltipContent displayedContent; private DifficultyIconTooltipContent? displayedContent;
public void SetContent(DifficultyIconTooltipContent content) public void SetContent(DifficultyIconTooltipContent content)
{ {
@ -72,6 +112,45 @@ namespace osu.Game.Beatmaps.Drawables
starRating.Current.BindTarget = displayedContent.Difficulty; starRating.Current.BindTarget = displayedContent.Difficulty;
difficultyName.Text = displayedContent.BeatmapInfo.DifficultyName; difficultyName.Text = displayedContent.BeatmapInfo.DifficultyName;
if (displayedContent.TooltipType == DifficultyIconTooltipType.StarRating)
{
difficultyFillFlowContainer.Hide();
miscFillFlowContainer.Hide();
return;
}
difficultyFillFlowContainer.Show();
miscFillFlowContainer.Show();
double rate = 1;
if (displayedContent.Mods != null)
{
foreach (var mod in displayedContent.Mods.OfType<IApplicableToRate>())
rate = mod.ApplyToRate(0, rate);
}
double bpmAdjusted = displayedContent.BeatmapInfo.BPM * rate;
BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(displayedContent.BeatmapInfo.Difficulty);
if (displayedContent.Mods != null)
{
foreach (var mod in displayedContent.Mods.OfType<IApplicableToDifficulty>())
mod.ApplyToDifficulty(originalDifficulty);
}
Ruleset ruleset = displayedContent.Ruleset.CreateInstance();
BeatmapDifficulty adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(originalDifficulty, rate);
circleSize.Text = @"CS: " + adjustedDifficulty.CircleSize.ToString(@"0.##");
drainRate.Text = @" HP: " + adjustedDifficulty.DrainRate.ToString(@"0.##");
approachRate.Text = @" AR: " + adjustedDifficulty.ApproachRate.ToString(@"0.##");
overallDifficulty.Text = @" OD: " + adjustedDifficulty.OverallDifficulty.ToString(@"0.##");
length.Text = "Length: " + TimeSpan.FromMilliseconds(displayedContent.BeatmapInfo.Length / rate).ToString(@"mm\:ss");
bpm.Text = " BPM: " + Math.Round(bpmAdjusted, 0);
} }
public void Move(Vector2 pos) => Position = pos; public void Move(Vector2 pos) => Position = pos;
@ -85,11 +164,20 @@ namespace osu.Game.Beatmaps.Drawables
{ {
public readonly IBeatmapInfo BeatmapInfo; public readonly IBeatmapInfo BeatmapInfo;
public readonly IBindable<StarDifficulty> Difficulty; public readonly IBindable<StarDifficulty> Difficulty;
public readonly IRulesetInfo Ruleset;
public readonly Mod[]? Mods;
public readonly DifficultyIconTooltipType TooltipType;
public DifficultyIconTooltipContent(IBeatmapInfo beatmapInfo, IBindable<StarDifficulty> difficulty) public DifficultyIconTooltipContent(IBeatmapInfo beatmapInfo, IBindable<StarDifficulty> difficulty, IRulesetInfo rulesetInfo, Mod[]? mods, DifficultyIconTooltipType tooltipType)
{ {
if (tooltipType == DifficultyIconTooltipType.None)
throw new ArgumentOutOfRangeException(nameof(tooltipType), tooltipType, "Cannot instantiate a tooltip without a type");
BeatmapInfo = beatmapInfo; BeatmapInfo = beatmapInfo;
Difficulty = difficulty; Difficulty = difficulty;
Ruleset = rulesetInfo;
Mods = mods;
TooltipType = tooltipType;
} }
} }
} }

View File

@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps
private readonly OffsetCorrectionClock? userGlobalOffsetClock; private readonly OffsetCorrectionClock? userGlobalOffsetClock;
private readonly OffsetCorrectionClock? platformOffsetClock; private readonly OffsetCorrectionClock? platformOffsetClock;
private readonly OffsetCorrectionClock? userBeatmapOffsetClock; private readonly FramedOffsetClock? userBeatmapOffsetClock;
private readonly IFrameBasedClock finalClockSource; private readonly IFrameBasedClock finalClockSource;
@ -70,7 +70,7 @@ namespace osu.Game.Beatmaps
userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock); userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock);
// User per-beatmap offset will be applied to this final clock. // User per-beatmap offset will be applied to this final clock.
finalClockSource = userBeatmapOffsetClock = new OffsetCorrectionClock(userGlobalOffsetClock); finalClockSource = userBeatmapOffsetClock = new FramedOffsetClock(userGlobalOffsetClock);
} }
else else
{ {
@ -122,7 +122,7 @@ namespace osu.Game.Beatmaps
Debug.Assert(userBeatmapOffsetClock != null); Debug.Assert(userBeatmapOffsetClock != null);
Debug.Assert(platformOffsetClock != null); Debug.Assert(platformOffsetClock != null);
return userGlobalOffsetClock.RateAdjustedOffset + userBeatmapOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset; return userGlobalOffsetClock.RateAdjustedOffset + userBeatmapOffsetClock.Offset + platformOffsetClock.RateAdjustedOffset;
} }
} }

View File

@ -115,6 +115,11 @@ namespace osu.Game.Collections
}; };
} }
public override bool IsPresent => base.IsPresent
// Safety for low pass filter potentially getting stuck in applied state due to
// transforms on `this` causing children to no longer be updated.
|| lowPassFilter.IsAttached;
protected override void PopIn() protected override void PopIn()
{ {
lowPassFilter.CutoffTo(300, 100, Easing.OutCubic); lowPassFilter.CutoffTo(300, 100, Easing.OutCubic);

View File

@ -77,12 +77,19 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.SavePassword, false).ValueChanged += enabled => SetDefault(OsuSetting.SavePassword, false).ValueChanged += enabled =>
{ {
if (enabled.NewValue) SetValue(OsuSetting.SaveUsername, true); if (enabled.NewValue)
SetValue(OsuSetting.SaveUsername, true);
else
GetBindable<string>(OsuSetting.Token).SetDefault();
}; };
SetDefault(OsuSetting.SaveUsername, true).ValueChanged += enabled => SetDefault(OsuSetting.SaveUsername, true).ValueChanged += enabled =>
{ {
if (!enabled.NewValue) SetValue(OsuSetting.SavePassword, false); if (!enabled.NewValue)
{
GetBindable<string>(OsuSetting.Username).SetDefault();
SetValue(OsuSetting.SavePassword, false);
}
}; };
SetDefault(OsuSetting.ExternalLinkWarning, true); SetDefault(OsuSetting.ExternalLinkWarning, true);

View File

@ -105,7 +105,12 @@ namespace osu.Game.Database
/// </summary> /// </summary>
public void Delete(List<TModel> items, bool silent = false) public void Delete(List<TModel> items, bool silent = false)
{ {
if (items.Count == 0) return; if (items.Count == 0)
{
if (!silent)
PostNotification?.Invoke(new ProgressCompletionNotification { Text = $"No {HumanisedModelName}s found to delete!" });
return;
}
var notification = new ProgressNotification var notification = new ProgressNotification
{ {
@ -142,7 +147,12 @@ namespace osu.Game.Database
/// </summary> /// </summary>
public void Undelete(List<TModel> items, bool silent = false) public void Undelete(List<TModel> items, bool silent = false)
{ {
if (!items.Any()) return; if (!items.Any())
{
if (!silent)
PostNotification?.Invoke(new ProgressCompletionNotification { Text = $"No {HumanisedModelName}s found to restore!" });
return;
}
var notification = new ProgressNotification var notification = new ProgressNotification
{ {

View File

@ -10,11 +10,11 @@ using osu.Game.Overlays;
namespace osu.Game.Graphics.Containers.Markdown namespace osu.Game.Graphics.Containers.Markdown
{ {
public partial class OsuMarkdownFencedCodeBlock : MarkdownFencedCodeBlock public partial class OsuMarkdownCodeBlock : MarkdownCodeBlock
{ {
// TODO : change to monospace font for this component // TODO : change to monospace font for this component
public OsuMarkdownFencedCodeBlock(FencedCodeBlock fencedCodeBlock) public OsuMarkdownCodeBlock(CodeBlock codeBlock)
: base(fencedCodeBlock) : base(codeBlock)
{ {
} }

View File

@ -67,7 +67,7 @@ namespace osu.Game.Graphics.Containers.Markdown
protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new OsuMarkdownHeading(headingBlock); protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new OsuMarkdownHeading(headingBlock);
protected override MarkdownFencedCodeBlock CreateFencedCodeBlock(FencedCodeBlock fencedCodeBlock) => new OsuMarkdownFencedCodeBlock(fencedCodeBlock); protected override MarkdownCodeBlock CreateCodeBlock(CodeBlock codeBlock) => new OsuMarkdownCodeBlock(codeBlock);
protected override MarkdownSeparator CreateSeparator(ThematicBreakBlock thematicBlock) => new OsuMarkdownSeparator(); protected override MarkdownSeparator CreateSeparator(ThematicBreakBlock thematicBlock) => new OsuMarkdownSeparator();

View File

@ -220,12 +220,16 @@ namespace osu.Game.Graphics.Cursor
{ {
activeCursor.FadeTo(1, 250, Easing.OutQuint); activeCursor.FadeTo(1, 250, Easing.OutQuint);
activeCursor.ScaleTo(1, 400, Easing.OutQuint); activeCursor.ScaleTo(1, 400, Easing.OutQuint);
activeCursor.RotateTo(0, 400, Easing.OutQuint);
dragRotationState = DragRotationState.NotDragging;
} }
protected override void PopOut() protected override void PopOut()
{ {
activeCursor.FadeTo(0, 250, Easing.OutQuint); activeCursor.FadeTo(0, 250, Easing.OutQuint);
activeCursor.ScaleTo(0.6f, 250, Easing.In); activeCursor.ScaleTo(0.6f, 250, Easing.In);
activeCursor.RotateTo(0, 400, Easing.OutQuint);
dragRotationState = DragRotationState.NotDragging;
} }
private void playTapSample(double baseFrequency = 1f) private void playTapSample(double baseFrequency = 1f)

View File

@ -104,16 +104,31 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString DeletedAllCollections => new TranslatableString(getKey(@"deleted_all_collections"), @"Deleted all collections!"); public static LocalisableString DeletedAllCollections => new TranslatableString(getKey(@"deleted_all_collections"), @"Deleted all collections!");
/// <summary>
/// "No collections found to delete!"
/// </summary>
public static LocalisableString NoCollectionsFoundToDelete => new TranslatableString(getKey(@"no_collections_found_to_delete"), @"No collections found to delete!");
/// <summary> /// <summary>
/// "Deleted all mod presets!" /// "Deleted all mod presets!"
/// </summary> /// </summary>
public static LocalisableString DeletedAllModPresets => new TranslatableString(getKey(@"deleted_all_mod_presets"), @"Deleted all mod presets!"); public static LocalisableString DeletedAllModPresets => new TranslatableString(getKey(@"deleted_all_mod_presets"), @"Deleted all mod presets!");
/// <summary>
/// "No mod presets found to delete!"
/// </summary>
public static LocalisableString NoModPresetsFoundToDelete => new TranslatableString(getKey(@"no_mod_presets_found_to_delete"), @"No mod presets found to delete!");
/// <summary> /// <summary>
/// "Restored all deleted mod presets!" /// "Restored all deleted mod presets!"
/// </summary> /// </summary>
public static LocalisableString RestoredAllDeletedModPresets => new TranslatableString(getKey(@"restored_all_deleted_mod_presets"), @"Restored all deleted mod presets!"); public static LocalisableString RestoredAllDeletedModPresets => new TranslatableString(getKey(@"restored_all_deleted_mod_presets"), @"Restored all deleted mod presets!");
/// <summary>
/// "No mod presets found to restore!"
/// </summary>
public static LocalisableString NoModPresetsFoundToRestore => new TranslatableString(getKey(@"no_mod_presets_found_to_restore"), @"No mod presets found to restore!");
/// <summary> /// <summary>
/// "Please select your osu!stable install location" /// "Please select your osu!stable install location"
/// </summary> /// </summary>

View File

@ -0,0 +1,49 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class StorageErrorDialogStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.StorageErrorDialog";
/// <summary>
/// "osu! storage error"
/// </summary>
public static LocalisableString StorageError => new TranslatableString(getKey(@"storage_error"), @"osu! storage error");
/// <summary>
/// "The specified osu! data location (&quot;{0}&quot;) is not accessible. If it is on external storage, please reconnect the device and try again."
/// </summary>
public static LocalisableString LocationIsNotAccessible(string? loc) => new TranslatableString(getKey(@"location_is_not_accessible"), @"The specified osu! data location (""{0}"") is not accessible. If it is on external storage, please reconnect the device and try again.", loc);
/// <summary>
/// "The specified osu! data location (&quot;{0}&quot;) is empty. If you have moved the files, please close osu! and move them back."
/// </summary>
public static LocalisableString LocationIsEmpty(string? loc2) => new TranslatableString(getKey(@"location_is_empty"), @"The specified osu! data location (""{0}"") is empty. If you have moved the files, please close osu! and move them back.", loc2);
/// <summary>
/// "Try again"
/// </summary>
public static LocalisableString TryAgain => new TranslatableString(getKey(@"try_again"), @"Try again");
/// <summary>
/// "Use default location until restart"
/// </summary>
public static LocalisableString UseDefaultLocation => new TranslatableString(getKey(@"use_default_location"), @"Use default location until restart");
/// <summary>
/// "Reset to default location"
/// </summary>
public static LocalisableString ResetToDefaultLocation => new TranslatableString(getKey(@"reset_to_default_location"), @"Reset to default location");
/// <summary>
/// "Start fresh at specified location"
/// </summary>
public static LocalisableString StartFresh => new TranslatableString(getKey(@"start_fresh"), @"Start fresh at specified location");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -34,6 +34,19 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"previous_usernames")] [JsonProperty(@"previous_usernames")]
public string[] PreviousUsernames; public string[] PreviousUsernames;
[JsonProperty(@"rank_highest")]
[CanBeNull]
public UserRankHighest RankHighest;
public class UserRankHighest
{
[JsonProperty(@"rank")]
public int Rank;
[JsonProperty(@"updated_at")]
public DateTimeOffset UpdatedAt;
}
[JsonProperty(@"country_code")] [JsonProperty(@"country_code")]
private string countryCodeString; private string countryCodeString;

View File

@ -55,7 +55,6 @@ using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.SkinEditor; using osu.Game.Overlays.SkinEditor;
using osu.Game.Overlays.Toolbar; using osu.Game.Overlays.Toolbar;
using osu.Game.Overlays.Volume; using osu.Game.Overlays.Volume;
using osu.Game.Performance;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens; using osu.Game.Screens;
@ -792,8 +791,6 @@ namespace osu.Game
protected virtual UpdateManager CreateUpdateManager() => new UpdateManager(); protected virtual UpdateManager CreateUpdateManager() => new UpdateManager();
protected virtual HighPerformanceSession CreateHighPerformanceSession() => new HighPerformanceSession();
protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything); protected override Container CreateScalingContainer() => new ScalingContainer(ScalingMode.Everything);
#region Beatmap progression #region Beatmap progression
@ -1088,8 +1085,6 @@ namespace osu.Game
loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true); loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true);
loadComponentSingleFile<IDialogOverlay>(new DialogOverlay(), topMostOverlayContent.Add, true); loadComponentSingleFile<IDialogOverlay>(new DialogOverlay(), topMostOverlayContent.Add, true);
loadComponentSingleFile(CreateHighPerformanceSession(), Add);
loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add); loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add);
Add(difficultyRecommender); Add(difficultyRecommender);

View File

@ -297,7 +297,7 @@ namespace osu.Game.Overlays.BeatmapSet
}, },
icon = new DifficultyIcon(beatmapInfo, ruleset) icon = new DifficultyIcon(beatmapInfo, ruleset)
{ {
ShowTooltip = false, TooltipType = DifficultyIconTooltipType.None,
Current = { Value = new StarDifficulty(beatmapInfo.StarRating, 0) }, Current = { Value = new StarDifficulty(beatmapInfo.StarRating, 0) },
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -66,13 +66,10 @@ namespace osu.Game.Overlays.Chat.ChannelList
Colour = colourProvider.Background4, Colour = colourProvider.Background4,
Alpha = 0f, Alpha = 0f,
}, },
new Container new GridContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = 18, Right = 10 }, Padding = new MarginPadding { Left = 18, Right = 10 },
Child = new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[] ColumnDimensions = new[]
{ {
new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize),
@ -98,9 +95,8 @@ namespace osu.Game.Overlays.Chat.ChannelList
createMentionPill(), createMentionPill(),
close = createCloseButton(), close = createCloseButton(),
} }
}, }
}, }
},
}; };
Action = () => OnRequestSelect?.Invoke(Channel); Action = () => OnRequestSelect?.Invoke(Channel);

View File

@ -27,6 +27,12 @@ namespace osu.Game.Overlays
public PopupDialog CurrentDialog { get; private set; } public PopupDialog CurrentDialog { get; private set; }
public override bool IsPresent => Scheduler.HasPendingTasks
|| dialogContainer.Children.Count > 0
// Safety for low pass filter potentially getting stuck in applied state due to
// transforms on `this` causing children to no longer be updated.
|| lowPassFilter.IsAttached;
public DialogOverlay() public DialogOverlay()
{ {
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
@ -95,8 +101,6 @@ namespace osu.Game.Overlays
} }
} }
public override bool IsPresent => Scheduler.HasPendingTasks || dialogContainer.Children.Count > 0;
protected override bool BlockNonPositionalInput => true; protected override bool BlockNonPositionalInput => true;
protected override void PopIn() protected override void PopIn()

View File

@ -19,6 +19,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
using osuTK; using osuTK;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
@ -168,7 +169,7 @@ namespace osu.Game.Overlays.Mods
foreach (var mod in mods.Value.OfType<IApplicableToRate>()) foreach (var mod in mods.Value.OfType<IApplicableToRate>())
rate = mod.ApplyToRate(0, rate); rate = mod.ApplyToRate(0, rate);
bpmDisplay.Current.Value = BeatmapInfo.Value.BPM * rate; bpmDisplay.Current.Value = FormatUtils.RoundBPM(BeatmapInfo.Value.BPM, rate);
BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(BeatmapInfo.Value.Difficulty); BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(BeatmapInfo.Value.Difficulty);
@ -194,11 +195,11 @@ namespace osu.Game.Overlays.Mods
RightContent.FadeTo(Collapsed.Value && !IsHovered ? 0 : 1, transition_duration, Easing.OutQuint); RightContent.FadeTo(Collapsed.Value && !IsHovered ? 0 : 1, transition_duration, Easing.OutQuint);
} }
private partial class BPMDisplay : RollingCounter<double> private partial class BPMDisplay : RollingCounter<int>
{ {
protected override double RollingDuration => 250; protected override double RollingDuration => 250;
protected override LocalisableString FormatCount(double count) => count.ToLocalisableString("0 BPM"); protected override LocalisableString FormatCount(int count) => count.ToLocalisableString("0 BPM");
protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText
{ {

View File

@ -67,8 +67,25 @@ namespace osu.Game.Overlays.Mods
private IModHotkeyHandler hotkeyHandler = null!; private IModHotkeyHandler hotkeyHandler = null!;
private Task? latestLoadTask; private Task? latestLoadTask;
private ICollection<ModPanel>? latestLoadedPanels; private ModPanel[]? latestLoadedPanels;
internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true && latestLoadedPanels?.All(panel => panel.Parent != null) == true; internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true && allPanelsLoaded;
private bool allPanelsLoaded
{
get
{
if (latestLoadedPanels == null)
return false;
foreach (var panel in latestLoadedPanels)
{
if (panel.Parent == null)
return false;
}
return true;
}
}
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;

View File

@ -77,11 +77,11 @@ namespace osu.Game.Overlays.Mods
/// <seealso cref="ModState.Visible"/> /// <seealso cref="ModState.Visible"/>
public bool Visible => modState.Visible; public bool Visible => modState.Visible;
public override IEnumerable<LocalisableString> FilterTerms => new[] public override IEnumerable<LocalisableString> FilterTerms => new LocalisableString[]
{ {
Mod.Name, Mod.Name,
Mod.Name.Replace(" ", string.Empty),
Mod.Acronym, Mod.Acronym,
Mod.Description
}; };
public override bool MatchingFilter public override bool MatchingFilter

View File

@ -15,6 +15,7 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -349,15 +350,18 @@ namespace osu.Game.Overlays.Mods
}); });
} }
private static readonly LocalisableString input_search_placeholder = Resources.Localisation.Web.CommonStrings.InputSearch;
private static readonly LocalisableString tab_to_search_placeholder = ModSelectOverlayStrings.TabToSearch;
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
SearchTextBox.PlaceholderText = SearchTextBox.HasFocus ? Resources.Localisation.Web.CommonStrings.InputSearch : ModSelectOverlayStrings.TabToSearch; SearchTextBox.PlaceholderText = SearchTextBox.HasFocus ? input_search_placeholder : tab_to_search_placeholder;
if (beatmapAttributesDisplay != null) if (beatmapAttributesDisplay != null)
{ {
float rightEdgeOfLastButton = footerButtonFlow.Last().ScreenSpaceDrawQuad.TopRight.X; float rightEdgeOfLastButton = footerButtonFlow[^1].ScreenSpaceDrawQuad.TopRight.X;
// this is cheating a bit; the 640 value is hardcoded based on how wide the expanded panel _generally_ is. // this is cheating a bit; the 640 value is hardcoded based on how wide the expanded panel _generally_ is.
// due to the transition applied, the raw screenspace quad of the panel cannot be used, as it will trigger an ugly feedback cycle of expanding and collapsing. // due to the transition applied, the raw screenspace quad of the panel cannot be used, as it will trigger an ugly feedback cycle of expanding and collapsing.

View File

@ -143,6 +143,13 @@ namespace osu.Game.Overlays.Profile.Header.Components
scoreRankInfo.Value.RankCount = user?.Statistics?.GradesCount[scoreRankInfo.Key] ?? 0; scoreRankInfo.Value.RankCount = user?.Statistics?.GradesCount[scoreRankInfo.Key] ?? 0;
detailGlobalRank.Content = user?.Statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; detailGlobalRank.Content = user?.Statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-";
var rankHighest = user?.RankHighest;
detailGlobalRank.ContentTooltipText = rankHighest != null
? UsersStrings.ShowRankHighest(rankHighest.Rank.ToLocalisableString("\\##,##0"), rankHighest.UpdatedAt.ToLocalisableString(@"d MMM yyyy"))
: string.Empty;
detailCountryRank.Content = user?.Statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-"; detailCountryRank.Content = user?.Statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-";
rankGraph.Statistics.Value = user?.Statistics; rankGraph.Statistics.Value = user?.Statistics;

View File

@ -4,6 +4,7 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -13,7 +14,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
public partial class ProfileValueDisplay : CompositeDrawable public partial class ProfileValueDisplay : CompositeDrawable
{ {
private readonly OsuSpriteText title; private readonly OsuSpriteText title;
private readonly OsuSpriteText content; private readonly ContentText content;
public LocalisableString Title public LocalisableString Title
{ {
@ -25,6 +26,11 @@ namespace osu.Game.Overlays.Profile.Header.Components
set => content.Text = value; set => content.Text = value;
} }
public LocalisableString ContentTooltipText
{
set => content.TooltipText = value;
}
public ProfileValueDisplay(bool big = false, int minimumWidth = 60) public ProfileValueDisplay(bool big = false, int minimumWidth = 60)
{ {
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
@ -38,9 +44,9 @@ namespace osu.Game.Overlays.Profile.Header.Components
{ {
Font = OsuFont.GetFont(size: 12) Font = OsuFont.GetFont(size: 12)
}, },
content = new OsuSpriteText content = new ContentText
{ {
Font = OsuFont.GetFont(size: big ? 30 : 20, weight: FontWeight.Light) Font = OsuFont.GetFont(size: big ? 30 : 20, weight: FontWeight.Light),
}, },
new Container // Add a minimum size to the FillFlowContainer new Container // Add a minimum size to the FillFlowContainer
{ {
@ -56,5 +62,10 @@ namespace osu.Game.Overlays.Profile.Header.Components
title.Colour = colourProvider.Content1; title.Colour = colourProvider.Content1;
content.Colour = colourProvider.Content2; content.Colour = colourProvider.Content2;
} }
private partial class ContentText : OsuSpriteText, IHasTooltip
{
public LocalisableString TooltipText { get; set; }
}
} }
} }

View File

@ -5,25 +5,19 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Profile.Header.Components namespace osu.Game.Overlays.Profile.Header.Components
{ {
public partial class TotalPlayTime : CompositeDrawable, IHasTooltip public partial class TotalPlayTime : CompositeDrawable
{ {
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>(); public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
public LocalisableString TooltipText { get; set; }
private ProfileValueDisplay info = null!; private ProfileValueDisplay info = null!;
public TotalPlayTime() public TotalPlayTime()
{ {
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
TooltipText = "0 hours";
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -32,6 +26,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
InternalChild = info = new ProfileValueDisplay(minimumWidth: 140) InternalChild = info = new ProfileValueDisplay(minimumWidth: 140)
{ {
Title = UsersStrings.ShowStatsPlayTime, Title = UsersStrings.ShowStatsPlayTime,
ContentTooltipText = "0 hours",
}; };
User.BindValueChanged(updateTime, true); User.BindValueChanged(updateTime, true);
@ -40,7 +35,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
private void updateTime(ValueChangedEvent<UserProfileData?> user) private void updateTime(ValueChangedEvent<UserProfileData?> user)
{ {
int? playTime = user.NewValue?.User.Statistics?.PlayTime; int? playTime = user.NewValue?.User.Statistics?.PlayTime;
TooltipText = (playTime ?? 0) / 3600 + " hours"; info.ContentTooltipText = (playTime ?? 0) / 3600 + " hours";
info.Content = formatTime(playTime); info.Content = formatTime(playTime);
} }

View File

@ -26,15 +26,11 @@ namespace osu.Game.Overlays.Profile.Header
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5, Colour = colourProvider.Background5,
}, },
new Container new GridContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = 10 }, Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = 10 },
Child = new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
RowDimensions = new[] RowDimensions = new[]
{ {
new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize),
@ -70,7 +66,6 @@ namespace osu.Game.Overlays.Profile.Header
} }
} }
} }
}
}; };
} }
} }

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq; using System.Linq;
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Configuration; using osu.Framework.Configuration;
using osu.Framework.Extensions; using osu.Framework.Extensions;
@ -28,15 +27,16 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
var renderer = config.GetBindable<RendererType>(FrameworkSetting.Renderer); var renderer = config.GetBindable<RendererType>(FrameworkSetting.Renderer);
automaticRendererInUse = renderer.Value == RendererType.Automatic; automaticRendererInUse = renderer.Value == RendererType.Automatic;
SettingsEnumDropdown<RendererType> rendererDropdown;
Children = new Drawable[] Children = new Drawable[]
{ {
rendererDropdown = new RendererSettingsDropdown new RendererSettingsDropdown
{ {
LabelText = GraphicsSettingsStrings.Renderer, LabelText = GraphicsSettingsStrings.Renderer,
Current = renderer, Current = renderer,
Items = host.GetPreferredRenderersForCurrentPlatform().Order().Where(t => t != RendererType.Vulkan), Items = host.GetPreferredRenderersForCurrentPlatform().Order()
#pragma warning disable CS0612 // Type or member is obsolete
.Where(t => t != RendererType.Vulkan && t != RendererType.OpenGLLegacy),
#pragma warning restore CS0612 // Type or member is obsolete
Keywords = new[] { @"compatibility", @"directx" }, Keywords = new[] { @"compatibility", @"directx" },
}, },
// TODO: this needs to be a custom dropdown at some point // TODO: this needs to be a custom dropdown at some point
@ -79,13 +79,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
})); }));
} }
}); });
// TODO: remove this once we support SDL+android.
if (RuntimeInfo.OS == RuntimeInfo.Platform.Android)
{
rendererDropdown.Items = new[] { RendererType.Automatic, RendererType.OpenGLLegacy };
rendererDropdown.SetNoticeText("New renderer support for android is coming soon!", true);
}
} }
private partial class RendererSettingsDropdown : SettingsEnumDropdown<RendererType> private partial class RendererSettingsDropdown : SettingsEnumDropdown<RendererType>

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Collections; using osu.Game.Collections;
@ -35,8 +36,20 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
private void deleteAllCollections() private void deleteAllCollections()
{ {
realm.Write(r => r.RemoveAll<BeatmapCollection>()); bool anyDeleted = realm.Write(r =>
notificationOverlay?.Post(new ProgressCompletionNotification { Text = MaintenanceSettingsStrings.DeletedAllCollections }); {
if (r.All<BeatmapCollection>().Any())
{
r.RemoveAll<BeatmapCollection>();
return true;
}
else
{
return false;
}
});
notificationOverlay?.Post(new ProgressCompletionNotification { Text = anyDeleted ? MaintenanceSettingsStrings.DeletedAllCollections : MaintenanceSettingsStrings.NoCollectionsFoundToDelete });
} }
} }
} }

View File

@ -4,6 +4,7 @@
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Logging; using osu.Framework.Logging;
@ -52,36 +53,50 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
}); });
} }
private void deleteAllModPresets() => private bool deleteAllModPresets() =>
realm.Write(r => realm.Write(r =>
{ {
bool anyDeleted = false;
foreach (var preset in r.All<ModPreset>()) foreach (var preset in r.All<ModPreset>())
{
anyDeleted |= !preset.DeletePending;
preset.DeletePending = true; preset.DeletePending = true;
}
return anyDeleted;
}); });
private void onAllModPresetsDeleted(Task deletionTask) private void onAllModPresetsDeleted(Task<bool> deletionTask)
{ {
deleteAllButton.Enabled.Value = true; deleteAllButton.Enabled.Value = true;
if (deletionTask.IsCompletedSuccessfully) if (deletionTask.IsCompletedSuccessfully)
notificationOverlay?.Post(new ProgressCompletionNotification { Text = MaintenanceSettingsStrings.DeletedAllModPresets }); notificationOverlay?.Post(new ProgressCompletionNotification { Text = deletionTask.GetResultSafely() ? MaintenanceSettingsStrings.DeletedAllModPresets : MaintenanceSettingsStrings.NoModPresetsFoundToDelete });
else if (deletionTask.IsFaulted) else if (deletionTask.IsFaulted)
Logger.Error(deletionTask.Exception, "Failed to delete all mod presets"); Logger.Error(deletionTask.Exception, "Failed to delete all mod presets");
} }
private void undeleteModPresets() => private bool undeleteModPresets() =>
realm.Write(r => realm.Write(r =>
{ {
bool anyRestored = false;
foreach (var preset in r.All<ModPreset>().Where(preset => preset.DeletePending)) foreach (var preset in r.All<ModPreset>().Where(preset => preset.DeletePending))
{
anyRestored |= preset.DeletePending;
preset.DeletePending = false; preset.DeletePending = false;
}
return anyRestored;
}); });
private void onModPresetsUndeleted(Task undeletionTask) private void onModPresetsUndeleted(Task<bool> undeletionTask)
{ {
undeleteButton.Enabled.Value = true; undeleteButton.Enabled.Value = true;
if (undeletionTask.IsCompletedSuccessfully) if (undeletionTask.IsCompletedSuccessfully)
notificationOverlay?.Post(new ProgressCompletionNotification { Text = MaintenanceSettingsStrings.RestoredAllDeletedModPresets }); notificationOverlay?.Post(new ProgressCompletionNotification { Text = undeletionTask.GetResultSafely() ? MaintenanceSettingsStrings.RestoredAllDeletedModPresets : MaintenanceSettingsStrings.NoModPresetsFoundToRestore });
else if (undeletionTask.IsFaulted) else if (undeletionTask.IsFaulted)
Logger.Error(undeletionTask.Exception, "Failed to restore mod presets"); Logger.Error(undeletionTask.Exception, "Failed to restore mod presets");
} }

View File

@ -103,13 +103,14 @@ namespace osu.Game.Overlays.SkinEditor
{ {
globallyDisableBeatmapSkinSetting(); globallyDisableBeatmapSkinSetting();
if (lastTargetScreen is MainMenu)
PresentGameplay();
if (skinEditor != null) if (skinEditor != null)
{ {
disableNestedInputManagers(); disableNestedInputManagers();
skinEditor.Show(); skinEditor.Show();
if (lastTargetScreen is MainMenu)
PresentGameplay();
return; return;
} }
@ -126,6 +127,9 @@ namespace osu.Game.Overlays.SkinEditor
AddInternal(editor); AddInternal(editor);
if (lastTargetScreen is MainMenu)
PresentGameplay();
Debug.Assert(lastTargetScreen != null); Debug.Assert(lastTargetScreen != null);
SetTarget(lastTargetScreen); SetTarget(lastTargetScreen);
@ -270,7 +274,7 @@ namespace osu.Game.Overlays.SkinEditor
Debug.Assert(skinEditor != null); Debug.Assert(skinEditor != null);
if (!target.IsLoaded) if (!target.IsLoaded || !skinEditor.IsLoaded)
{ {
Scheduler.AddOnce(setTarget, target); Scheduler.AddOnce(setTarget, target);
return; return;
@ -350,7 +354,7 @@ namespace osu.Game.Overlays.SkinEditor
base.LoadComplete(); base.LoadComplete();
if (!LoadedBeatmapSuccessfully) if (!LoadedBeatmapSuccessfully)
Scheduler.AddDelayed(this.Exit, 3000); Scheduler.AddDelayed(this.Exit, 1000);
} }
protected override void Update() protected override void Update()

View File

@ -1,42 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Screens.Play;
namespace osu.Game.Performance
{
public partial class HighPerformanceSession : Component
{
private readonly IBindable<bool> localUserPlaying = new Bindable<bool>();
[BackgroundDependencyLoader]
private void load(ILocalUserPlayInfo localUserInfo)
{
localUserPlaying.BindTo(localUserInfo.IsPlaying);
}
protected override void LoadComplete()
{
base.LoadComplete();
localUserPlaying.BindValueChanged(playing =>
{
if (playing.NewValue)
EnableHighPerformanceSession();
else
DisableHighPerformanceSession();
}, true);
}
protected virtual void EnableHighPerformanceSession()
{
}
protected virtual void DisableHighPerformanceSession()
{
}
}
}

View File

@ -0,0 +1,23 @@
// 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;
namespace osu.Game.Performance
{
/// <summary>
/// Allows creating a temporary "high performance" session, with the goal of optimising runtime
/// performance for gameplay purposes.
///
/// On desktop platforms, this will set a low latency GC mode which collects more frequently to avoid
/// GC spikes.
/// </summary>
public interface IHighPerformanceSessionManager
{
/// <summary>
/// Start a new high performance session.
/// </summary>
/// <returns>An <see cref="IDisposable"/> which will end the session when disposed.</returns>
IDisposable BeginSession();
}
}

View File

@ -4,7 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
@ -124,12 +123,34 @@ namespace osu.Game.Rulesets.Edit
private (HitObject before, HitObject after)? getObjectsOnEitherSideOfCurrentTime() private (HitObject before, HitObject after)? getObjectsOnEitherSideOfCurrentTime()
{ {
HitObject? lastBefore = playfield.HitObjectContainer.AliveObjects.LastOrDefault(h => h.HitObject.StartTime < editorClock.CurrentTime)?.HitObject; HitObject? lastBefore = null;
foreach (var entry in playfield.HitObjectContainer.AliveEntries)
{
double objTime = entry.Value.HitObject.StartTime;
if (objTime >= editorClock.CurrentTime)
continue;
if (lastBefore == null || objTime > lastBefore.StartTime)
lastBefore = entry.Value.HitObject;
}
if (lastBefore == null) if (lastBefore == null)
return null; return null;
HitObject? firstAfter = playfield.HitObjectContainer.AliveObjects.FirstOrDefault(h => h.HitObject.StartTime >= editorClock.CurrentTime)?.HitObject; HitObject? firstAfter = null;
foreach (var entry in playfield.HitObjectContainer.AliveEntries)
{
double objTime = entry.Value.HitObject.StartTime;
if (objTime < editorClock.CurrentTime)
continue;
if (firstAfter == null || objTime < firstAfter.StartTime)
firstAfter = entry.Value.HitObject;
}
if (firstAfter == null) if (firstAfter == null)
return null; return null;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -13,6 +14,7 @@ using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Shaders.Types; using osu.Framework.Graphics.Shaders.Types;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.OpenGL.Vertices; using osu.Game.Graphics.OpenGL.Vertices;
@ -84,6 +86,7 @@ namespace osu.Game.Rulesets.Mods
flashlight.Depth = float.MinValue; flashlight.Depth = float.MinValue;
flashlight.Combo.BindTo(Combo); flashlight.Combo.BindTo(Combo);
flashlight.GetPlayfieldScale = () => drawableRuleset.Playfield.Scale;
drawableRuleset.Overlays.Add(flashlight); drawableRuleset.Overlays.Add(flashlight);
} }
@ -100,6 +103,8 @@ namespace osu.Game.Rulesets.Mods
public override bool RemoveCompletedTransforms => false; public override bool RemoveCompletedTransforms => false;
internal Func<Vector2>? GetPlayfieldScale;
private readonly float defaultFlashlightSize; private readonly float defaultFlashlightSize;
private readonly float sizeMultiplier; private readonly float sizeMultiplier;
private readonly bool comboBasedSize; private readonly bool comboBasedSize;
@ -139,10 +144,19 @@ namespace osu.Game.Rulesets.Mods
protected abstract string FragmentShader { get; } protected abstract string FragmentShader { get; }
protected float GetSize() public float GetSize()
{ {
float size = defaultFlashlightSize * sizeMultiplier; float size = defaultFlashlightSize * sizeMultiplier;
if (GetPlayfieldScale != null)
{
Vector2 playfieldScale = GetPlayfieldScale();
Debug.Assert(Precision.AlmostEquals(Math.Abs(playfieldScale.X), Math.Abs(playfieldScale.Y)),
@"Playfield has non-proportional scaling. Flashlight implementations should be revisited with regard to balance.");
size *= Math.Abs(playfieldScale.X);
}
if (isBreakTime.Value) if (isBreakTime.Value)
size *= 2.5f; size *= 2.5f;
else if (comboBasedSize) else if (comboBasedSize)

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Mods
public override LocalisableString Description => "Can you still feel the rhythm without music?"; public override LocalisableString Description => "Can you still feel the rhythm without music?";
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override bool Ranked => UsesDefaultConfiguration; public override bool Ranked => true;
} }
public abstract class ModMuted<TObject> : ModMuted, IApplicableToDrawableRuleset<TObject>, IApplicableToTrack, IApplicableToScoreProcessor public abstract class ModMuted<TObject> : ModMuted, IApplicableToDrawableRuleset<TObject>, IApplicableToTrack, IApplicableToScoreProcessor

View File

@ -11,10 +11,12 @@ using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ListExtensions;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Lists;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Audio; using osu.Game.Audio;
@ -65,7 +67,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
public virtual IEnumerable<HitSampleInfo> GetSamples() => HitObject.Samples; public virtual IEnumerable<HitSampleInfo> GetSamples() => HitObject.Samples;
private readonly List<DrawableHitObject> nestedHitObjects = new List<DrawableHitObject>(); private readonly List<DrawableHitObject> nestedHitObjects = new List<DrawableHitObject>();
public IReadOnlyList<DrawableHitObject> NestedHitObjects => nestedHitObjects; public SlimReadOnlyListWrapper<DrawableHitObject> NestedHitObjects => nestedHitObjects.AsSlimReadOnly();
/// <summary> /// <summary>
/// Whether this object should handle any user input events. /// Whether this object should handle any user input events.

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Performance;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
@ -41,7 +40,22 @@ namespace osu.Game.Rulesets.Objects
/// <summary> /// <summary>
/// Whether <see cref="HitObject"/> and all of its nested objects have been judged. /// Whether <see cref="HitObject"/> and all of its nested objects have been judged.
/// </summary> /// </summary>
public bool AllJudged => Judged && NestedEntries.All(h => h.AllJudged); public bool AllJudged
{
get
{
if (!Judged)
return false;
foreach (var entry in NestedEntries)
{
if (!entry.AllJudged)
return false;
}
return true;
}
}
private readonly IBindable<double> startTimeBindable = new BindableDouble(); private readonly IBindable<double> startTimeBindable = new BindableDouble();

View File

@ -47,10 +47,7 @@ namespace osu.Game.Screens.Edit
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4, Colour = colourProvider.Background4,
}, },
new Container new GridContainer
{
RelativeSizeAxes = Axes.Both,
Child = new GridContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[] ColumnDimensions = new[]
@ -75,7 +72,6 @@ namespace osu.Game.Screens.Edit
} }
}, },
} }
},
} }
}; };
} }

View File

@ -97,11 +97,14 @@ namespace osu.Game.Screens.Edit.Components
editorClock.Start(); editorClock.Start();
} }
private static readonly IconUsage play_icon = FontAwesome.Regular.PlayCircle;
private static readonly IconUsage pause_icon = FontAwesome.Regular.PauseCircle;
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
playButton.Icon = editorClock.IsRunning ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle; playButton.Icon = editorClock.IsRunning ? pause_icon : play_icon;
} }
private partial class PlaybackTabControl : OsuTabControl<double> private partial class PlaybackTabControl : OsuTabControl<double>

View File

@ -47,11 +47,26 @@ namespace osu.Game.Screens.Edit.Components
}; };
} }
private double? lastTime;
private double? lastBPM;
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
if (lastTime != editorClock.CurrentTime)
{
lastTime = editorClock.CurrentTime;
trackTimer.Text = editorClock.CurrentTime.ToEditorFormattedString(); trackTimer.Text = editorClock.CurrentTime.ToEditorFormattedString();
bpm.Text = @$"{editorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime).BPM:0} BPM"; }
double newBPM = editorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime).BPM;
if (lastBPM != newBPM)
{
lastBPM = newBPM;
bpm.Text = @$"{newBPM:0} BPM";
}
} }
} }
} }

View File

@ -86,10 +86,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background3 Colour = colourProvider.Background3
}, },
new Container new GridContainer
{
RelativeSizeAxes = Axes.Both,
Child = new GridContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Content = new[] Content = new[]
@ -118,19 +115,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
} }
} }
}
}, },
new Drawable[] new Drawable[]
{ {
new Container new GridContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Child = new GridContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Content = new[] Content = new[]
@ -157,9 +145,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
new Dimension(GridSizeMode.Absolute, 20) new Dimension(GridSizeMode.Absolute, 20)
} }
} }
}
}
}
}, },
new Drawable[] new Drawable[]
{ {

View File

@ -116,6 +116,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
updateStacking(); updateStacking();
} }
private readonly Stack<HitObject> currentConcurrentObjects = new Stack<HitObject>();
private void updateStacking() private void updateStacking()
{ {
// because only blueprints of objects which are alive (via pooling) are displayed in the timeline, it's feasible to do this every-update. // because only blueprints of objects which are alive (via pooling) are displayed in the timeline, it's feasible to do this every-update.
@ -125,10 +127,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
// after the stack gets this tall, we can presume there is space underneath to draw subsequent blueprints. // after the stack gets this tall, we can presume there is space underneath to draw subsequent blueprints.
const int stack_reset_count = 3; const int stack_reset_count = 3;
Stack<HitObject> currentConcurrentObjects = new Stack<HitObject>(); currentConcurrentObjects.Clear();
foreach (var b in SelectionBlueprints.Reverse()) for (int i = SelectionBlueprints.Count - 1; i >= 0; i--)
{ {
var b = SelectionBlueprints[i];
// remove objects from the stack as long as their end time is in the past. // remove objects from the stack as long as their end time is in the past.
while (currentConcurrentObjects.TryPeek(out HitObject hitObject)) while (currentConcurrentObjects.TryPeek(out HitObject hitObject))
{ {

View File

@ -1,10 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Linq; using System.Collections.Generic;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -20,7 +18,7 @@ namespace osu.Game.Screens.Edit
/// </summary> /// </summary>
public class EditorBeatmapSkin : ISkin public class EditorBeatmapSkin : ISkin
{ {
public event Action BeatmapSkinChanged; public event Action? BeatmapSkinChanged;
/// <summary> /// <summary>
/// The underlying beatmap skin. /// The underlying beatmap skin.
@ -38,8 +36,17 @@ namespace osu.Game.Screens.Edit
Skin = skin; Skin = skin;
ComboColours = new BindableList<Colour4>(); ComboColours = new BindableList<Colour4>();
if (Skin.Configuration.ComboColours != null)
ComboColours.AddRange(Skin.Configuration.ComboColours.Select(c => (Colour4)c)); if (Skin.Configuration.ComboColours is IReadOnlyList<Color4> comboColours)
{
// due to the foibles of how `IHasComboInformation` / `ComboIndexWithOffsets` work,
// the actual effective first combo colour that will be used on the beatmap is the one with index 1, not 0.
// see also: `IHasComboInformation.UpdateComboInformation`,
// https://github.com/peppy/osu-stable-reference/blob/46cd3a10af7cc6cc96f4eba92ef1812dc8c3a27e/osu!/GameModes/Edit/Forms/SongSetup.cs#L233-L234.
for (int i = 0; i < comboColours.Count; ++i)
ComboColours.Add(comboColours[(i + 1) % comboColours.Count]);
}
ComboColours.BindCollectionChanged((_, _) => updateColours()); ComboColours.BindCollectionChanged((_, _) => updateColours());
} }
@ -47,16 +54,23 @@ namespace osu.Game.Screens.Edit
private void updateColours() private void updateColours()
{ {
Skin.Configuration.CustomComboColours = ComboColours.Select(c => (Color4)c).ToList(); // performs the inverse of the index rotation operation described in the ctor.
Skin.Configuration.CustomComboColours.Clear();
for (int i = 0; i < ComboColours.Count; ++i)
Skin.Configuration.CustomComboColours.Add(ComboColours[(ComboColours.Count + i - 1) % ComboColours.Count]);
invokeSkinChanged(); invokeSkinChanged();
} }
#region Delegated ISkin implementation #region Delegated ISkin implementation
public Drawable GetDrawableComponent(ISkinComponentLookup lookup) => Skin.GetDrawableComponent(lookup); public Drawable? GetDrawableComponent(ISkinComponentLookup lookup) => Skin.GetDrawableComponent(lookup);
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => Skin.GetTexture(componentName, wrapModeS, wrapModeT); public Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => Skin.GetTexture(componentName, wrapModeS, wrapModeT);
public ISample GetSample(ISampleInfo sampleInfo) => Skin.GetSample(sampleInfo); public ISample? GetSample(ISampleInfo sampleInfo) => Skin.GetSample(sampleInfo);
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => Skin.GetConfig<TLookup, TValue>(lookup);
public IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
where TLookup : notnull
where TValue : notnull
=> Skin.GetConfig<TLookup, TValue>(lookup);
#endregion #endregion
} }

View File

@ -57,16 +57,12 @@ namespace osu.Game.Screens.Edit
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4 Colour = colourProvider.Background4
}, },
new Container new GridContainer
{ {
Name = "Timeline content", Name = "Timeline content",
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = PADDING, Top = PADDING }, Padding = new MarginPadding { Horizontal = PADDING, Top = PADDING },
Child = new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Content = new[] Content = new[]
{ {
new Drawable[] new Drawable[]
@ -87,7 +83,6 @@ namespace osu.Game.Screens.Edit
new Dimension(), new Dimension(),
new Dimension(GridSizeMode.Absolute, 90), new Dimension(GridSizeMode.Absolute, 90),
} }
},
} }
} }
}, },

View File

@ -64,16 +64,11 @@ namespace osu.Game.Screens.Edit.Timing
Content = new[] Content = new[]
{ {
new Drawable[] new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(padding),
Children = new Drawable[]
{ {
new GridContainer new GridContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(padding),
ColumnDimensions = new[] ColumnDimensions = new[]
{ {
new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize),
@ -92,8 +87,6 @@ namespace osu.Game.Screens.Edit.Timing
} }
}, },
} }
}
},
}, },
new Drawable[] new Drawable[]
{ {

View File

@ -34,9 +34,6 @@ namespace osu.Game.Screens.Edit.Verify
InterpretedDifficulty.Default = StarDifficulty.GetDifficultyRating(EditorBeatmap.BeatmapInfo.StarRating); InterpretedDifficulty.Default = StarDifficulty.GetDifficultyRating(EditorBeatmap.BeatmapInfo.StarRating);
InterpretedDifficulty.SetDefault(); InterpretedDifficulty.SetDefault();
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Child = new GridContainer Child = new GridContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -53,7 +50,6 @@ namespace osu.Game.Screens.Edit.Verify
new IssueSettings(), new IssueSettings(),
}, },
} }
}
}; };
} }

View File

@ -102,7 +102,7 @@ namespace osu.Game.Screens.Menu
buttonArea.AddRange(new Drawable[] buttonArea.AddRange(new Drawable[]
{ {
new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, OsuIcon.Settings, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O), new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, OsuIcon.Settings, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O, Key.S),
backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel,
-WEDGE_WIDTH) -WEDGE_WIDTH)
{ {
@ -132,11 +132,11 @@ namespace osu.Game.Screens.Menu
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Tournament, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L)); buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Tournament, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L));
buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play);
buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), () => OnEditBeatmap?.Invoke(), WEDGE_WIDTH, Key.B)); buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), () => OnEditBeatmap?.Invoke(), WEDGE_WIDTH, Key.B, Key.E));
buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), () => OnEditSkin?.Invoke(), 0, Key.S)); buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), () => OnEditSkin?.Invoke(), 0, Key.S));
buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit); buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit);
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P)); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P, Key.M, Key.L));
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => State = ButtonSystemState.Edit, 0, Key.E)); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => State = ButtonSystemState.Edit, 0, Key.E));
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.B, Key.D)); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.B, Key.D));

View File

@ -64,6 +64,10 @@ namespace osu.Game.Screens.Menu
private Sample? sampleHover; private Sample? sampleHover;
private SampleChannel? sampleChannel; private SampleChannel? sampleChannel;
public override bool IsPresent => base.IsPresent
// Allow keyboard interaction based on state rather than waiting for delayed animations.
|| state == ButtonState.Expanded;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos);
public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action? clickAction = null, float extraWidth = 0, params Key[] triggerKeys) public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action? clickAction = null, float extraWidth = 0, params Key[] triggerKeys)

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Localisation;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Dialog;
@ -17,7 +18,7 @@ namespace osu.Game.Screens.Menu
public StorageErrorDialog(OsuStorage storage, OsuStorageError error) public StorageErrorDialog(OsuStorage storage, OsuStorageError error)
{ {
HeaderText = "osu! storage error"; HeaderText = StorageErrorDialogStrings.StorageError;
Icon = FontAwesome.Solid.ExclamationTriangle; Icon = FontAwesome.Solid.ExclamationTriangle;
var buttons = new List<PopupDialogButton>(); var buttons = new List<PopupDialogButton>();
@ -25,13 +26,13 @@ namespace osu.Game.Screens.Menu
switch (error) switch (error)
{ {
case OsuStorageError.NotAccessible: case OsuStorageError.NotAccessible:
BodyText = $"The specified osu! data location (\"{storage.CustomStoragePath}\") is not accessible. If it is on external storage, please reconnect the device and try again."; BodyText = StorageErrorDialogStrings.LocationIsNotAccessible(storage.CustomStoragePath);
buttons.AddRange(new PopupDialogButton[] buttons.AddRange(new PopupDialogButton[]
{ {
new PopupDialogCancelButton new PopupDialogCancelButton
{ {
Text = "Try again", Text = StorageErrorDialogStrings.TryAgain,
Action = () => Action = () =>
{ {
if (!storage.TryChangeToCustomStorage(out var nextError)) if (!storage.TryChangeToCustomStorage(out var nextError))
@ -40,29 +41,29 @@ namespace osu.Game.Screens.Menu
}, },
new PopupDialogCancelButton new PopupDialogCancelButton
{ {
Text = "Use default location until restart", Text = StorageErrorDialogStrings.UseDefaultLocation,
}, },
new PopupDialogOkButton new PopupDialogOkButton
{ {
Text = "Reset to default location", Text = StorageErrorDialogStrings.ResetToDefaultLocation,
Action = storage.ResetCustomStoragePath Action = storage.ResetCustomStoragePath
}, },
}); });
break; break;
case OsuStorageError.AccessibleButEmpty: case OsuStorageError.AccessibleButEmpty:
BodyText = $"The specified osu! data location (\"{storage.CustomStoragePath}\") is empty. If you have moved the files, please close osu! and move them back."; BodyText = StorageErrorDialogStrings.LocationIsEmpty(storage.CustomStoragePath);
// Todo: Provide the option to search for the files similar to migration. // Todo: Provide the option to search for the files similar to migration.
buttons.AddRange(new PopupDialogButton[] buttons.AddRange(new PopupDialogButton[]
{ {
new PopupDialogCancelButton new PopupDialogCancelButton
{ {
Text = "Start fresh at specified location" Text = StorageErrorDialogStrings.StartFresh
}, },
new PopupDialogOkButton new PopupDialogOkButton
{ {
Text = "Reset to default location", Text = StorageErrorDialogStrings.ResetToDefaultLocation,
Action = storage.ResetCustomStoragePath Action = storage.ResetCustomStoragePath
}, },
}); });

View File

@ -26,18 +26,15 @@ namespace osu.Game.Screens.OnlinePlay.Components
[Resolved(typeof(Room))] [Resolved(typeof(Room))]
protected BindableList<PlaylistItem> Playlist { get; private set; } protected BindableList<PlaylistItem> Playlist { get; private set; }
private readonly Drawable playlistArea; private readonly GridContainer playlistArea;
private readonly DrawableRoomPlaylist playlist; private readonly DrawableRoomPlaylist playlist;
public MatchBeatmapDetailArea() public MatchBeatmapDetailArea()
{ {
Add(playlistArea = new Container Add(playlistArea = new GridContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Vertical = 10 }, Padding = new MarginPadding { Vertical = 10 },
Child = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[] Content = new[]
{ {
new Drawable[] new Drawable[]
@ -68,7 +65,6 @@ namespace osu.Game.Screens.OnlinePlay.Components
new Dimension(), new Dimension(),
new Dimension(GridSizeMode.Absolute, 50), new Dimension(GridSizeMode.Absolute, 50),
} }
}
}); });
} }

View File

@ -281,7 +281,13 @@ namespace osu.Game.Screens.OnlinePlay
} }
if (beatmap != null) if (beatmap != null)
difficultyIconContainer.Child = new DifficultyIcon(beatmap, ruleset) { Size = new Vector2(icon_height) }; {
difficultyIconContainer.Child = new DifficultyIcon(beatmap, ruleset, requiredMods)
{
Size = new Vector2(icon_height),
TooltipType = DifficultyIconTooltipType.Extended,
};
}
else else
difficultyIconContainer.Clear(); difficultyIconContainer.Clear();

View File

@ -40,15 +40,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
Colour = Color4.Black, Colour = Color4.Black,
Alpha = 0.5f Alpha = 0.5f
}, },
new Container new GridContainer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = padding }, Padding = new MarginPadding { Horizontal = padding },
Child = new GridContainer
{
AutoSizeAxes = Axes.Both,
ColumnDimensions = new[] ColumnDimensions = new[]
{ {
new Dimension(GridSizeMode.AutoSize, minSize: 80 - 2 * padding) new Dimension(GridSizeMode.AutoSize, minSize: 80 - 2 * padding)
@ -74,7 +71,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
} }
} }
} }
}
}; };
} }
} }

View File

@ -95,13 +95,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
new Drawable[] new Drawable[]
{ {
// Playlist items column // Playlist items column
new Container new GridContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = 5 }, Padding = new MarginPadding { Right = 5 },
Child = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[] Content = new[]
{ {
new Drawable[] { new OverlinedPlaylistHeader(), }, new Drawable[] { new OverlinedPlaylistHeader(), },
@ -127,7 +124,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize),
new Dimension(), new Dimension(),
} }
}
}, },
// Spacer // Spacer
null, null,

View File

@ -258,14 +258,10 @@ namespace osu.Game.Screens.Play
Vector2? highestBottomScreenSpace = null; Vector2? highestBottomScreenSpace = null;
foreach (var element in mainComponents.Components) processDrawables(mainComponents);
processDrawable(element);
if (rulesetComponents != null) if (rulesetComponents != null)
{ processDrawables(rulesetComponents);
foreach (var element in rulesetComponents.Components)
processDrawable(element);
}
if (lowestTopScreenSpaceRight.HasValue) if (lowestTopScreenSpaceRight.HasValue)
topRightElements.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceRight.Value)).Y, 0, DrawHeight - topRightElements.DrawHeight); topRightElements.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceRight.Value)).Y, 0, DrawHeight - topRightElements.DrawHeight);
@ -282,6 +278,14 @@ namespace osu.Game.Screens.Play
else else
bottomRightElements.Y = 0; bottomRightElements.Y = 0;
void processDrawables(SkinComponentsContainer components)
{
// Avoid using foreach due to missing GetEnumerator implementation.
// See https://github.com/ppy/osu-framework/blob/e10051e6643731e393b09de40a3a3d209a545031/osu.Framework/Bindables/IBindableList.cs#L41-L44.
for (int i = 0; i < components.Components.Count; i++)
processDrawable(components.Components[i]);
}
void processDrawable(ISerialisableDrawable element) void processDrawable(ISerialisableDrawable element)
{ {
// Cast can be removed when IDrawable interface includes Anchor / RelativeSizeAxes. // Cast can be removed when IDrawable interface includes Anchor / RelativeSizeAxes.

View File

@ -545,7 +545,7 @@ namespace osu.Game.Screens.Play
if (playable.HitObjects.Count == 0) if (playable.HitObjects.Count == 0)
{ {
Logger.Log("Beatmap contains no hit objects!", level: LogLevel.Error); Logger.Log("Beatmap contains no hit objects!", level: LogLevel.Important);
return null; return null;
} }
} }

View File

@ -25,6 +25,7 @@ using osu.Game.Input;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using osu.Game.Performance;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -78,8 +79,8 @@ namespace osu.Game.Screens.Play
private readonly BindableDouble volumeAdjustment = new BindableDouble(1); private readonly BindableDouble volumeAdjustment = new BindableDouble(1);
private AudioFilter lowPassFilter = null!; private AudioFilter? lowPassFilter;
private AudioFilter highPassFilter = null!; private AudioFilter? highPassFilter;
private SkinnableSound sampleRestart = null!; private SkinnableSound sampleRestart = null!;
@ -140,6 +141,8 @@ namespace osu.Game.Screens.Play
private bool quickRestart; private bool quickRestart;
private IDisposable? highPerformanceSession;
[Resolved] [Resolved]
private INotificationOverlay? notificationOverlay { get; set; } private INotificationOverlay? notificationOverlay { get; set; }
@ -152,13 +155,16 @@ namespace osu.Game.Screens.Play
[Resolved] [Resolved]
private BatteryInfo? batteryInfo { get; set; } private BatteryInfo? batteryInfo { get; set; }
[Resolved]
private IHighPerformanceSessionManager? highPerformanceSessionManager { get; set; }
public PlayerLoader(Func<Player> createPlayer) public PlayerLoader(Func<Player> createPlayer)
{ {
this.createPlayer = createPlayer; this.createPlayer = createPlayer;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(SessionStatics sessionStatics, AudioManager audio, OsuConfigManager config) private void load(SessionStatics sessionStatics, OsuConfigManager config)
{ {
muteWarningShownOnce = sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce); muteWarningShownOnce = sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce);
batteryWarningShownOnce = sessionStatics.GetBindable<bool>(Static.LowBatteryNotificationShownOnce); batteryWarningShownOnce = sessionStatics.GetBindable<bool>(Static.LowBatteryNotificationShownOnce);
@ -205,8 +211,6 @@ namespace osu.Game.Screens.Play
}, },
}, },
idleTracker = new IdleTracker(750), idleTracker = new IdleTracker(750),
lowPassFilter = new AudioFilter(audio.TrackMixer),
highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
sampleRestart = new SkinnableSound(new SampleInfo(@"Gameplay/restart", @"pause-retry-click")) sampleRestart = new SkinnableSound(new SampleInfo(@"Gameplay/restart", @"pause-retry-click"))
}; };
@ -264,6 +268,9 @@ namespace osu.Game.Screens.Play
Debug.Assert(CurrentPlayer != null); Debug.Assert(CurrentPlayer != null);
highPerformanceSession?.Dispose();
highPerformanceSession = null;
// prepare for a retry. // prepare for a retry.
CurrentPlayer = null; CurrentPlayer = null;
playerConsumed = false; playerConsumed = false;
@ -284,8 +291,9 @@ namespace osu.Game.Screens.Play
// stop the track before removing adjustment to avoid a volume spike. // stop the track before removing adjustment to avoid a volume spike.
Beatmap.Value.Track.Stop(); Beatmap.Value.Track.Stop();
Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment); Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment);
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
highPassFilter.CutoffTo(0); lowPassFilter?.RemoveAndDisposeImmediately();
highPassFilter?.RemoveAndDisposeImmediately();
} }
public override bool OnExiting(ScreenExitEvent e) public override bool OnExiting(ScreenExitEvent e)
@ -304,6 +312,9 @@ namespace osu.Game.Screens.Play
BackgroundBrightnessReduction = false; BackgroundBrightnessReduction = false;
Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment); Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment);
highPerformanceSession?.Dispose();
highPerformanceSession = null;
return base.OnExiting(e); return base.OnExiting(e);
} }
@ -425,6 +436,12 @@ namespace osu.Game.Screens.Play
settingsScroll.FadeInFromZero(500, Easing.Out) settingsScroll.FadeInFromZero(500, Easing.Out)
.MoveToX(0, 500, Easing.OutQuint); .MoveToX(0, 500, Easing.OutQuint);
AddRangeInternal(new[]
{
lowPassFilter = new AudioFilter(audioManager.TrackMixer),
highPassFilter = new AudioFilter(audioManager.TrackMixer, BQFType.HighPass),
});
lowPassFilter.CutoffTo(1000, 650, Easing.OutQuint); lowPassFilter.CutoffTo(1000, 650, Easing.OutQuint);
highPassFilter.CutoffTo(300).Then().CutoffTo(0, 1250); // 1250 is to line up with the appearance of MetadataInfo (750 delay + 500 fade-in) highPassFilter.CutoffTo(300).Then().CutoffTo(0, 1250); // 1250 is to line up with the appearance of MetadataInfo (750 delay + 500 fade-in)
@ -437,13 +454,23 @@ namespace osu.Game.Screens.Play
content.StopTracking(); content.StopTracking();
content.ScaleTo(0.7f, CONTENT_OUT_DURATION * 2, Easing.OutQuint); content.ScaleTo(0.7f, CONTENT_OUT_DURATION * 2, Easing.OutQuint);
content.FadeOut(CONTENT_OUT_DURATION, Easing.OutQuint); content.FadeOut(CONTENT_OUT_DURATION, Easing.OutQuint)
// Safety for filter potentially getting stuck in applied state due to
// transforms on `this` causing children to no longer be updated.
.OnComplete(_ =>
{
highPassFilter?.RemoveAndDisposeImmediately();
highPassFilter = null;
lowPassFilter?.RemoveAndDisposeImmediately();
lowPassFilter = null;
});
settingsScroll.FadeOut(CONTENT_OUT_DURATION, Easing.OutQuint) settingsScroll.FadeOut(CONTENT_OUT_DURATION, Easing.OutQuint)
.MoveToX(settingsScroll.DrawWidth, CONTENT_OUT_DURATION * 2, Easing.OutQuint); .MoveToX(settingsScroll.DrawWidth, CONTENT_OUT_DURATION * 2, Easing.OutQuint);
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, CONTENT_OUT_DURATION); lowPassFilter?.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, CONTENT_OUT_DURATION);
highPassFilter.CutoffTo(0, CONTENT_OUT_DURATION); highPassFilter?.CutoffTo(0, CONTENT_OUT_DURATION);
} }
private void pushWhenLoaded() private void pushWhenLoaded()
@ -463,6 +490,10 @@ namespace osu.Game.Screens.Play
if (scheduledPushPlayer != null) if (scheduledPushPlayer != null)
return; return;
// Now that everything's been loaded, we can safely switch to a higher performance session without incurring too much overhead.
// Doing this prior to the game being pushed gives us a bit of time to stabilise into the high performance mode before gameplay starts.
highPerformanceSession ??= highPerformanceSessionManager?.BeginSession();
scheduledPushPlayer = Scheduler.AddDelayed(() => scheduledPushPlayer = Scheduler.AddDelayed(() =>
{ {
// ensure that once we have reached this "point of no return", readyForPush will be false for all future checks (until a new player instance is prepared). // ensure that once we have reached this "point of no return", readyForPush will be false for all future checks (until a new player instance is prepared).

View File

@ -150,13 +150,10 @@ namespace osu.Game.Screens.Ranking.Contracted
}, },
new Drawable[] new Drawable[]
{ {
new Container new GridContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Vertical = 5 }, Padding = new MarginPadding { Vertical = 5 },
Child = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[] Content = new[]
{ {
new Drawable[] new Drawable[]
@ -189,7 +186,6 @@ namespace osu.Game.Screens.Ranking.Contracted
new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize),
} }
} }
}
}, },
}, },
RowDimensions = new[] RowDimensions = new[]

View File

@ -64,7 +64,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
/// <summary> /// <summary>
/// Relative width of the rank circles. /// Relative width of the rank circles.
/// </summary> /// </summary>
public const float RANK_CIRCLE_RADIUS = 0.06f; public const float RANK_CIRCLE_RADIUS = 0.05f;
/// <summary> /// <summary>
/// Relative width of the circle showing the accuracy. /// Relative width of the circle showing the accuracy.
@ -74,12 +74,12 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
/// <summary> /// <summary>
/// SS is displayed as a 1% region, otherwise it would be invisible. /// SS is displayed as a 1% region, otherwise it would be invisible.
/// </summary> /// </summary>
private const double virtual_ss_percentage = 0.01; public const double VIRTUAL_SS_PERCENTAGE = 0.01;
/// <summary> /// <summary>
/// The width of a <see cref="RankNotch"/> in terms of accuracy. /// The width of spacing in terms of accuracy between the grade circles.
/// </summary> /// </summary>
public const double NOTCH_WIDTH_PERCENTAGE = 1.0 / 360; public const double GRADE_SPACING_PERCENTAGE = 2.0 / 360;
/// <summary> /// <summary>
/// The easing for the circle filling transforms. /// The easing for the circle filling transforms.
@ -89,7 +89,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
private readonly ScoreInfo score; private readonly ScoreInfo score;
private CircularProgress accuracyCircle; private CircularProgress accuracyCircle;
private CircularProgress innerMask; private GradedCircles gradedCircles;
private Container<RankBadge> badges; private Container<RankBadge> badges;
private RankText rankText; private RankText rankText;
@ -158,82 +158,16 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#7CF6FF"), Color4Extensions.FromHex("#BAFFA9")), Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("#7CF6FF"), Color4Extensions.FromHex("#BAFFA9")),
InnerRadius = accuracy_circle_radius, InnerRadius = accuracy_circle_radius,
}, },
new BufferedContainer new Container
{ {
Name = "Graded circles",
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.8f), Size = new Vector2(0.8f),
Padding = new MarginPadding(2), Padding = new MarginPadding(2.5f),
Children = new Drawable[] Child = gradedCircles = new GradedCircles(accuracyC, accuracyB, accuracyA, accuracyS, accuracyX)
{ {
new CircularProgress RelativeSizeAxes = Axes.Both
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.X),
InnerRadius = RANK_CIRCLE_RADIUS,
Current = { Value = accuracyX }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.S),
InnerRadius = RANK_CIRCLE_RADIUS,
Current = { Value = accuracyX - virtual_ss_percentage }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.A),
InnerRadius = RANK_CIRCLE_RADIUS,
Current = { Value = accuracyS }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.B),
InnerRadius = RANK_CIRCLE_RADIUS,
Current = { Value = accuracyA }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.C),
InnerRadius = RANK_CIRCLE_RADIUS,
Current = { Value = accuracyB }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.D),
InnerRadius = RANK_CIRCLE_RADIUS,
Current = { Value = accuracyC }
},
new RankNotch((float)accuracyX),
new RankNotch((float)(accuracyX - virtual_ss_percentage)),
new RankNotch((float)accuracyS),
new RankNotch((float)accuracyA),
new RankNotch((float)accuracyB),
new RankNotch((float)accuracyC),
new BufferedContainer
{
Name = "Graded circle mask",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(1),
Blending = new BlendingParameters
{
Source = BlendingType.DstColor,
Destination = BlendingType.OneMinusSrcColor,
SourceAlpha = BlendingType.One,
DestinationAlpha = BlendingType.SrcAlpha
},
Child = innerMask = new CircularProgress
{
RelativeSizeAxes = Axes.Both,
InnerRadius = RANK_CIRCLE_RADIUS - 0.02f,
}
}
} }
}, },
badges = new Container<RankBadge> badges = new Container<RankBadge>
@ -248,7 +182,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
new RankBadge(accuracyB, Interpolation.Lerp(accuracyB, accuracyA, 0.5), getRank(ScoreRank.B)), new RankBadge(accuracyB, Interpolation.Lerp(accuracyB, accuracyA, 0.5), getRank(ScoreRank.B)),
// The S and A badges are moved down slightly to prevent collision with the SS badge. // The S and A badges are moved down slightly to prevent collision with the SS badge.
new RankBadge(accuracyA, Interpolation.Lerp(accuracyA, accuracyS, 0.25), getRank(ScoreRank.A)), new RankBadge(accuracyA, Interpolation.Lerp(accuracyA, accuracyS, 0.25), getRank(ScoreRank.A)),
new RankBadge(accuracyS, Interpolation.Lerp(accuracyS, (accuracyX - virtual_ss_percentage), 0.25), getRank(ScoreRank.S)), new RankBadge(accuracyS, Interpolation.Lerp(accuracyS, (accuracyX - VIRTUAL_SS_PERCENTAGE), 0.25), getRank(ScoreRank.S)),
new RankBadge(accuracyX, accuracyX, getRank(ScoreRank.X)), new RankBadge(accuracyX, accuracyX, getRank(ScoreRank.X)),
} }
}, },
@ -296,7 +230,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
} }
using (BeginDelayedSequence(RANK_CIRCLE_TRANSFORM_DELAY)) using (BeginDelayedSequence(RANK_CIRCLE_TRANSFORM_DELAY))
innerMask.FillTo(1f, RANK_CIRCLE_TRANSFORM_DURATION, ACCURACY_TRANSFORM_EASING); gradedCircles.TransformTo(nameof(GradedCircles.Progress), 1.0, RANK_CIRCLE_TRANSFORM_DURATION, ACCURACY_TRANSFORM_EASING);
using (BeginDelayedSequence(ACCURACY_TRANSFORM_DELAY)) using (BeginDelayedSequence(ACCURACY_TRANSFORM_DELAY))
{ {
@ -313,10 +247,10 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
// to prevent ambiguity on what grade it's pointing at. // to prevent ambiguity on what grade it's pointing at.
foreach (double p in notchPercentages) foreach (double p in notchPercentages)
{ {
if (Precision.AlmostEquals(p, targetAccuracy, NOTCH_WIDTH_PERCENTAGE / 2)) if (Precision.AlmostEquals(p, targetAccuracy, GRADE_SPACING_PERCENTAGE / 2))
{ {
int tippingDirection = targetAccuracy - p >= 0 ? 1 : -1; // We "round up" here to match rank criteria int tippingDirection = targetAccuracy - p >= 0 ? 1 : -1; // We "round up" here to match rank criteria
targetAccuracy = p + tippingDirection * (NOTCH_WIDTH_PERCENTAGE / 2); targetAccuracy = p + tippingDirection * (GRADE_SPACING_PERCENTAGE / 2);
break; break;
} }
} }
@ -325,7 +259,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
if (score.Rank == ScoreRank.X || score.Rank == ScoreRank.XH) if (score.Rank == ScoreRank.X || score.Rank == ScoreRank.XH)
targetAccuracy = 1; targetAccuracy = 1;
else else
targetAccuracy = Math.Min(accuracyX - virtual_ss_percentage - NOTCH_WIDTH_PERCENTAGE / 2, targetAccuracy); targetAccuracy = Math.Min(accuracyX - VIRTUAL_SS_PERCENTAGE - GRADE_SPACING_PERCENTAGE / 2, targetAccuracy);
// The accuracy circle gauge visually fills up a bit too much. // The accuracy circle gauge visually fills up a bit too much.
// This wouldn't normally matter but we want it to align properly with the inner graded circle in the above cases. // This wouldn't normally matter but we want it to align properly with the inner graded circle in the above cases.
@ -365,7 +299,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
continue; continue;
using (BeginDelayedSequence( using (BeginDelayedSequence(
inverseEasing(ACCURACY_TRANSFORM_EASING, Math.Min(accuracyX - virtual_ss_percentage, badge.Accuracy) / targetAccuracy) * ACCURACY_TRANSFORM_DURATION)) inverseEasing(ACCURACY_TRANSFORM_EASING, Math.Min(accuracyX - VIRTUAL_SS_PERCENTAGE, badge.Accuracy) / targetAccuracy) * ACCURACY_TRANSFORM_DURATION))
{ {
badge.Appear(); badge.Appear();
@ -425,7 +359,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
.FadeOut(800, Easing.Out); .FadeOut(800, Easing.Out);
accuracyCircle accuracyCircle
.FillTo(accuracyS - NOTCH_WIDTH_PERCENTAGE / 2 - visual_alignment_offset, 70, Easing.OutQuint); .FillTo(accuracyS - GRADE_SPACING_PERCENTAGE / 2 - visual_alignment_offset, 70, Easing.OutQuint);
badges.Single(b => b.Rank == getRank(ScoreRank.S)) badges.Single(b => b.Rank == getRank(ScoreRank.S))
.FadeOut(70, Easing.OutQuint); .FadeOut(70, Easing.OutQuint);

View File

@ -0,0 +1,89 @@
// 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.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Scoring;
namespace osu.Game.Screens.Ranking.Expanded.Accuracy
{
public partial class GradedCircles : CompositeDrawable
{
private double progress;
public double Progress
{
get => progress;
set
{
progress = value;
foreach (var circle in circles)
circle.RevealProgress = value;
}
}
private readonly Container<GradedCircle> circles;
public GradedCircles(double accuracyC, double accuracyB, double accuracyA, double accuracyS, double accuracyX)
{
InternalChild = circles = new Container<GradedCircle>
{
RelativeSizeAxes = Axes.Both,
Children = new[]
{
new GradedCircle(0.0, accuracyC)
{
Colour = OsuColour.ForRank(ScoreRank.D),
},
new GradedCircle(accuracyC, accuracyB)
{
Colour = OsuColour.ForRank(ScoreRank.C),
},
new GradedCircle(accuracyB, accuracyA)
{
Colour = OsuColour.ForRank(ScoreRank.B),
},
new GradedCircle(accuracyA, accuracyS)
{
Colour = OsuColour.ForRank(ScoreRank.A),
},
new GradedCircle(accuracyS, accuracyX - AccuracyCircle.VIRTUAL_SS_PERCENTAGE)
{
Colour = OsuColour.ForRank(ScoreRank.S),
},
new GradedCircle(accuracyX - AccuracyCircle.VIRTUAL_SS_PERCENTAGE, 1.0)
{
Colour = OsuColour.ForRank(ScoreRank.X)
}
}
};
}
private partial class GradedCircle : CircularProgress
{
public double RevealProgress
{
set => Current.Value = Math.Clamp(value, startProgress, endProgress) - startProgress;
}
private readonly double startProgress;
private readonly double endProgress;
public GradedCircle(double startProgress, double endProgress)
{
this.startProgress = startProgress + AccuracyCircle.GRADE_SPACING_PERCENTAGE * 0.5;
this.endProgress = endProgress - AccuracyCircle.GRADE_SPACING_PERCENTAGE * 0.5;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
InnerRadius = AccuracyCircle.RANK_CIRCLE_RADIUS;
Rotation = (float)this.startProgress * 360;
}
}
}
}

View File

@ -1,49 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osuTK;
namespace osu.Game.Screens.Ranking.Expanded.Accuracy
{
/// <summary>
/// A solid "notch" of the <see cref="AccuracyCircle"/> that appears at the ends of the rank circles to add separation.
/// </summary>
public partial class RankNotch : CompositeDrawable
{
private readonly float position;
public RankNotch(float position)
{
this.position = position;
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Rotation = position * 360f,
Child = new Box
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y,
Height = AccuracyCircle.RANK_CIRCLE_RADIUS,
Width = (float)AccuracyCircle.NOTCH_WIDTH_PERCENTAGE * 360f,
Colour = OsuColour.Gray(0.3f),
EdgeSmoothness = new Vector2(1f)
}
};
}
}
}

View File

@ -75,15 +75,10 @@ namespace osu.Game.Screens.Select
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Colour4.Black.Opacity(0.3f), Colour = Colour4.Black.Opacity(0.3f),
}, },
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = spacing },
Children = new Drawable[]
{
new GridContainer new GridContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = spacing },
RowDimensions = new[] RowDimensions = new[]
{ {
new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize),
@ -167,8 +162,6 @@ namespace osu.Game.Screens.Select
} }
} }
}, },
},
},
loading = new LoadingLayer(true) loading = new LoadingLayer(true)
}; };
} }

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