diff --git a/osu.Android.props b/osu.Android.props
index c0c75b8d71..e5b0245dd0 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index cd31df316a..2079f136d2 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -16,6 +16,7 @@ using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Updater;
+using osu.Desktop.Windows;
namespace osu.Desktop
{
@@ -98,6 +99,9 @@ namespace osu.Desktop
LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, Add);
LoadComponentAsync(new DiscordRichPresence(), Add);
+
+ if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
+ LoadComponentAsync(new GameplayWinKeyBlocker(), Add);
}
protected override void ScreenChanged(IScreen lastScreen, IScreen newScreen)
diff --git a/osu.Desktop/Windows/GameplayWinKeyBlocker.cs b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs
new file mode 100644
index 0000000000..86174ceb90
--- /dev/null
+++ b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs
@@ -0,0 +1,41 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Platform;
+using osu.Game.Configuration;
+
+namespace osu.Desktop.Windows
+{
+ public class GameplayWinKeyBlocker : Component
+ {
+ private Bindable allowScreenSuspension;
+ private Bindable disableWinKey;
+
+ private GameHost host;
+
+ [BackgroundDependencyLoader]
+ private void load(GameHost host, OsuConfigManager config)
+ {
+ this.host = host;
+
+ allowScreenSuspension = host.AllowScreenSuspension.GetBoundCopy();
+ allowScreenSuspension.BindValueChanged(_ => updateBlocking());
+
+ disableWinKey = config.GetBindable(OsuSetting.GameplayDisableWinKey);
+ disableWinKey.BindValueChanged(_ => updateBlocking(), true);
+ }
+
+ private void updateBlocking()
+ {
+ bool shouldDisable = disableWinKey.Value && !allowScreenSuspension.Value;
+
+ if (shouldDisable)
+ host.InputThread.Scheduler.Add(WindowsKey.Disable);
+ else
+ host.InputThread.Scheduler.Add(WindowsKey.Enable);
+ }
+ }
+}
diff --git a/osu.Desktop/Windows/WindowsKey.cs b/osu.Desktop/Windows/WindowsKey.cs
new file mode 100644
index 0000000000..f19d741107
--- /dev/null
+++ b/osu.Desktop/Windows/WindowsKey.cs
@@ -0,0 +1,80 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Runtime.InteropServices;
+
+namespace osu.Desktop.Windows
+{
+ internal class WindowsKey
+ {
+ private delegate int LowLevelKeyboardProcDelegate(int nCode, int wParam, ref KdDllHookStruct lParam);
+
+ private static bool isBlocked;
+
+ private const int wh_keyboard_ll = 13;
+ private const int wm_keydown = 256;
+ private const int wm_syskeyup = 261;
+
+ //Resharper disable once NotAccessedField.Local
+ private static LowLevelKeyboardProcDelegate keyboardHookDelegate; // keeping a reference alive for the GC
+ private static IntPtr keyHook;
+
+ [StructLayout(LayoutKind.Explicit)]
+ private readonly struct KdDllHookStruct
+ {
+ [FieldOffset(0)]
+ public readonly int VkCode;
+
+ [FieldOffset(8)]
+ public readonly int Flags;
+ }
+
+ private static int lowLevelKeyboardProc(int nCode, int wParam, ref KdDllHookStruct lParam)
+ {
+ if (wParam >= wm_keydown && wParam <= wm_syskeyup)
+ {
+ switch (lParam.VkCode)
+ {
+ case 0x5B: // left windows key
+ case 0x5C: // right windows key
+ return 1;
+ }
+ }
+
+ return callNextHookEx(0, nCode, wParam, ref lParam);
+ }
+
+ internal static void Disable()
+ {
+ if (keyHook != IntPtr.Zero || isBlocked)
+ return;
+
+ keyHook = setWindowsHookEx(wh_keyboard_ll, (keyboardHookDelegate = lowLevelKeyboardProc), Marshal.GetHINSTANCE(System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0]), 0);
+
+ isBlocked = true;
+ }
+
+ internal static void Enable()
+ {
+ if (keyHook == IntPtr.Zero || !isBlocked)
+ return;
+
+ keyHook = unhookWindowsHookEx(keyHook);
+ keyboardHookDelegate = null;
+
+ keyHook = IntPtr.Zero;
+
+ isBlocked = false;
+ }
+
+ [DllImport(@"user32.dll", EntryPoint = @"SetWindowsHookExA")]
+ private static extern IntPtr setWindowsHookEx(int idHook, LowLevelKeyboardProcDelegate lpfn, IntPtr hMod, int dwThreadId);
+
+ [DllImport(@"user32.dll", EntryPoint = @"UnhookWindowsHookEx")]
+ private static extern IntPtr unhookWindowsHookEx(IntPtr hHook);
+
+ [DllImport(@"user32.dll", EntryPoint = @"CallNextHookEx")]
+ private static extern int callNextHookEx(int hHook, int nCode, int wParam, ref KdDllHookStruct lParam);
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs
index 3e06e78dba..c1b7214d72 100644
--- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs
+++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs
@@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
public void TestDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Droplet { StartTime = 1000 }), shouldMiss);
// We only care about testing misses, hits are tested via JuiceStream
- [TestCase(true)]
+ [TestCase(false)]
public void TestTinyDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new TinyDroplet { StartTime = 1000 }), shouldMiss);
}
}
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
index 2ee7cea645..d700f79e5b 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs
@@ -78,7 +78,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
if (mods.Any(m => m is ModHidden))
{
- value *= 1.05 + 0.075 * (10.0 - Math.Min(10.0, Attributes.ApproachRate)); // 7.5% for each AR below 10
// Hiddens gives almost nothing on max approach rate, and more the lower it is
if (approachRate <= 10.0)
value *= 1.05 + 0.075 * (10.0 - approachRate); // 7.5% for each AR below 10
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs b/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs
index e3391c47f1..fb92399102 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs
@@ -1,17 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Game.Rulesets.Catch.Judgements;
-using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModPerfect : ModPerfect
{
- protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
- => !(result.Judgement is CatchBananaJudgement)
- && base.FailCondition(healthProcessor, result);
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
index c3b4d2625e..854626d362 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
@@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.Tests
const double time_slider = 1500;
const double time_circle = 1510;
Vector2 positionCircle = Vector2.Zero;
- Vector2 positionSlider = new Vector2(80);
+ Vector2 positionSlider = new Vector2(30);
var hitObjects = new List
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index 6b1394d799..6e277ff37e 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -1,26 +1,28 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
-using osu.Framework.Utils;
+using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Timing;
+using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Replays;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
-using osuTK;
-using System.Collections.Generic;
-using System.Linq;
-using osu.Framework.Graphics.Sprites;
-using osu.Game.Replays;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Storyboards;
+using osu.Game.Tests.Visual;
+using osuTK;
using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
namespace osu.Game.Rulesets.Osu.Tests
@@ -34,6 +36,8 @@ namespace osu.Game.Rulesets.Osu.Tests
protected override bool Autoplay => true;
+ protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer();
+
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
@@ -129,18 +133,44 @@ namespace osu.Game.Rulesets.Osu.Tests
.ToList()
};
+ [Test]
+ public void TestSpinnerNormalBonusRewinding()
+ {
+ addSeekStep(1000);
+
+ AddAssert("player score matching expected bonus score", () =>
+ {
+ // multipled by 2 to nullify the score multiplier. (autoplay mod selected)
+ var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
+ return totalScore == (int)(drawableSpinner.Disc.CumulativeRotation / 360) * 100;
+ });
+
+ addSeekStep(0);
+
+ AddAssert("player score is 0", () => ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value == 0);
+ }
+
+ [Test]
+ public void TestSpinnerCompleteBonusRewinding()
+ {
+ addSeekStep(2500);
+ addSeekStep(0);
+
+ AddAssert("player score is 0", () => ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value == 0);
+ }
+
[Test]
public void TestSpinPerMinuteOnRewind()
{
double estimatedSpm = 0;
- addSeekStep(2500);
+ addSeekStep(1000);
AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute);
- addSeekStep(5000);
+ addSeekStep(2000);
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
- addSeekStep(2500);
+ addSeekStep(1000);
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
}
@@ -160,12 +190,17 @@ namespace osu.Game.Rulesets.Osu.Tests
Position = new Vector2(256, 192),
EndTime = 6000,
},
- // placeholder object to avoid hitting the results screen
- new HitCircle
- {
- StartTime = 99999,
- }
}
};
+
+ private class ScoreExposedPlayer : TestPlayer
+ {
+ public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
+
+ public ScoreExposedPlayer()
+ : base(false, false)
+ {
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 72502c02cd..07f40f763b 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
using osuTK;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
@@ -11,6 +12,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Skinning;
+using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osuTK.Graphics;
using osu.Game.Skinning;
@@ -81,6 +83,42 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
foreach (var drawableHitObject in NestedHitObjects)
drawableHitObject.AccentColour.Value = colour.NewValue;
}, true);
+
+ Tracking.BindValueChanged(updateSlidingSample);
+ }
+
+ private SkinnableSound slidingSample;
+
+ protected override void LoadSamples()
+ {
+ base.LoadSamples();
+
+ slidingSample?.Expire();
+ slidingSample = null;
+
+ var firstSample = HitObject.Samples.FirstOrDefault();
+
+ if (firstSample != null)
+ {
+ var clone = HitObject.SampleControlPoint.ApplyTo(firstSample);
+ clone.Name = "sliderslide";
+
+ AddInternal(slidingSample = new SkinnableSound(clone)
+ {
+ Looping = true
+ });
+ }
+ }
+
+ private void updateSlidingSample(ValueChangedEvent tracking)
+ {
+ // note that samples will not start playing if exiting a seek operation in the middle of a slider.
+ // may be something we want to address at a later point, but not so easy to make happen right now
+ // (SkinnableSound would need to expose whether the sample is already playing and this logic would need to run in Update).
+ if (tracking.NewValue && ShouldPlaySamples)
+ slidingSample?.Play();
+ else
+ slidingSample?.Stop();
}
protected override void AddNestedHitObject(DrawableHitObject hitObject)
@@ -156,6 +194,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Tracking.Value = Ball.Tracking;
+ if (Tracking.Value && slidingSample != null)
+ // keep the sliding sample playing at the current tracking position
+ slidingSample.Balance.Value = CalculateSamplePlaybackBalance(Ball.X / OsuPlayfield.BASE_SIZE.X);
+
double completionProgress = Math.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1);
Ball.UpdateProgress(completionProgress);
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 9c4608cbb1..ecf78efdd9 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -14,6 +14,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Rulesets.Objects;
using osu.Framework.Utils;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Ranking;
@@ -24,9 +25,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
protected readonly Spinner Spinner;
+ private readonly Container ticks;
+
public readonly SpinnerDisc Disc;
public readonly SpinnerTicks Ticks;
public readonly SpinnerSpmCounter SpmCounter;
+ private readonly SpinnerBonusDisplay bonusDisplay;
private readonly Container mainContainer;
@@ -60,6 +64,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
InternalChildren = new Drawable[]
{
+ ticks = new Container(),
circleContainer = new Container
{
AutoSizeAxes = Axes.Both,
@@ -120,10 +125,48 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre,
Y = 120,
Alpha = 0
+ },
+ bonusDisplay = new SpinnerBonusDisplay
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Y = -120,
}
};
}
+ protected override void AddNestedHitObject(DrawableHitObject hitObject)
+ {
+ base.AddNestedHitObject(hitObject);
+
+ switch (hitObject)
+ {
+ case DrawableSpinnerTick tick:
+ ticks.Add(tick);
+ break;
+ }
+ }
+
+ protected override void ClearNestedHitObjects()
+ {
+ base.ClearNestedHitObjects();
+ ticks.Clear();
+ }
+
+ protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
+ {
+ switch (hitObject)
+ {
+ case SpinnerBonusTick bonusTick:
+ return new DrawableSpinnerBonusTick(bonusTick);
+
+ case SpinnerTick tick:
+ return new DrawableSpinnerTick(tick);
+ }
+
+ return base.CreateNestedHitObject(hitObject);
+ }
+
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
@@ -156,6 +199,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (userTriggered || Time.Current < Spinner.EndTime)
return;
+ // Trigger a miss result for remaining ticks to avoid infinite gameplay.
+ foreach (var tick in ticks.Where(t => !t.IsHit))
+ tick.TriggerResult(false);
+
ApplyResult(r =>
{
if (Progress >= 1)
@@ -185,8 +232,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
circle.Rotation = Disc.Rotation;
Ticks.Rotation = Disc.Rotation;
+
SpmCounter.SetRotation(Disc.CumulativeRotation);
+ updateBonusScore();
+
float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight;
float targetScale = relativeCircleScale + (1 - relativeCircleScale) * Progress;
Disc.Scale = new Vector2((float)Interpolation.Lerp(Disc.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1)));
@@ -194,6 +244,38 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, Disc.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1));
}
+ private int wholeSpins;
+
+ private void updateBonusScore()
+ {
+ if (ticks.Count == 0)
+ return;
+
+ int spins = (int)(Disc.CumulativeRotation / 360);
+
+ if (spins < wholeSpins)
+ {
+ // rewinding, silently handle
+ wholeSpins = spins;
+ return;
+ }
+
+ while (wholeSpins != spins)
+ {
+ var tick = ticks.FirstOrDefault(t => !t.IsHit);
+
+ // tick may be null if we've hit the spin limit.
+ if (tick != null)
+ {
+ tick.TriggerResult(true);
+ if (tick is DrawableSpinnerBonusTick)
+ bonusDisplay.SetBonusCount(spins - Spinner.SpinsRequired);
+ }
+
+ wholeSpins++;
+ }
+ }
+
protected override void UpdateInitialTransforms()
{
base.UpdateInitialTransforms();
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs
new file mode 100644
index 0000000000..2e1c07c4c6
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs
@@ -0,0 +1,13 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Rulesets.Osu.Objects.Drawables
+{
+ public class DrawableSpinnerBonusTick : DrawableSpinnerTick
+ {
+ public DrawableSpinnerBonusTick(SpinnerBonusTick spinnerTick)
+ : base(spinnerTick)
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs
new file mode 100644
index 0000000000..c390b673be
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs
@@ -0,0 +1,23 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Osu.Objects.Drawables
+{
+ public class DrawableSpinnerTick : DrawableOsuHitObject
+ {
+ public override bool DisplayResult => false;
+
+ public DrawableSpinnerTick(SpinnerTick spinnerTick)
+ : base(spinnerTick)
+ {
+ }
+
+ ///
+ /// Apply a judgement result.
+ ///
+ /// Whether this tick was reached.
+ internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : HitResult.Miss);
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
index 395c76a233..07dc6021c9 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private readonly Slider slider;
private readonly Drawable followCircle;
private readonly DrawableSlider drawableSlider;
- private readonly CircularContainer ball;
+ private readonly Drawable ball;
public SliderBall(Slider slider, DrawableSlider drawableSlider = null)
{
@@ -54,19 +54,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Alpha = 0,
Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()),
},
- ball = new CircularContainer
+ ball = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall())
{
- Masking = true,
- RelativeSizeAxes = Axes.Both,
- Origin = Anchor.Centre,
Anchor = Anchor.Centre,
- Alpha = 1,
- Child = new Container
- {
- RelativeSizeAxes = Axes.Both,
- Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall()),
- }
- }
+ Origin = Anchor.Centre,
+ },
};
}
@@ -187,12 +179,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
return;
Position = newPos;
- Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI);
+ ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI);
lastPosition = newPos;
}
- private class FollowCircleContainer : Container
+ private class FollowCircleContainer : CircularContainer
{
public override bool HandlePositionalInput => true;
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs
new file mode 100644
index 0000000000..a8f5580735
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs
@@ -0,0 +1,44 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+
+namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
+{
+ ///
+ /// Shows incremental bonus score achieved for a spinner.
+ ///
+ public class SpinnerBonusDisplay : CompositeDrawable
+ {
+ private readonly OsuSpriteText bonusCounter;
+
+ public SpinnerBonusDisplay()
+ {
+ AutoSizeAxes = Axes.Both;
+
+ InternalChild = bonusCounter = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Font = OsuFont.Numeric.With(size: 24),
+ Alpha = 0,
+ };
+ }
+
+ private int displayedCount;
+
+ public void SetBonusCount(int count)
+ {
+ if (displayedCount == count)
+ return;
+
+ displayedCount = count;
+ bonusCounter.Text = $"{1000 * count}";
+ bonusCounter.FadeOutFromOne(1500);
+ bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
index 418375c090..619b49926e 100644
--- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
@@ -3,9 +3,9 @@
using System;
using osu.Game.Beatmaps;
-using osu.Game.Rulesets.Objects.Types;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
@@ -26,14 +26,43 @@ namespace osu.Game.Rulesets.Osu.Objects
///
public int SpinsRequired { get; protected set; } = 1;
+ ///
+ /// Number of spins available to give bonus, beyond .
+ ///
+ public int MaximumBonusSpins { get; protected set; } = 1;
+
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
- SpinsRequired = (int)(Duration / 1000 * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5));
-
// spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being.
- SpinsRequired = (int)Math.Max(1, SpinsRequired * 0.6);
+ const double stable_matching_fudge = 0.6;
+
+ // close to 477rpm
+ const double maximum_rotations_per_second = 8;
+
+ double secondsDuration = Duration / 1000;
+
+ double minimumRotationsPerSecond = stable_matching_fudge * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5);
+
+ SpinsRequired = (int)Math.Max(1, (secondsDuration * minimumRotationsPerSecond));
+ MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration);
+ }
+
+ protected override void CreateNestedHitObjects()
+ {
+ base.CreateNestedHitObjects();
+
+ int totalSpins = MaximumBonusSpins + SpinsRequired;
+
+ for (int i = 0; i < totalSpins; i++)
+ {
+ double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
+
+ AddNested(i < SpinsRequired
+ ? new SpinnerTick { StartTime = startTime }
+ : new SpinnerBonusTick { StartTime = startTime });
+ }
}
public override Judgement CreateJudgement() => new OsuJudgement();
diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs
new file mode 100644
index 0000000000..6ca2d4d72d
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs
@@ -0,0 +1,26 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Audio;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Osu.Objects
+{
+ public class SpinnerBonusTick : SpinnerTick
+ {
+ public SpinnerBonusTick()
+ {
+ Samples.Add(new HitSampleInfo { Name = "spinnerbonus" });
+ }
+
+ public override Judgement CreateJudgement() => new OsuSpinnerBonusTickJudgement();
+
+ public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement
+ {
+ protected override int NumericResultFor(HitResult result) => 1100;
+
+ protected override double HealthIncreaseFor(HitResult result) => base.HealthIncreaseFor(result) * 2;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs
new file mode 100644
index 0000000000..c81348fbbf
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs
@@ -0,0 +1,25 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Osu.Judgements;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Osu.Objects
+{
+ public class SpinnerTick : OsuHitObject
+ {
+ public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement();
+
+ protected override HitWindows CreateHitWindows() => HitWindows.Empty;
+
+ public class OsuSpinnerTickJudgement : OsuJudgement
+ {
+ public override bool AffectsCombo => false;
+
+ protected override int NumericResultFor(HitResult result) => 100;
+
+ protected override double HealthIncreaseFor(HitResult result) => result == MaxResult ? 0.6 * base.HealthIncreaseFor(result) : 0;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
index 9ab358ee12..3356a0fbe0 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Replays
///
protected static readonly Vector2 SPINNER_CENTRE = OsuPlayfield.BASE_SIZE / 2;
- protected const float SPIN_RADIUS = 50;
+ public const float SPIN_RADIUS = 50;
///
/// The time in ms between each ReplayFrame.
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs
index b4ed75d97c..0f586034d5 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs
@@ -15,6 +15,9 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
private readonly Drawable animationContent;
+ private Sprite layerNd;
+ private Sprite layerSpec;
+
public LegacySliderBall(Drawable animationContent)
{
this.animationContent = animationContent;
@@ -29,18 +32,37 @@ namespace osu.Game.Rulesets.Osu.Skinning
InternalChildren = new[]
{
- new Sprite
+ layerNd = new Sprite
{
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
Texture = skin.GetTexture("sliderb-nd"),
Colour = new Color4(5, 5, 5, 255),
},
- animationContent,
- new Sprite
+ animationContent.With(d =>
{
+ d.Anchor = Anchor.Centre;
+ d.Origin = Anchor.Centre;
+ }),
+ layerSpec = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
Texture = skin.GetTexture("sliderb-spec"),
Blending = BlendingParameters.Additive,
},
};
}
+
+ protected override void UpdateAfterChildren()
+ {
+ base.UpdateAfterChildren();
+
+ //undo rotation on layers which should not be rotated.
+ float appliedRotation = Parent.Rotation;
+
+ layerNd.Rotation = -appliedRotation;
+ layerSpec.Rotation = -appliedRotation;
+ }
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonclear.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonclear.png
new file mode 100644
index 0000000000..c5bcdbd3fc
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonclear.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonfail.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonfail.png
new file mode 100644
index 0000000000..39cf737285
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonfail.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonidle.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonidle.png
new file mode 100644
index 0000000000..4c3b2bfec9
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonidle.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonkiai.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonkiai.png
new file mode 100644
index 0000000000..7de00b5390
Binary files /dev/null and b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonkiai.png differ
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs
index d200c44a02..47d8a5c012 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs
@@ -8,6 +8,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Animations;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -36,6 +37,10 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
private TaikoScoreProcessor scoreProcessor;
private IEnumerable mascots => this.ChildrenOfType();
+
+ private IEnumerable animatedMascots =>
+ mascots.Where(mascot => mascot.ChildrenOfType().All(animation => animation.FrameCount > 0));
+
private IEnumerable playfields => this.ChildrenOfType();
[SetUp]
@@ -72,11 +77,11 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
AddStep("set clear state", () => mascots.ForEach(mascot => mascot.State.Value = TaikoMascotAnimationState.Clear));
AddStep("miss", () => mascots.ForEach(mascot => mascot.LastResult.Value = new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }));
- AddAssert("skins with animations remain in clear state", () => someMascotsIn(TaikoMascotAnimationState.Clear));
+ AddAssert("skins with animations remain in clear state", () => animatedMascotsIn(TaikoMascotAnimationState.Clear));
AddUntilStep("state reverts to fail", () => allMascotsIn(TaikoMascotAnimationState.Fail));
AddStep("set clear state again", () => mascots.ForEach(mascot => mascot.State.Value = TaikoMascotAnimationState.Clear));
- AddAssert("skins with animations change to clear", () => someMascotsIn(TaikoMascotAnimationState.Clear));
+ AddAssert("skins with animations change to clear", () => animatedMascotsIn(TaikoMascotAnimationState.Clear));
}
[Test]
@@ -111,7 +116,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail);
- assertStateAfterResult(new JudgementResult(new DrumRoll(), new TaikoDrumRollJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Fail);
+ assertStateAfterResult(new JudgementResult(new DrumRoll(), new TaikoDrumRollJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Idle);
}
@@ -186,10 +191,18 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
private void assertStateAfterResult(JudgementResult judgementResult, TaikoMascotAnimationState expectedState)
{
- AddStep($"{judgementResult.Type.ToString().ToLower()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}",
- () => applyNewResult(judgementResult));
+ TaikoMascotAnimationState[] mascotStates = null;
- AddAssert($"state is {expectedState.ToString().ToLower()}", () => allMascotsIn(expectedState));
+ AddStep($"{judgementResult.Type.ToString().ToLower()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}",
+ () =>
+ {
+ applyNewResult(judgementResult);
+ // store the states as soon as possible, so that the delay between steps doesn't incorrectly fail the test
+ // due to not checking if the state changed quickly enough.
+ Schedule(() => mascotStates = animatedMascots.Select(mascot => mascot.State.Value).ToArray());
+ });
+
+ AddAssert($"state is {expectedState.ToString().ToLower()}", () => mascotStates.All(state => state == expectedState));
}
private void applyNewResult(JudgementResult judgementResult)
@@ -211,6 +224,6 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
}
private bool allMascotsIn(TaikoMascotAnimationState state) => mascots.All(d => d.State.Value == state);
- private bool someMascotsIn(TaikoMascotAnimationState state) => mascots.Any(d => d.State.Value == state);
+ private bool animatedMascotsIn(TaikoMascotAnimationState state) => animatedMascots.Any(d => d.State.Value == state);
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs
index aaa634648a..0be005e1c4 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
};
[Test]
- public void TestSpinnerDoesNotFail()
+ public void TestSpinnerDoesFail()
{
bool judged = false;
AddStep("Setup judgements", () =>
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
Player.ScoreProcessor.NewJudgement += b => judged = true;
});
AddUntilStep("swell judged", () => judged);
- AddAssert("not failed", () => !Player.HasFailed);
+ AddAssert("failed", () => Player.HasFailed);
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs
new file mode 100644
index 0000000000..7c39c040b1
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs
@@ -0,0 +1,64 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Audio;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Taiko.Audio
+{
+ ///
+ /// Stores samples for the input drum.
+ /// The lifetime of the samples is adjusted so that they are only alive during the appropriate sample control point.
+ ///
+ public class DrumSampleContainer : LifetimeManagementContainer
+ {
+ private readonly ControlPointInfo controlPoints;
+ private readonly Dictionary mappings = new Dictionary();
+
+ public DrumSampleContainer(ControlPointInfo controlPoints)
+ {
+ this.controlPoints = controlPoints;
+
+ IReadOnlyList samplePoints = controlPoints.SamplePoints.Count == 0 ? new[] { controlPoints.SamplePointAt(double.MinValue) } : controlPoints.SamplePoints;
+
+ for (int i = 0; i < samplePoints.Count; i++)
+ {
+ var samplePoint = samplePoints[i];
+
+ var centre = samplePoint.GetSampleInfo();
+ var rim = samplePoint.GetSampleInfo(HitSampleInfo.HIT_CLAP);
+
+ var lifetimeStart = i > 0 ? samplePoint.Time : double.MinValue;
+ var lifetimeEnd = i + 1 < samplePoints.Count ? samplePoints[i + 1].Time : double.MaxValue;
+
+ mappings[samplePoint.Time] = new DrumSample
+ {
+ Centre = addSound(centre, lifetimeStart, lifetimeEnd),
+ Rim = addSound(rim, lifetimeStart, lifetimeEnd)
+ };
+ }
+ }
+
+ private SkinnableSound addSound(HitSampleInfo hitSampleInfo, double lifetimeStart, double lifetimeEnd)
+ {
+ var drawable = new SkinnableSound(hitSampleInfo)
+ {
+ LifetimeStart = lifetimeStart,
+ LifetimeEnd = lifetimeEnd
+ };
+ AddInternal(drawable);
+ return drawable;
+ }
+
+ public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time).Time];
+
+ public class DrumSample
+ {
+ public SkinnableSound Centre;
+ public SkinnableSound Rim;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs
deleted file mode 100644
index c31b07344d..0000000000
--- a/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System.Collections.Generic;
-using osu.Game.Audio;
-using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Skinning;
-
-namespace osu.Game.Rulesets.Taiko.Audio
-{
- public class DrumSampleMapping
- {
- private readonly ControlPointInfo controlPoints;
- private readonly Dictionary mappings = new Dictionary();
-
- public readonly List Sounds = new List();
-
- public DrumSampleMapping(ControlPointInfo controlPoints)
- {
- this.controlPoints = controlPoints;
-
- IEnumerable samplePoints = controlPoints.SamplePoints.Count == 0 ? new[] { controlPoints.SamplePointAt(double.MinValue) } : controlPoints.SamplePoints;
-
- foreach (var s in samplePoints)
- {
- var centre = s.GetSampleInfo();
- var rim = s.GetSampleInfo(HitSampleInfo.HIT_CLAP);
-
- mappings[s.Time] = new DrumSample
- {
- Centre = addSound(centre),
- Rim = addSound(rim)
- };
- }
- }
-
- private SkinnableSound addSound(HitSampleInfo hitSampleInfo)
- {
- var drawable = new SkinnableSound(hitSampleInfo);
- Sounds.Add(drawable);
- return drawable;
- }
-
- public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time).Time];
-
- public class DrumSample
- {
- public SkinnableSound Centre;
- public SkinnableSound Rim;
- }
- }
-}
diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs
index 604daa929f..0d91002f4b 100644
--- a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs
+++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs
@@ -7,8 +7,6 @@ namespace osu.Game.Rulesets.Taiko.Judgements
{
public class TaikoDrumRollJudgement : TaikoJudgement
{
- public override bool AffectsCombo => false;
-
protected override double HealthIncreaseFor(HitResult result)
{
// Drum rolls can be ignored with no health penalty
diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs
index 29be5e0eac..4d61efd3ee 100644
--- a/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs
+++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs
@@ -7,8 +7,6 @@ namespace osu.Game.Rulesets.Taiko.Judgements
{
public class TaikoSwellJudgement : TaikoJudgement
{
- public override bool AffectsCombo => false;
-
protected override double HealthIncreaseFor(HitResult result)
{
switch (result)
diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs
index 81d645e294..b7b55b9ae0 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs
@@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
public readonly Sprite Centre;
[Resolved]
- private DrumSampleMapping sampleMappings { get; set; }
+ private DrumSampleContainer sampleContainer { get; set; }
public LegacyHalfDrum(bool flipped)
{
@@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
public bool OnPressed(TaikoAction action)
{
Drawable target = null;
- var drumSample = sampleMappings.SampleAt(Time.Current);
+ var drumSample = sampleContainer.SampleAt(Time.Current);
if (action == CentreAction)
{
diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
index 23d675cfb0..f032c5f485 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
@@ -91,10 +91,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
return null;
case TaikoSkinComponents.Mascot:
- if (GetTexture("pippidonclear0") != null)
- return new DrawableTaikoMascot();
-
- return null;
+ return new DrawableTaikoMascot();
}
return Source.GetDrawableComponent(component);
diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
index 06ccd45cb8..5966b24b34 100644
--- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
@@ -25,11 +25,11 @@ namespace osu.Game.Rulesets.Taiko.UI
private const float middle_split = 0.025f;
[Cached]
- private DrumSampleMapping sampleMapping;
+ private DrumSampleContainer sampleContainer;
public InputDrum(ControlPointInfo controlPoints)
{
- sampleMapping = new DrumSampleMapping(controlPoints);
+ sampleContainer = new DrumSampleContainer(controlPoints);
RelativeSizeAxes = Axes.Both;
}
@@ -37,39 +37,41 @@ namespace osu.Game.Rulesets.Taiko.UI
[BackgroundDependencyLoader]
private void load()
{
- Child = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.InputDrum), _ => new Container
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.Both,
- FillMode = FillMode.Fit,
- Scale = new Vector2(0.9f),
- Children = new Drawable[]
+ new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.InputDrum), _ => new Container
{
- new TaikoHalfDrum(false)
+ RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fit,
+ Scale = new Vector2(0.9f),
+ Children = new Drawable[]
{
- Name = "Left Half",
- Anchor = Anchor.Centre,
- Origin = Anchor.CentreRight,
- RelativeSizeAxes = Axes.Both,
- RelativePositionAxes = Axes.X,
- X = -middle_split / 2,
- RimAction = TaikoAction.LeftRim,
- CentreAction = TaikoAction.LeftCentre
- },
- new TaikoHalfDrum(true)
- {
- Name = "Right Half",
- Anchor = Anchor.Centre,
- Origin = Anchor.CentreLeft,
- RelativeSizeAxes = Axes.Both,
- RelativePositionAxes = Axes.X,
- X = middle_split / 2,
- RimAction = TaikoAction.RightRim,
- CentreAction = TaikoAction.RightCentre
+ new TaikoHalfDrum(false)
+ {
+ Name = "Left Half",
+ Anchor = Anchor.Centre,
+ Origin = Anchor.CentreRight,
+ RelativeSizeAxes = Axes.Both,
+ RelativePositionAxes = Axes.X,
+ X = -middle_split / 2,
+ RimAction = TaikoAction.LeftRim,
+ CentreAction = TaikoAction.LeftCentre
+ },
+ new TaikoHalfDrum(true)
+ {
+ Name = "Right Half",
+ Anchor = Anchor.Centre,
+ Origin = Anchor.CentreLeft,
+ RelativeSizeAxes = Axes.Both,
+ RelativePositionAxes = Axes.X,
+ X = middle_split / 2,
+ RimAction = TaikoAction.RightRim,
+ CentreAction = TaikoAction.RightCentre
+ }
}
- }
- });
-
- AddRangeInternal(sampleMapping.Sounds);
+ }),
+ sampleContainer
+ };
}
///
@@ -93,7 +95,7 @@ namespace osu.Game.Rulesets.Taiko.UI
private readonly Sprite centreHit;
[Resolved]
- private DrumSampleMapping sampleMappings { get; set; }
+ private DrumSampleContainer sampleContainer { get; set; }
public TaikoHalfDrum(bool flipped)
{
@@ -154,7 +156,7 @@ namespace osu.Game.Rulesets.Taiko.UI
Drawable target = null;
Drawable back = null;
- var drumSample = sampleMappings.SampleAt(Time.Current);
+ var drumSample = sampleContainer.SampleAt(Time.Current);
if (action == CentreAction)
{
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs
index 6f25a5f662..9c76aea54c 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs
@@ -128,6 +128,13 @@ namespace osu.Game.Rulesets.Taiko.UI
}
private static Texture getAnimationFrame(ISkin skin, TaikoMascotAnimationState state, int frameIndex)
- => skin.GetTexture($"pippidon{state.ToString().ToLower()}{frameIndex}");
+ {
+ var texture = skin.GetTexture($"pippidon{state.ToString().ToLower()}{frameIndex}");
+
+ if (frameIndex == 0 && texture == null)
+ texture = skin.GetTexture($"pippidon{state.ToString().ToLower()}");
+
+ return texture;
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs
index 9fc7c336cb..0023866124 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs
@@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Game.Online.API;
-using osu.Game.Online.API.Requests;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
@@ -65,11 +64,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void bindHandler(double delay = 0)
{
- var roomScores = new List();
+ var roomScores = new List();
for (int i = 0; i < 10; i++)
{
- roomScores.Add(new RoomScore
+ roomScores.Add(new MultiplayerScore
{
ID = i,
Accuracy = 0.9 - 0.01 * i,
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs
index 60af5b37ef..2a76b8e265 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs
@@ -36,11 +36,11 @@ namespace osu.Game.Tests.Visual.UserInterface
}
});
- addHeader("Orange OverlayHeader (no background)", new TestNoBackgroundHeader(), OverlayColourScheme.Orange);
- addHeader("Blue OverlayHeader", new TestNoControlHeader(), OverlayColourScheme.Blue);
+ addHeader("Orange OverlayHeader (no background, 100 padding)", new TestNoBackgroundHeader(), OverlayColourScheme.Orange);
+ addHeader("Blue OverlayHeader (default 50 padding)", new TestNoControlHeader(), OverlayColourScheme.Blue);
addHeader("Green TabControlOverlayHeader (string) with ruleset selector", new TestStringTabControlHeader(), OverlayColourScheme.Green);
- addHeader("Pink TabControlOverlayHeader (enum)", new TestEnumTabControlHeader(), OverlayColourScheme.Pink);
- addHeader("Red BreadcrumbControlOverlayHeader (no background)", new TestBreadcrumbControlHeader(), OverlayColourScheme.Red);
+ addHeader("Pink TabControlOverlayHeader (enum, 30 padding)", new TestEnumTabControlHeader(), OverlayColourScheme.Pink);
+ addHeader("Red BreadcrumbControlOverlayHeader (no background, 10 padding)", new TestBreadcrumbControlHeader(), OverlayColourScheme.Red);
}
private void addHeader(string name, OverlayHeader header, OverlayColourScheme colourScheme)
@@ -86,6 +86,11 @@ namespace osu.Game.Tests.Visual.UserInterface
private class TestNoBackgroundHeader : OverlayHeader
{
protected override OverlayTitle CreateTitle() => new TestTitle();
+
+ public TestNoBackgroundHeader()
+ {
+ ContentSidePadding = 100;
+ }
}
private class TestNoControlHeader : OverlayHeader
@@ -112,6 +117,11 @@ namespace osu.Game.Tests.Visual.UserInterface
private class TestEnumTabControlHeader : TabControlOverlayHeader
{
+ public TestEnumTabControlHeader()
+ {
+ ContentSidePadding = 30;
+ }
+
protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/rankings");
protected override OverlayTitle CreateTitle() => new TestTitle();
@@ -130,6 +140,8 @@ namespace osu.Game.Tests.Visual.UserInterface
public TestBreadcrumbControlHeader()
{
+ ContentSidePadding = 10;
+
TabControl.AddItem("tab1");
TabControl.AddItem("tab2");
TabControl.Current.Value = "tab2";
diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs
new file mode 100644
index 0000000000..5e644fbf1c
--- /dev/null
+++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs
@@ -0,0 +1,278 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using JetBrains.Annotations;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Lists;
+using osu.Framework.Threading;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mods;
+
+namespace osu.Game.Beatmaps
+{
+ public class BeatmapDifficultyManager : CompositeDrawable
+ {
+ // Too many simultaneous updates can lead to stutters. One thread seems to work fine for song select display purposes.
+ private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(1, nameof(BeatmapDifficultyManager));
+
+ // A permanent cache to prevent re-computations.
+ private readonly ConcurrentDictionary difficultyCache = new ConcurrentDictionary();
+
+ // All bindables that should be updated along with the current ruleset + mods.
+ private readonly LockedWeakList trackedBindables = new LockedWeakList();
+
+ [Resolved]
+ private BeatmapManager beatmapManager { get; set; }
+
+ [Resolved]
+ private Bindable currentRuleset { get; set; }
+
+ [Resolved]
+ private Bindable> currentMods { get; set; }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ currentRuleset.BindValueChanged(_ => updateTrackedBindables());
+ currentMods.BindValueChanged(_ => updateTrackedBindables(), true);
+ }
+
+ ///
+ /// Retrieves a bindable containing the star difficulty of a that follows the currently-selected ruleset and mods.
+ ///
+ /// The to get the difficulty of.
+ /// An optional which stops updating the star difficulty for the given .
+ /// A bindable that is updated to contain the star difficulty when it becomes available.
+ public IBindable GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default)
+ {
+ var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken);
+ trackedBindables.Add(bindable);
+ return bindable;
+ }
+
+ ///
+ /// Retrieves a bindable containing the star difficulty of a with a given and combination.
+ ///
+ ///
+ /// The bindable will not update to follow the currently-selected ruleset and mods.
+ ///
+ /// The to get the difficulty of.
+ /// The to get the difficulty with. If null, the 's ruleset is used.
+ /// The s to get the difficulty with. If null, no mods will be assumed.
+ /// An optional which stops updating the star difficulty for the given .
+ /// A bindable that is updated to contain the star difficulty when it becomes available.
+ public IBindable GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable mods,
+ CancellationToken cancellationToken = default)
+ => createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken);
+
+ ///
+ /// Retrieves the difficulty of a .
+ ///
+ /// The to get the difficulty of.
+ /// The to get the difficulty with.
+ /// The s to get the difficulty with.
+ /// An optional which stops computing the star difficulty.
+ /// The .
+ public async Task GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IEnumerable mods = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key))
+ return existing;
+
+ return await Task.Factory.StartNew(() => computeDifficulty(key, beatmapInfo, rulesetInfo), cancellationToken,
+ TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler);
+ }
+
+ ///
+ /// Retrieves the difficulty of a .
+ ///
+ /// The to get the difficulty of.
+ /// The to get the difficulty with.
+ /// The s to get the difficulty with.
+ /// The .
+ public StarDifficulty GetDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IEnumerable mods = null)
+ {
+ if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key))
+ return existing;
+
+ return computeDifficulty(key, beatmapInfo, rulesetInfo);
+ }
+
+ private CancellationTokenSource trackedUpdateCancellationSource;
+
+ ///
+ /// Updates all tracked using the current ruleset and mods.
+ ///
+ private void updateTrackedBindables()
+ {
+ trackedUpdateCancellationSource?.Cancel();
+ trackedUpdateCancellationSource = new CancellationTokenSource();
+
+ foreach (var b in trackedBindables)
+ {
+ if (trackedUpdateCancellationSource.IsCancellationRequested)
+ break;
+
+ using (var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(trackedUpdateCancellationSource.Token, b.CancellationToken))
+ updateBindable(b, currentRuleset.Value, currentMods.Value, linkedSource.Token);
+ }
+ }
+
+ ///
+ /// Updates the value of a with a given ruleset + mods.
+ ///
+ /// The to update.
+ /// The to update with.
+ /// The s to update with.
+ /// A token that may be used to cancel this update.
+ private void updateBindable([NotNull] BindableStarDifficulty bindable, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable mods, CancellationToken cancellationToken = default)
+ {
+ GetDifficultyAsync(bindable.Beatmap, rulesetInfo, mods, cancellationToken).ContinueWith(t =>
+ {
+ // We're on a threadpool thread, but we should exit back to the update thread so consumers can safely handle value-changed events.
+ Schedule(() =>
+ {
+ if (!cancellationToken.IsCancellationRequested)
+ bindable.Value = t.Result;
+ });
+ }, cancellationToken);
+ }
+
+ ///
+ /// Creates a new and triggers an initial value update.
+ ///
+ /// The that star difficulty should correspond to.
+ /// The initial to get the difficulty with.
+ /// The initial s to get the difficulty with.
+ /// An optional which stops updating the star difficulty for the given .
+ /// The .
+ private BindableStarDifficulty createBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo initialRulesetInfo, [CanBeNull] IEnumerable initialMods,
+ CancellationToken cancellationToken)
+ {
+ var bindable = new BindableStarDifficulty(beatmapInfo, cancellationToken);
+ updateBindable(bindable, initialRulesetInfo, initialMods, cancellationToken);
+ return bindable;
+ }
+
+ ///
+ /// Computes the difficulty defined by a key, and stores it to the timed cache.
+ ///
+ /// The that defines the computation parameters.
+ /// The to compute the difficulty of.
+ /// The to compute the difficulty with.
+ /// The .
+ private StarDifficulty computeDifficulty(in DifficultyCacheLookup key, BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo)
+ {
+ // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset.
+ rulesetInfo ??= beatmapInfo.Ruleset;
+
+ try
+ {
+ var ruleset = rulesetInfo.CreateInstance();
+ Debug.Assert(ruleset != null);
+
+ var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(beatmapInfo));
+ var attributes = calculator.Calculate(key.Mods);
+
+ return difficultyCache[key] = new StarDifficulty(attributes.StarRating);
+ }
+ catch
+ {
+ return difficultyCache[key] = new StarDifficulty(0);
+ }
+ }
+
+ ///
+ /// Attempts to retrieve an existing difficulty for the combination.
+ ///
+ /// The .
+ /// The .
+ /// The s.
+ /// The existing difficulty value, if present.
+ /// The key that was used to perform this lookup. This can be further used to query .
+ /// Whether an existing difficulty was found.
+ private bool tryGetExisting(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IEnumerable mods, out StarDifficulty existingDifficulty, out DifficultyCacheLookup key)
+ {
+ // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset.
+ rulesetInfo ??= beatmapInfo.Ruleset;
+
+ // Difficulty can only be computed if the beatmap and ruleset are locally available.
+ if (beatmapInfo.ID == 0 || rulesetInfo.ID == null)
+ {
+ // If not, fall back to the existing star difficulty (e.g. from an online source).
+ existingDifficulty = new StarDifficulty(beatmapInfo.StarDifficulty);
+ key = default;
+
+ return true;
+ }
+
+ key = new DifficultyCacheLookup(beatmapInfo.ID, rulesetInfo.ID.Value, mods);
+ return difficultyCache.TryGetValue(key, out existingDifficulty);
+ }
+
+ private readonly struct DifficultyCacheLookup : IEquatable
+ {
+ public readonly int BeatmapId;
+ public readonly int RulesetId;
+ public readonly Mod[] Mods;
+
+ public DifficultyCacheLookup(int beatmapId, int rulesetId, IEnumerable mods)
+ {
+ BeatmapId = beatmapId;
+ RulesetId = rulesetId;
+ Mods = mods?.OrderBy(m => m.Acronym).ToArray() ?? Array.Empty();
+ }
+
+ public bool Equals(DifficultyCacheLookup other)
+ => BeatmapId == other.BeatmapId
+ && RulesetId == other.RulesetId
+ && Mods.SequenceEqual(other.Mods);
+
+ public override int GetHashCode()
+ {
+ var hashCode = new HashCode();
+
+ hashCode.Add(BeatmapId);
+ hashCode.Add(RulesetId);
+ foreach (var mod in Mods)
+ hashCode.Add(mod.Acronym);
+
+ return hashCode.ToHashCode();
+ }
+ }
+
+ private class BindableStarDifficulty : Bindable
+ {
+ public readonly BeatmapInfo Beatmap;
+ public readonly CancellationToken CancellationToken;
+
+ public BindableStarDifficulty(BeatmapInfo beatmap, CancellationToken cancellationToken)
+ {
+ Beatmap = beatmap;
+ CancellationToken = cancellationToken;
+ }
+ }
+ }
+
+ public readonly struct StarDifficulty
+ {
+ public readonly double Stars;
+
+ public StarDifficulty(double stars)
+ {
+ Stars = stars;
+
+ // Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...)
+ }
+ }
+}
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index 268328272c..a8a8794320 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -99,6 +99,7 @@ namespace osu.Game.Configuration
Set(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised);
Set(OsuSetting.IncreaseFirstObjectVisibility, true);
+ Set(OsuSetting.GameplayDisableWinKey, true);
// Update
Set(OsuSetting.ReleaseStream, ReleaseStream.Lazer);
@@ -229,6 +230,7 @@ namespace osu.Game.Configuration
IntroSequence,
UIHoldActivationDelay,
HitLighting,
- MenuBackgroundSource
+ MenuBackgroundSource,
+ GameplayDisableWinKey
}
}
diff --git a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs
index b7ea1ba56a..3015c44613 100644
--- a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs
+++ b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@@ -55,7 +54,16 @@ namespace osu.Game.Graphics.Cursor
return;
}
- var newTarget = inputManager.HoveredDrawables.OfType().FirstOrDefault(t => t.ProvidingUserCursor) ?? this;
+ IProvideCursor newTarget = this;
+
+ foreach (var d in inputManager.HoveredDrawables)
+ {
+ if (d is IProvideCursor p && p.ProvidingUserCursor)
+ {
+ newTarget = p;
+ break;
+ }
+ }
if (currentTarget == newTarget)
return;
diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs
index 9804aefce8..d1f6fd445e 100644
--- a/osu.Game/Graphics/ScreenshotManager.cs
+++ b/osu.Game/Graphics/ScreenshotManager.cs
@@ -19,6 +19,7 @@ using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats.Jpeg;
namespace osu.Game.Graphics
{
@@ -119,7 +120,9 @@ namespace osu.Game.Graphics
break;
case ScreenshotFormat.Jpg:
- image.SaveAsJpeg(stream);
+ const int jpeg_quality = 92;
+
+ image.SaveAsJpeg(stream, new JpegEncoder { Quality = jpeg_quality });
break;
default:
diff --git a/osu.Game/Online/API/Requests/Responses/APICreatedRoom.cs b/osu.Game/Online/Multiplayer/APICreatedRoom.cs
similarity index 78%
rename from osu.Game/Online/API/Requests/Responses/APICreatedRoom.cs
rename to osu.Game/Online/Multiplayer/APICreatedRoom.cs
index a554101bc7..2a3bb39647 100644
--- a/osu.Game/Online/API/Requests/Responses/APICreatedRoom.cs
+++ b/osu.Game/Online/Multiplayer/APICreatedRoom.cs
@@ -2,9 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using Newtonsoft.Json;
-using osu.Game.Online.Multiplayer;
-namespace osu.Game.Online.API.Requests.Responses
+namespace osu.Game.Online.Multiplayer
{
public class APICreatedRoom : Room
{
diff --git a/osu.Game/Online/API/APIPlaylistBeatmap.cs b/osu.Game/Online/Multiplayer/APIPlaylistBeatmap.cs
similarity index 94%
rename from osu.Game/Online/API/APIPlaylistBeatmap.cs
rename to osu.Game/Online/Multiplayer/APIPlaylistBeatmap.cs
index 4f7786e880..98972ef36d 100644
--- a/osu.Game/Online/API/APIPlaylistBeatmap.cs
+++ b/osu.Game/Online/Multiplayer/APIPlaylistBeatmap.cs
@@ -6,7 +6,7 @@ using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
-namespace osu.Game.Online.API
+namespace osu.Game.Online.Multiplayer
{
public class APIPlaylistBeatmap : APIBeatmap
{
diff --git a/osu.Game/Online/API/Requests/Responses/APIScoreToken.cs b/osu.Game/Online/Multiplayer/APIScoreToken.cs
similarity index 85%
rename from osu.Game/Online/API/Requests/Responses/APIScoreToken.cs
rename to osu.Game/Online/Multiplayer/APIScoreToken.cs
index 1d2465bedf..1f0063d94e 100644
--- a/osu.Game/Online/API/Requests/Responses/APIScoreToken.cs
+++ b/osu.Game/Online/Multiplayer/APIScoreToken.cs
@@ -3,7 +3,7 @@
using Newtonsoft.Json;
-namespace osu.Game.Online.API.Requests.Responses
+namespace osu.Game.Online.Multiplayer
{
public class APIScoreToken
{
diff --git a/osu.Game/Online/API/Requests/CreateRoomRequest.cs b/osu.Game/Online/Multiplayer/CreateRoomRequest.cs
similarity index 86%
rename from osu.Game/Online/API/Requests/CreateRoomRequest.cs
rename to osu.Game/Online/Multiplayer/CreateRoomRequest.cs
index c848c55cc6..dcb4ed51ea 100644
--- a/osu.Game/Online/API/Requests/CreateRoomRequest.cs
+++ b/osu.Game/Online/Multiplayer/CreateRoomRequest.cs
@@ -4,10 +4,9 @@
using System.Net.Http;
using Newtonsoft.Json;
using osu.Framework.IO.Network;
-using osu.Game.Online.API.Requests.Responses;
-using osu.Game.Online.Multiplayer;
+using osu.Game.Online.API;
-namespace osu.Game.Online.API.Requests
+namespace osu.Game.Online.Multiplayer
{
public class CreateRoomRequest : APIRequest
{
diff --git a/osu.Game/Online/API/Requests/CreateRoomScoreRequest.cs b/osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs
similarity index 90%
rename from osu.Game/Online/API/Requests/CreateRoomScoreRequest.cs
rename to osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs
index e6246b4f1f..f973f96b37 100644
--- a/osu.Game/Online/API/Requests/CreateRoomScoreRequest.cs
+++ b/osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs
@@ -3,9 +3,9 @@
using System.Net.Http;
using osu.Framework.IO.Network;
-using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.API;
-namespace osu.Game.Online.API.Requests
+namespace osu.Game.Online.Multiplayer
{
public class CreateRoomScoreRequest : APIRequest
{
diff --git a/osu.Game/Online/API/Requests/GetRoomPlaylistScoresRequest.cs b/osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs
similarity index 85%
rename from osu.Game/Online/API/Requests/GetRoomPlaylistScoresRequest.cs
rename to osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs
index 38f852870b..3d3bd20ff3 100644
--- a/osu.Game/Online/API/Requests/GetRoomPlaylistScoresRequest.cs
+++ b/osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs
@@ -3,8 +3,9 @@
using System.Collections.Generic;
using Newtonsoft.Json;
+using osu.Game.Online.API;
-namespace osu.Game.Online.API.Requests
+namespace osu.Game.Online.Multiplayer
{
public class GetRoomPlaylistScoresRequest : APIRequest
{
@@ -23,6 +24,6 @@ namespace osu.Game.Online.API.Requests
public class RoomPlaylistScores
{
[JsonProperty("scores")]
- public List Scores { get; set; }
+ public List Scores { get; set; }
}
}
diff --git a/osu.Game/Online/API/Requests/GetRoomRequest.cs b/osu.Game/Online/Multiplayer/GetRoomRequest.cs
similarity index 84%
rename from osu.Game/Online/API/Requests/GetRoomRequest.cs
rename to osu.Game/Online/Multiplayer/GetRoomRequest.cs
index 531e1857de..2907b49f1d 100644
--- a/osu.Game/Online/API/Requests/GetRoomRequest.cs
+++ b/osu.Game/Online/Multiplayer/GetRoomRequest.cs
@@ -1,9 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Game.Online.Multiplayer;
+using osu.Game.Online.API;
-namespace osu.Game.Online.API.Requests
+namespace osu.Game.Online.Multiplayer
{
public class GetRoomRequest : APIRequest
{
diff --git a/osu.Game/Online/API/Requests/GetRoomScoresRequest.cs b/osu.Game/Online/Multiplayer/GetRoomScoresRequest.cs
similarity index 89%
rename from osu.Game/Online/API/Requests/GetRoomScoresRequest.cs
rename to osu.Game/Online/Multiplayer/GetRoomScoresRequest.cs
index eb53369d18..bc913030dd 100644
--- a/osu.Game/Online/API/Requests/GetRoomScoresRequest.cs
+++ b/osu.Game/Online/Multiplayer/GetRoomScoresRequest.cs
@@ -2,9 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
-namespace osu.Game.Online.API.Requests
+namespace osu.Game.Online.Multiplayer
{
public class GetRoomScoresRequest : APIRequest>
{
diff --git a/osu.Game/Online/API/Requests/GetRoomsRequest.cs b/osu.Game/Online/Multiplayer/GetRoomsRequest.cs
similarity index 94%
rename from osu.Game/Online/API/Requests/GetRoomsRequest.cs
rename to osu.Game/Online/Multiplayer/GetRoomsRequest.cs
index c47ed20909..64e0386f77 100644
--- a/osu.Game/Online/API/Requests/GetRoomsRequest.cs
+++ b/osu.Game/Online/Multiplayer/GetRoomsRequest.cs
@@ -4,10 +4,10 @@
using System.Collections.Generic;
using Humanizer;
using osu.Framework.IO.Network;
-using osu.Game.Online.Multiplayer;
+using osu.Game.Online.API;
using osu.Game.Screens.Multi.Lounge.Components;
-namespace osu.Game.Online.API.Requests
+namespace osu.Game.Online.Multiplayer
{
public class GetRoomsRequest : APIRequest>
{
diff --git a/osu.Game/Online/API/Requests/JoinRoomRequest.cs b/osu.Game/Online/Multiplayer/JoinRoomRequest.cs
similarity index 90%
rename from osu.Game/Online/API/Requests/JoinRoomRequest.cs
rename to osu.Game/Online/Multiplayer/JoinRoomRequest.cs
index b0808afa45..74375af856 100644
--- a/osu.Game/Online/API/Requests/JoinRoomRequest.cs
+++ b/osu.Game/Online/Multiplayer/JoinRoomRequest.cs
@@ -3,9 +3,9 @@
using System.Net.Http;
using osu.Framework.IO.Network;
-using osu.Game.Online.Multiplayer;
+using osu.Game.Online.API;
-namespace osu.Game.Online.API.Requests
+namespace osu.Game.Online.Multiplayer
{
public class JoinRoomRequest : APIRequest
{
diff --git a/osu.Game/Online/API/RoomScore.cs b/osu.Game/Online/Multiplayer/MultiplayerScore.cs
similarity index 95%
rename from osu.Game/Online/API/RoomScore.cs
rename to osu.Game/Online/Multiplayer/MultiplayerScore.cs
index 3c7f8c9833..3bbf19b11f 100644
--- a/osu.Game/Online/API/RoomScore.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerScore.cs
@@ -6,15 +6,15 @@ using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
-using osu.Game.Online.Multiplayer;
+using osu.Game.Online.API;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Users;
-namespace osu.Game.Online.API
+namespace osu.Game.Online.Multiplayer
{
- public class RoomScore
+ public class MultiplayerScore
{
[JsonProperty("id")]
public int ID { get; set; }
diff --git a/osu.Game/Online/API/Requests/PartRoomRequest.cs b/osu.Game/Online/Multiplayer/PartRoomRequest.cs
similarity index 90%
rename from osu.Game/Online/API/Requests/PartRoomRequest.cs
rename to osu.Game/Online/Multiplayer/PartRoomRequest.cs
index c988cd5c9e..54bb005d96 100644
--- a/osu.Game/Online/API/Requests/PartRoomRequest.cs
+++ b/osu.Game/Online/Multiplayer/PartRoomRequest.cs
@@ -3,9 +3,9 @@
using System.Net.Http;
using osu.Framework.IO.Network;
-using osu.Game.Online.Multiplayer;
+using osu.Game.Online.API;
-namespace osu.Game.Online.API.Requests
+namespace osu.Game.Online.Multiplayer
{
public class PartRoomRequest : APIRequest
{
diff --git a/osu.Game/Online/API/Requests/SubmitRoomScoreRequest.cs b/osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs
similarity index 90%
rename from osu.Game/Online/API/Requests/SubmitRoomScoreRequest.cs
rename to osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs
index 8eb2952159..d31aef2ea5 100644
--- a/osu.Game/Online/API/Requests/SubmitRoomScoreRequest.cs
+++ b/osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs
@@ -4,11 +4,12 @@
using System.Net.Http;
using Newtonsoft.Json;
using osu.Framework.IO.Network;
+using osu.Game.Online.API;
using osu.Game.Scoring;
-namespace osu.Game.Online.API.Requests
+namespace osu.Game.Online.Multiplayer
{
- public class SubmitRoomScoreRequest : APIRequest
+ public class SubmitRoomScoreRequest : APIRequest
{
private readonly int scoreId;
private readonly int roomId;
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index f4bb10340e..d6a07651e2 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -18,6 +18,7 @@ using osu.Game.Screens.Menu;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Humanizer;
using JetBrains.Annotations;
using osu.Framework.Audio;
using osu.Framework.Bindables;
@@ -759,7 +760,7 @@ namespace osu.Game
Schedule(() => notifications.Post(new SimpleNotification
{
Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb,
- Text = entry.Message + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty),
+ Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty),
}));
}
else if (recentLogCount == short_term_display_limit)
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index dd120937af..fe5c0704b7 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -199,6 +199,10 @@ namespace osu.Game
ScoreManager.Undelete(getBeatmapScores(item), true);
});
+ var difficultyManager = new BeatmapDifficultyManager();
+ dependencies.Cache(difficultyManager);
+ AddInternal(difficultyManager);
+
dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore));
dependencies.Cache(SettingsStore = new SettingsStore(contextFactory));
dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore));
diff --git a/osu.Game/Overlays/OverlayHeader.cs b/osu.Game/Overlays/OverlayHeader.cs
index dbc934bde9..cc7f798c4a 100644
--- a/osu.Game/Overlays/OverlayHeader.cs
+++ b/osu.Game/Overlays/OverlayHeader.cs
@@ -12,9 +12,26 @@ namespace osu.Game.Overlays
{
public abstract class OverlayHeader : Container
{
- public const int CONTENT_X_MARGIN = 50;
+ private float contentSidePadding;
+
+ ///
+ /// Horizontal padding of the header content.
+ ///
+ protected float ContentSidePadding
+ {
+ get => contentSidePadding;
+ set
+ {
+ contentSidePadding = value;
+ content.Padding = new MarginPadding
+ {
+ Horizontal = value
+ };
+ }
+ }
private readonly Box titleBackground;
+ private readonly Container content;
protected readonly FillFlowContainer HeaderInfo;
@@ -50,14 +67,10 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both,
Colour = Color4.Gray,
},
- new Container
+ content = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding
- {
- Horizontal = CONTENT_X_MARGIN,
- },
Children = new[]
{
CreateTitle().With(title =>
@@ -79,6 +92,8 @@ namespace osu.Game.Overlays
CreateContent()
}
});
+
+ ContentSidePadding = 50;
}
[BackgroundDependencyLoader]
diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs
index 0161d91daa..2e5f1071f2 100644
--- a/osu.Game/Overlays/Profile/ProfileHeader.cs
+++ b/osu.Game/Overlays/Profile/ProfileHeader.cs
@@ -23,6 +23,8 @@ namespace osu.Game.Overlays.Profile
public ProfileHeader()
{
+ ContentSidePadding = UserProfileOverlay.CONTENT_X_MARGIN;
+
User.ValueChanged += e => updateDisplay(e.NewValue);
TabControl.AddItem("info");
diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
index 93a02ea0e4..0149e6c3a6 100644
--- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Configuration;
@@ -78,6 +79,15 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
Bindable = config.GetBindable(OsuSetting.ScoreDisplayMode)
}
};
+
+ if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
+ {
+ Add(new SettingsCheckbox
+ {
+ LabelText = "Disable Windows key during gameplay",
+ Bindable = config.GetBindable(OsuSetting.GameplayDisableWinKey)
+ });
+ }
}
}
}
diff --git a/osu.Game/Overlays/TabControlOverlayHeader.cs b/osu.Game/Overlays/TabControlOverlayHeader.cs
index e8e000f441..61605d9e9e 100644
--- a/osu.Game/Overlays/TabControlOverlayHeader.cs
+++ b/osu.Game/Overlays/TabControlOverlayHeader.cs
@@ -22,6 +22,7 @@ namespace osu.Game.Overlays
protected OsuTabControl TabControl;
private readonly Box controlBackground;
+ private readonly Container tabControlContainer;
private readonly BindableWithCurrent current = new BindableWithCurrent();
public Bindable Current
@@ -30,6 +31,16 @@ namespace osu.Game.Overlays
set => current.Current = value;
}
+ protected new float ContentSidePadding
+ {
+ get => base.ContentSidePadding;
+ set
+ {
+ base.ContentSidePadding = value;
+ tabControlContainer.Padding = new MarginPadding { Horizontal = value };
+ }
+ }
+
protected TabControlOverlayHeader()
{
HeaderInfo.Add(new Container
@@ -42,11 +53,16 @@ namespace osu.Game.Overlays
{
RelativeSizeAxes = Axes.Both,
},
- TabControl = CreateTabControl().With(control =>
+ tabControlContainer = new Container
{
- control.Margin = new MarginPadding { Left = CONTENT_X_MARGIN };
- control.Current = Current;
- })
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding { Horizontal = ContentSidePadding },
+ Child = TabControl = CreateTabControl().With(control =>
+ {
+ control.Current = Current;
+ })
+ }
}
});
}
diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs
index 7fe606d584..65f1a972ed 100644
--- a/osu.Game/Rulesets/Mods/ModPerfect.cs
+++ b/osu.Game/Rulesets/Mods/ModPerfect.cs
@@ -17,6 +17,7 @@ namespace osu.Game.Rulesets.Mods
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
=> !(result.Judgement is IgnoreJudgement)
+ && result.Judgement.AffectsCombo
&& result.Type != result.Judgement.MaxResult;
}
}
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index b633cb0860..581617b567 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -126,12 +126,12 @@ namespace osu.Game.Rulesets.Objects.Drawables
if (Result == null)
throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}.");
- loadSamples();
+ LoadSamples();
}
- protected override void LoadComplete()
+ protected override void LoadAsyncComplete()
{
- base.LoadComplete();
+ base.LoadAsyncComplete();
HitObject.DefaultsApplied += onDefaultsApplied;
@@ -145,14 +145,19 @@ namespace osu.Game.Rulesets.Objects.Drawables
}
samplesBindable = HitObject.SamplesBindable.GetBoundCopy();
- samplesBindable.CollectionChanged += (_, __) => loadSamples();
+ samplesBindable.CollectionChanged += (_, __) => LoadSamples();
apply(HitObject);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
updateState(ArmedState.Idle, true);
}
- private void loadSamples()
+ protected virtual void LoadSamples()
{
if (Samples != null)
{
@@ -353,17 +358,32 @@ namespace osu.Game.Rulesets.Objects.Drawables
[Resolved(canBeNull: true)]
private GameplayClock gameplayClock { get; set; }
+ ///
+ /// Calculate the position to be used for sample playback at a specified X position (0..1).
+ ///
+ /// The lookup X position. Generally should be .
+ ///
+ protected double CalculateSamplePlaybackBalance(double position)
+ {
+ const float balance_adjust_amount = 0.4f;
+
+ return balance_adjust_amount * (userPositionalHitSounds.Value ? position - 0.5f : 0);
+ }
+
+ ///
+ /// Whether samples should currently be playing. Will be false during seek operations.
+ ///
+ protected bool ShouldPlaySamples => gameplayClock?.IsSeeking != true;
+
///
/// Plays all the hit sounds for this .
/// This is invoked automatically when this is hit.
///
public virtual void PlaySamples()
{
- const float balance_adjust_amount = 0.4f;
-
- if (Samples != null && gameplayClock?.IsSeeking != true)
+ if (Samples != null && ShouldPlaySamples)
{
- Samples.Balance.Value = balance_adjust_amount * (userPositionalHitSounds.Value ? SamplePlaybackPosition - 0.5f : 0);
+ Samples.Balance.Value = CalculateSamplePlaybackBalance(SamplePlaybackPosition);
Samples.Play();
}
}
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
index 0dc3324559..bf64175468 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
@@ -2,11 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Layout;
+using osu.Framework.Threading;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
@@ -17,7 +19,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
{
private readonly IBindable timeRange = new BindableDouble();
private readonly IBindable direction = new Bindable();
- private readonly Dictionary hitObjectInitialStateCache = new Dictionary();
+ private readonly Dictionary hitObjectInitialStateCache = new Dictionary();
[Resolved]
private IScrollingInfo scrollingInfo { get; set; }
@@ -175,10 +177,10 @@ namespace osu.Game.Rulesets.UI.Scrolling
{
// The cache may not exist if the hitobject state hasn't been computed yet (e.g. if the hitobject was added + defaults applied in the same frame).
// In such a case, combinedObjCache will take care of updating the hitobject.
- if (hitObjectInitialStateCache.TryGetValue(drawableObject, out var objCache))
+ if (hitObjectInitialStateCache.TryGetValue(drawableObject, out var state))
{
combinedObjCache.Invalidate();
- objCache.Invalidate();
+ state.Cache.Invalidate();
}
}
@@ -190,8 +192,8 @@ namespace osu.Game.Rulesets.UI.Scrolling
if (!layoutCache.IsValid)
{
- foreach (var cached in hitObjectInitialStateCache.Values)
- cached.Invalidate();
+ foreach (var state in hitObjectInitialStateCache.Values)
+ state.Cache.Invalidate();
combinedObjCache.Invalidate();
scrollingInfo.Algorithm.Reset();
@@ -215,16 +217,18 @@ namespace osu.Game.Rulesets.UI.Scrolling
foreach (var obj in Objects)
{
- if (!hitObjectInitialStateCache.TryGetValue(obj, out var objCache))
- objCache = hitObjectInitialStateCache[obj] = new Cached();
+ if (!hitObjectInitialStateCache.TryGetValue(obj, out var state))
+ state = hitObjectInitialStateCache[obj] = new InitialState(new Cached());
- if (objCache.IsValid)
+ if (state.Cache.IsValid)
continue;
- computeLifetimeStartRecursive(obj);
- computeInitialStateRecursive(obj);
+ state.ScheduledComputation?.Cancel();
+ state.ScheduledComputation = computeInitialStateRecursive(obj);
- objCache.Validate();
+ computeLifetimeStartRecursive(obj);
+
+ state.Cache.Validate();
}
combinedObjCache.Validate();
@@ -267,8 +271,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
return scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength);
}
- // Cant use AddOnce() since the delegate is re-constructed every invocation
- private void computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() =>
+ private ScheduledDelegate computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() =>
{
if (hitObject.HitObject is IHasDuration e)
{
@@ -325,5 +328,19 @@ namespace osu.Game.Rulesets.UI.Scrolling
break;
}
}
+
+ private class InitialState
+ {
+ [NotNull]
+ public readonly Cached Cache;
+
+ [CanBeNull]
+ public ScheduledDelegate ScheduledComputation;
+
+ public InitialState(Cached cache)
+ {
+ Cache = cache;
+ }
+ }
}
}
diff --git a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs
index 571bbde716..1afbf5c32a 100644
--- a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs
+++ b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs
@@ -6,7 +6,6 @@ using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.API;
-using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Leaderboards;
using osu.Game.Online.Multiplayer;
diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs
index cf0197d26b..c2381fe219 100644
--- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs
+++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs
@@ -10,7 +10,6 @@ using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Online.API;
-using osu.Game.Online.API.Requests;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Scoring;
diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs
index 5cafc974f1..f367d44347 100644
--- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs
+++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs
@@ -9,7 +9,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
-using osu.Game.Online.API.Requests;
using osu.Game.Online.Multiplayer;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
diff --git a/osu.Game/Screens/Multi/RoomManager.cs b/osu.Game/Screens/Multi/RoomManager.cs
index 491be2e946..2a96fa536d 100644
--- a/osu.Game/Screens/Multi/RoomManager.cs
+++ b/osu.Game/Screens/Multi/RoomManager.cs
@@ -14,7 +14,6 @@ using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Online;
using osu.Game.Online.API;
-using osu.Game.Online.API.Requests;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Screens.Multi.Lounge.Components;
diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs
index 9633f5c533..5da432d5b2 100644
--- a/osu.Game/Screens/Ranking/ScorePanel.cs
+++ b/osu.Game/Screens/Ranking/ScorePanel.cs
@@ -13,6 +13,7 @@ using osu.Framework.Input.Events;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Contracted;
using osu.Game.Screens.Ranking.Expanded;
+using osu.Game.Users;
using osuTK;
using osuTK.Graphics;
@@ -142,7 +143,16 @@ namespace osu.Game.Screens.Ranking
CornerRadius = 20,
CornerExponent = 2.5f,
Masking = true,
- Child = middleLayerBackground = new Box { RelativeSizeAxes = Axes.Both }
+ Children = new[]
+ {
+ middleLayerBackground = new Box { RelativeSizeAxes = Axes.Both },
+ new UserCoverBackground
+ {
+ RelativeSizeAxes = Axes.Both,
+ User = Score.User,
+ Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0.5f), Color4Extensions.FromHex("#444").Opacity(0))
+ },
+ }
},
middleLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both }
}
@@ -155,18 +165,10 @@ namespace osu.Game.Screens.Ranking
{
base.LoadComplete();
- if (state == PanelState.Expanded)
- {
- topLayerBackground.FadeColour(expanded_top_layer_colour);
- middleLayerBackground.FadeColour(expanded_middle_layer_colour);
- }
- else
- {
- topLayerBackground.FadeColour(contracted_top_layer_colour);
- middleLayerBackground.FadeColour(contracted_middle_layer_colour);
- }
-
updateState();
+
+ topLayerBackground.FinishTransforms(false, nameof(Colour));
+ middleLayerBackground.FinishTransforms(false, nameof(Colour));
}
private PanelState state = PanelState.Contracted;
diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs
index 3e4798a812..c559b4f8f5 100644
--- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs
+++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs
@@ -3,7 +3,9 @@
using System;
using System.Collections.Generic;
+using System.Threading;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
@@ -41,6 +43,12 @@ namespace osu.Game.Screens.Select.Carousel
[Resolved(CanBeNull = true)]
private BeatmapSetOverlay beatmapOverlay { get; set; }
+ [Resolved]
+ private BeatmapDifficultyManager difficultyManager { get; set; }
+
+ private IBindable starDifficultyBindable;
+ private CancellationTokenSource starDifficultyCancellationSource;
+
public DrawableCarouselBeatmap(CarouselBeatmap panel)
: base(panel)
{
@@ -137,7 +145,6 @@ namespace osu.Game.Screens.Select.Carousel
},
starCounter = new StarCounter
{
- Current = (float)beatmap.StarDifficulty,
Scale = new Vector2(0.8f),
}
}
@@ -181,6 +188,16 @@ namespace osu.Game.Screens.Select.Carousel
if (Item.State.Value != CarouselItemState.Collapsed && Alpha == 0)
starCounter.ReplayAnimation();
+ starDifficultyCancellationSource?.Cancel();
+
+ // Only compute difficulty when the item is visible.
+ if (Item.State.Value != CarouselItemState.Collapsed)
+ {
+ // We've potentially cancelled the computation above so a new bindable is required.
+ starDifficultyBindable = difficultyManager.GetBindableDifficulty(beatmap, (starDifficultyCancellationSource = new CancellationTokenSource()).Token);
+ starDifficultyBindable.BindValueChanged(d => starCounter.Current = (float)d.NewValue.Stars, true);
+ }
+
base.ApplyState();
}
@@ -205,5 +222,11 @@ namespace osu.Game.Screens.Select.Carousel
return items.ToArray();
}
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ starDifficultyCancellationSource?.Cancel();
+ }
}
}
diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs
index 02822ea608..44c328187f 100644
--- a/osu.Game/Screens/Select/Details/AdvancedStats.cs
+++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs
@@ -14,10 +14,12 @@ using osu.Framework.Bindables;
using System.Collections.Generic;
using osu.Game.Rulesets.Mods;
using System.Linq;
+using System.Threading;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
+using osu.Game.Rulesets;
namespace osu.Game.Screens.Select.Details
{
@@ -26,6 +28,12 @@ namespace osu.Game.Screens.Select.Details
[Resolved]
private IBindable> mods { get; set; }
+ [Resolved]
+ private IBindable ruleset { get; set; }
+
+ [Resolved]
+ private BeatmapDifficultyManager difficultyManager { get; set; }
+
protected readonly StatisticRow FirstValue, HpDrain, Accuracy, ApproachRate;
private readonly StatisticRow starDifficulty;
@@ -71,6 +79,7 @@ namespace osu.Game.Screens.Select.Details
{
base.LoadComplete();
+ ruleset.BindValueChanged(_ => updateStatistics());
mods.BindValueChanged(modsChanged, true);
}
@@ -132,11 +141,39 @@ namespace osu.Game.Screens.Select.Details
break;
}
- starDifficulty.Value = ((float)(Beatmap?.StarDifficulty ?? 0), null);
-
HpDrain.Value = (baseDifficulty?.DrainRate ?? 0, adjustedDifficulty?.DrainRate);
Accuracy.Value = (baseDifficulty?.OverallDifficulty ?? 0, adjustedDifficulty?.OverallDifficulty);
ApproachRate.Value = (baseDifficulty?.ApproachRate ?? 0, adjustedDifficulty?.ApproachRate);
+
+ updateStarDifficulty();
+ }
+
+ private IBindable normalStarDifficulty;
+ private IBindable moddedStarDifficulty;
+ private CancellationTokenSource starDifficultyCancellationSource;
+
+ private void updateStarDifficulty()
+ {
+ starDifficultyCancellationSource?.Cancel();
+
+ if (Beatmap == null)
+ return;
+
+ starDifficultyCancellationSource = new CancellationTokenSource();
+
+ normalStarDifficulty = difficultyManager.GetBindableDifficulty(Beatmap, ruleset.Value, null, starDifficultyCancellationSource.Token);
+ moddedStarDifficulty = difficultyManager.GetBindableDifficulty(Beatmap, ruleset.Value, mods.Value, starDifficultyCancellationSource.Token);
+
+ normalStarDifficulty.BindValueChanged(_ => updateDisplay());
+ moddedStarDifficulty.BindValueChanged(_ => updateDisplay(), true);
+
+ void updateDisplay() => starDifficulty.Value = ((float)normalStarDifficulty.Value.Stars, (float)moddedStarDifficulty.Value.Stars);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ starDifficultyCancellationSource?.Cancel();
}
public class StatisticRow : Container, IHasAccentColour
diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs
index 49f9f01cff..fb9cab74c8 100644
--- a/osu.Game/Skinning/SkinnableSound.cs
+++ b/osu.Game/Skinning/SkinnableSound.cs
@@ -22,6 +22,9 @@ namespace osu.Game.Skinning
[Resolved]
private ISampleStore samples { get; set; }
+ public override bool RemoveWhenNotAlive => false;
+ public override bool RemoveCompletedTransforms => false;
+
public SkinnableSound(ISampleInfo hitSamples)
: this(new[] { hitSamples })
{
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index e8c333b6b1..5af28ae11a 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -24,7 +24,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 8d1b837995..4a94ec33d8 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -80,7 +80,7 @@
-
+