diff --git a/global.json b/global.json
index 9aa5b6192b..233a040d18 100644
--- a/global.json
+++ b/global.json
@@ -5,6 +5,6 @@
"version": "3.1.100"
},
"msbuild-sdks": {
- "Microsoft.Build.Traversal": "2.0.50"
+ "Microsoft.Build.Traversal": "2.0.52"
}
}
\ No newline at end of file
diff --git a/osu.Android.props b/osu.Android.props
index 1d1583c55a..7e6f1469f5 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
-
+
+
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.Tests/TestSceneCatchModHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs
new file mode 100644
index 0000000000..f15da29993
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs
@@ -0,0 +1,56 @@
+// 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.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Catch.Mods;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public class TestSceneCatchModHidden : ModTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ LocalConfig.Set(OsuSetting.IncreaseFirstObjectVisibility, false);
+ }
+
+ [Test]
+ public void TestJuiceStream()
+ {
+ CreateModTest(new ModTestData
+ {
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new JuiceStream
+ {
+ StartTime = 1000,
+ Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(0, -192) }),
+ X = CatchPlayfield.WIDTH / 2
+ }
+ }
+ },
+ Mod = new CatchModHidden(),
+ PassCondition = () => Player.Results.Count > 0
+ && Player.ChildrenOfType().Single().Alpha > 0
+ && Player.ChildrenOfType().Last().Alpha > 0
+ });
+ }
+
+ protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
+ }
+}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
index fbb22a8498..b4f123598b 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Catch.Objects;
@@ -25,6 +26,11 @@ namespace osu.Game.Rulesets.Catch.Tests
{
private RulesetInfo catchRuleset;
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
+ private Catcher catcher => this.ChildrenOfType().First().MovableCatcher;
+
public TestSceneCatcherArea()
{
AddSliderStep("CircleSize", 0, 8, 5, createCatcher);
@@ -34,24 +40,43 @@ namespace osu.Game.Rulesets.Catch.Tests
AddRepeatStep("catch fruit", () => catchFruit(new TestFruit(false)
{
- X = this.ChildrenOfType().First().MovableCatcher.X
+ X = catcher.X
}), 20);
AddRepeatStep("catch fruit last in combo", () => catchFruit(new TestFruit(false)
{
- X = this.ChildrenOfType().First().MovableCatcher.X,
+ X = catcher.X,
LastInCombo = true,
}), 20);
AddRepeatStep("catch kiai fruit", () => catchFruit(new TestFruit(true)
{
- X = this.ChildrenOfType().First().MovableCatcher.X,
+ X = catcher.X
}), 20);
AddRepeatStep("miss fruit", () => catchFruit(new Fruit
{
- X = this.ChildrenOfType().First().MovableCatcher.X + 100,
+ X = catcher.X + 100,
LastInCombo = true,
}, true), 20);
}
+ [TestCase(true)]
+ [TestCase(false)]
+ public void TestHitLighting(bool enable)
+ {
+ AddStep("create catcher", () => createCatcher(5));
+
+ AddStep("toggle hit lighting", () => config.Set(OsuSetting.HitLighting, enable));
+ AddStep("catch fruit", () => catchFruit(new TestFruit(false)
+ {
+ X = catcher.X
+ }));
+ AddStep("catch fruit last in combo", () => catchFruit(new TestFruit(false)
+ {
+ X = catcher.X,
+ LastInCombo = true
+ }));
+ AddAssert("check hit explosion", () => catcher.ChildrenOfType().Any() == enable);
+ }
+
private void catchFruit(Fruit fruit, bool miss = false)
{
this.ChildrenOfType().ForEach(area =>
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.Catch/Replays/CatchFramedReplayInputHandler.cs b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs
index f122588a2b..99d899db80 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs
@@ -35,18 +35,15 @@ namespace osu.Game.Rulesets.Catch.Replays
}
}
- public override List GetPendingInputs()
+ public override void CollectPendingInputs(List inputs)
{
- if (!Position.HasValue) return new List();
+ if (!Position.HasValue) return;
- return new List
+ inputs.Add(new CatchReplayState
{
- new CatchReplayState
- {
- PressedActions = CurrentFrame?.Actions ?? new List(),
- CatcherX = Position.Value
- },
- };
+ PressedActions = CurrentFrame?.Actions ?? new List(),
+ CatcherX = Position.Value
+ });
}
public class CatchReplayState : ReplayState
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
index d034f3c7d4..154e1576db 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
@@ -35,22 +35,25 @@ namespace osu.Game.Rulesets.Catch.UI
public CatchPlayfield(BeatmapDifficulty difficulty, Func> createDrawableRepresentation)
{
- Container explodingFruitContainer;
-
- InternalChildren = new Drawable[]
+ var explodingFruitContainer = new Container
{
- explodingFruitContainer = new Container
- {
- RelativeSizeAxes = Axes.Both,
- },
- CatcherArea = new CatcherArea(difficulty)
- {
- CreateDrawableRepresentation = createDrawableRepresentation,
- ExplodingFruitTarget = explodingFruitContainer,
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.TopLeft,
- },
- HitObjectContainer
+ RelativeSizeAxes = Axes.Both,
+ };
+
+ CatcherArea = new CatcherArea(difficulty)
+ {
+ CreateDrawableRepresentation = createDrawableRepresentation,
+ ExplodingFruitTarget = explodingFruitContainer,
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.TopLeft,
+ };
+
+ InternalChildren = new[]
+ {
+ explodingFruitContainer,
+ CatcherArea.MovableCatcher.CreateProxiedContent(),
+ HitObjectContainer,
+ CatcherArea
};
}
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index 82cbbefcca..8820dff730 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -5,12 +5,14 @@ using System;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.Skinning;
@@ -46,6 +48,12 @@ namespace osu.Game.Rulesets.Catch.UI
public Container ExplodingFruitTarget;
+ private Container caughtFruitContainer { get; } = new Container
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.BottomCentre,
+ };
+
[NotNull]
private readonly Container trailsTarget;
@@ -83,8 +91,6 @@ namespace osu.Game.Rulesets.Catch.UI
///
private readonly float catchWidth;
- private Container caughtFruit;
-
private CatcherSprite catcherIdle;
private CatcherSprite catcherKiai;
private CatcherSprite catcherFail;
@@ -99,6 +105,7 @@ namespace osu.Game.Rulesets.Catch.UI
private double hyperDashModifier = 1;
private int hyperDashDirection;
private float hyperDashTargetPosition;
+ private Bindable hitLighting;
public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null)
{
@@ -114,15 +121,13 @@ namespace osu.Game.Rulesets.Catch.UI
}
[BackgroundDependencyLoader]
- private void load()
+ private void load(OsuConfigManager config)
{
+ hitLighting = config.GetBindable(OsuSetting.HitLighting);
+
InternalChildren = new Drawable[]
{
- caughtFruit = new Container
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.BottomCentre,
- },
+ caughtFruitContainer,
catcherIdle = new CatcherSprite(CatcherAnimationState.Idle)
{
Anchor = Anchor.TopCentre,
@@ -145,6 +150,11 @@ namespace osu.Game.Rulesets.Catch.UI
updateCatcher();
}
+ ///
+ /// Creates proxied content to be displayed beneath hitobjects.
+ ///
+ public Drawable CreateProxiedContent() => caughtFruitContainer.CreateProxy();
+
///
/// Calculates the scale of the catcher based off the provided beatmap difficulty.
///
@@ -176,7 +186,7 @@ namespace osu.Game.Rulesets.Catch.UI
const float allowance = 10;
- while (caughtFruit.Any(f =>
+ while (caughtFruitContainer.Any(f =>
f.LifetimeEnd == double.MaxValue &&
Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2)))
{
@@ -187,13 +197,16 @@ namespace osu.Game.Rulesets.Catch.UI
fruit.X = Math.Clamp(fruit.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2);
- caughtFruit.Add(fruit);
+ caughtFruitContainer.Add(fruit);
- AddInternal(new HitExplosion(fruit)
+ if (hitLighting.Value)
{
- X = fruit.X,
- Scale = new Vector2(fruit.HitObject.Scale)
- });
+ AddInternal(new HitExplosion(fruit)
+ {
+ X = fruit.X,
+ Scale = new Vector2(fruit.HitObject.Scale)
+ });
+ }
}
///
@@ -342,7 +355,7 @@ namespace osu.Game.Rulesets.Catch.UI
///
public void Drop()
{
- foreach (var f in caughtFruit.ToArray())
+ foreach (var f in caughtFruitContainer.ToArray())
Drop(f);
}
@@ -351,7 +364,7 @@ namespace osu.Game.Rulesets.Catch.UI
///
public void Explode()
{
- foreach (var f in caughtFruit.ToArray())
+ foreach (var f in caughtFruitContainer.ToArray())
Explode(f);
}
@@ -450,9 +463,9 @@ namespace osu.Game.Rulesets.Catch.UI
if (ExplodingFruitTarget != null)
{
fruit.Anchor = Anchor.TopLeft;
- fruit.Position = caughtFruit.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget);
+ fruit.Position = caughtFruitContainer.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget);
- if (!caughtFruit.Remove(fruit))
+ if (!caughtFruitContainer.Remove(fruit))
// we may have already been removed by a previous operation (due to the weird OnLoadComplete scheduling).
// this avoids a crash on potentially attempting to Add a fruit to ExplodingFruitTarget twice.
return;
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index bf1ac5bc0e..4255c3b1af 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Catch.UI
public Func> CreateDrawableRepresentation;
+ public readonly Catcher MovableCatcher;
+
public Container ExplodingFruitTarget
{
set => MovableCatcher.ExplodingFruitTarget = value;
@@ -104,7 +106,5 @@ namespace osu.Game.Rulesets.Catch.UI
if (state?.CatcherX != null)
MovableCatcher.X = state.CatcherX.Value;
}
-
- protected internal readonly Catcher MovableCatcher;
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
index 0d13b85901..95072cf4f8 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
@@ -2,6 +2,7 @@
// 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.Screens;
using osu.Game.Beatmaps;
@@ -10,6 +11,8 @@ using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
+using osu.Game.Rulesets.Mania.Scoring;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
@@ -236,6 +239,53 @@ namespace osu.Game.Rulesets.Mania.Tests
assertTailJudgement(HitResult.Meh);
}
+ [Test]
+ public void TestMissReleaseAndHitSecondRelease()
+ {
+ var windows = new ManiaHitWindows();
+ windows.SetDifficulty(10);
+
+ var beatmap = new Beatmap
+ {
+ HitObjects =
+ {
+ new HoldNote
+ {
+ StartTime = 1000,
+ Duration = 500,
+ Column = 0,
+ },
+ new HoldNote
+ {
+ StartTime = 1000 + 500 + windows.WindowFor(HitResult.Miss) + 10,
+ Duration = 500,
+ Column = 0,
+ },
+ },
+ BeatmapInfo =
+ {
+ BaseDifficulty = new BeatmapDifficulty
+ {
+ SliderTickRate = 4,
+ OverallDifficulty = 10,
+ },
+ Ruleset = new ManiaRuleset().RulesetInfo
+ },
+ };
+
+ performTest(new List
+ {
+ new ManiaReplayFrame(beatmap.HitObjects[1].StartTime, ManiaAction.Key1),
+ new ManiaReplayFrame(beatmap.HitObjects[1].GetEndTime()),
+ }, beatmap);
+
+ AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject))
+ .All(j => j.Type == HitResult.Miss));
+
+ AddAssert("second hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject))
+ .All(j => j.Type == HitResult.Perfect));
+ }
+
private void assertHeadJudgement(HitResult result)
=> AddAssert($"head judged as {result}", () => judgementResults[0].Type == result);
@@ -250,11 +300,11 @@ namespace osu.Game.Rulesets.Mania.Tests
private ScoreAccessibleReplayPlayer currentPlayer;
- private void performTest(List frames)
+ private void performTest(List frames, Beatmap beatmap = null)
{
- AddStep("load player", () =>
+ if (beatmap == null)
{
- Beatmap.Value = CreateWorkingBeatmap(new Beatmap
+ beatmap = new Beatmap
{
HitObjects =
{
@@ -270,9 +320,14 @@ namespace osu.Game.Rulesets.Mania.Tests
BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 4 },
Ruleset = new ManiaRuleset().RulesetInfo
},
- });
+ };
- Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
+ beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
+ }
+
+ AddStep("load player", () =>
+ {
+ Beatmap.Value = CreateWorkingBeatmap(beatmap);
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
diff --git a/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs b/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs
new file mode 100644
index 0000000000..8698ba3abd
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs
@@ -0,0 +1,56 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Tests.Visual;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ public class TestScenePlayfieldCoveringContainer : OsuTestScene
+ {
+ private readonly ScrollingTestContainer scrollingContainer;
+ private readonly PlayfieldCoveringWrapper cover;
+
+ public TestScenePlayfieldCoveringContainer()
+ {
+ Child = scrollingContainer = new ScrollingTestContainer(ScrollingDirection.Down)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(300, 500),
+ Child = cover = new PlayfieldCoveringWrapper(new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Orange
+ })
+ {
+ RelativeSizeAxes = Axes.Both,
+ }
+ };
+ }
+
+ [Test]
+ public void TestScrollingDownwards()
+ {
+ AddStep("set down scroll", () => scrollingContainer.Direction = ScrollingDirection.Down);
+ AddStep("set coverage = 0.5", () => cover.Coverage = 0.5f);
+ AddStep("set coverage = 0.8f", () => cover.Coverage = 0.8f);
+ AddStep("set coverage = 0.2f", () => cover.Coverage = 0.2f);
+ }
+
+ [Test]
+ public void TestScrollingUpwards()
+ {
+ AddStep("set up scroll", () => scrollingContainer.Direction = ScrollingDirection.Up);
+ AddStep("set coverage = 0.5", () => cover.Coverage = 0.5f);
+ AddStep("set coverage = 0.8f", () => cover.Coverage = 0.8f);
+ AddStep("set coverage = 0.2f", () => cover.Coverage = 0.2f);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs
index 53db676a54..53967ffa05 100644
--- a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs
+++ b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Judgements
return 300;
case HitResult.Perfect:
- return 320;
+ return 350;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
index 4c125ad6ef..cbdcd49c5b 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
@@ -1,23 +1,19 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
-using osu.Game.Rulesets.Mania.Objects;
-using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Mods
{
- public class ManiaModFadeIn : Mod
+ public class ManiaModFadeIn : ManiaModHidden
{
public override string Name => "Fade In";
public override string Acronym => "FI";
public override IconUsage? Icon => OsuIcon.ModHidden;
- public override ModType Type => ModType.DifficultyIncrease;
public override string Description => @"Keys appear out of nowhere!";
- public override double ScoreMultiplier => 1;
- public override bool Ranked => true;
- public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) };
+
+ protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AlongScroll;
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs
index 66b90984b4..4bdb15526f 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs
@@ -2,15 +2,44 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania.Mods
{
- public class ManiaModHidden : ModHidden
+ public class ManiaModHidden : ModHidden, IApplicableToDrawableRuleset
{
public override string Description => @"Keys fade out before you hit them!";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) };
+
+ ///
+ /// The direction in which the cover should expand.
+ ///
+ protected virtual CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll;
+
+ public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
+ {
+ ManiaPlayfield maniaPlayfield = (ManiaPlayfield)drawableRuleset.Playfield;
+
+ foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns))
+ {
+ HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer;
+ Container hocParent = (Container)hoc.Parent;
+
+ hocParent.Remove(hoc);
+ hocParent.Add(new PlayfieldCoveringWrapper(hoc).With(c =>
+ {
+ c.RelativeSizeAxes = Axes.Both;
+ c.Direction = ExpandDirection;
+ c.Coverage = 0.5f;
+ }));
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 2262bd2b7d..0c5289efe1 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -167,6 +167,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (action != Action.Value)
return false;
+ // The tail has a lenience applied to it which is factored into the miss window (i.e. the miss judgement will be delayed).
+ // But the hold cannot ever be started within the late-lenience window, so we should skip trying to begin the hold during that time.
+ // Note: Unlike below, we use the tail's start time to determine the time offset.
+ if (Time.Current > Tail.HitObject.StartTime && !Tail.HitObject.HitWindows.CanBeHit(Time.Current - Tail.HitObject.StartTime))
+ return false;
+
beginHoldAt(Time.Current - Head.HitObject.StartTime);
Head.UpdateResult();
diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs b/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs
index 899718b77e..aa0c148caf 100644
--- a/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs
+++ b/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs
@@ -18,6 +18,9 @@ namespace osu.Game.Rulesets.Mania.Replays
protected override bool IsImportant(ManiaReplayFrame frame) => frame.Actions.Any();
- public override List GetPendingInputs() => new List { new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() } };
+ public override void CollectPendingInputs(List inputs)
+ {
+ inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() });
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
index 9b54b48de3..4b2f643333 100644
--- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
+++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
@@ -7,6 +7,10 @@ namespace osu.Game.Rulesets.Mania.Scoring
{
internal class ManiaScoreProcessor : ScoreProcessor
{
+ protected override double DefaultAccuracyPortion => 0.95;
+
+ protected override double DefaultComboPortion => 0.05;
+
public override HitWindows CreateHitWindows() => new ManiaHitWindows();
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs
index a749f80855..9f716428c0 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs
@@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
@@ -31,7 +32,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
string imageName = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value
?? $"mania-note{FallbackColumnIndex}L";
- sprite = skin.GetAnimation(imageName, true, true).With(d =>
+ sprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d =>
{
if (d == null)
return;
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs
index 515c941d65..283b04373b 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs
@@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.UI.Scrolling;
@@ -92,7 +93,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
string noteImage = GetColumnSkinConfig(skin, lookup)?.Value
?? $"mania-note{FallbackColumnIndex}{suffix}";
- return skin.GetTexture(noteImage);
+ return skin.GetTexture(noteImage, WrapMode.ClampToEdge, WrapMode.ClampToEdge);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index 511d6c8623..642353bd0b 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -33,11 +33,11 @@ namespace osu.Game.Rulesets.Mania.UI
public readonly Bindable Action = new Bindable();
- private readonly ColumnHitObjectArea hitObjectArea;
+ public readonly ColumnHitObjectArea HitObjectArea;
internal readonly Container TopLevelContainer;
- public Container UnderlayElements => hitObjectArea.UnderlayElements;
+ public Container UnderlayElements => HitObjectArea.UnderlayElements;
public Column(int index)
{
@@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.UI
{
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements
background.CreateProxy(),
- hitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both },
+ HitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both },
new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, Index), _ => new DefaultKeyArea())
{
RelativeSizeAxes = Axes.Both
@@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Mania.UI
TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
};
- TopLevelContainer.Add(hitObjectArea.Explosions.CreateProxy());
+ TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
}
public override Axes RelativeSizeAxes => Axes.Y;
@@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Both
};
- hitObjectArea.Explosions.Add(explosion);
+ HitObjectArea.Explosions.Add(explosion);
explosion.Delay(200).Expire(true);
}
diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs
index ba5281a1a2..8f7880dafa 100644
--- a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs
+++ b/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs
@@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
@@ -14,12 +15,14 @@ namespace osu.Game.Rulesets.Mania.UI.Components
public class HitObjectArea : SkinReloadableDrawable
{
protected readonly IBindable Direction = new Bindable();
+ public readonly HitObjectContainer HitObjectContainer;
public HitObjectArea(HitObjectContainer hitObjectContainer)
{
- InternalChildren = new[]
+ InternalChild = new Container
{
- hitObjectContainer,
+ RelativeSizeAxes = Axes.Both,
+ Child = HitObjectContainer = hitObjectContainer
};
}
diff --git a/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs
new file mode 100644
index 0000000000..15d216e8c5
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs
@@ -0,0 +1,133 @@
+// 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.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.UI
+{
+ ///
+ /// A that has its contents partially hidden by an adjustable "cover". This is intended to be used in a playfield.
+ ///
+ public class PlayfieldCoveringWrapper : CompositeDrawable
+ {
+ ///
+ /// The complete cover, including gradient and fill.
+ ///
+ private readonly Drawable cover;
+
+ ///
+ /// The gradient portion of the cover.
+ ///
+ private readonly Box gradient;
+
+ ///
+ /// The fully-opaque portion of the cover.
+ ///
+ private readonly Box filled;
+
+ private readonly IBindable scrollDirection = new Bindable();
+
+ public PlayfieldCoveringWrapper(Drawable content)
+ {
+ InternalChild = new BufferedContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new[]
+ {
+ content,
+ cover = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Blending = new BlendingParameters
+ {
+ // Don't change the destination colour.
+ RGBEquation = BlendingEquation.Add,
+ Source = BlendingType.Zero,
+ Destination = BlendingType.One,
+ // Subtract the cover's alpha from the destination (points with alpha 1 should make the destination completely transparent).
+ AlphaEquation = BlendingEquation.Add,
+ SourceAlpha = BlendingType.Zero,
+ DestinationAlpha = BlendingType.OneMinusSrcAlpha
+ },
+ Children = new Drawable[]
+ {
+ gradient = new Box
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ RelativeSizeAxes = Axes.Both,
+ RelativePositionAxes = Axes.Both,
+ Height = 0.25f,
+ Colour = ColourInfo.GradientVertical(
+ Color4.White.Opacity(0f),
+ Color4.White.Opacity(1f)
+ )
+ },
+ filled = new Box
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ RelativeSizeAxes = Axes.Both,
+ Height = 0
+ }
+ }
+ }
+ }
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(IScrollingInfo scrollingInfo)
+ {
+ scrollDirection.BindTo(scrollingInfo.Direction);
+ scrollDirection.BindValueChanged(onScrollDirectionChanged, true);
+ }
+
+ private void onScrollDirectionChanged(ValueChangedEvent direction)
+ => cover.Rotation = direction.NewValue == ScrollingDirection.Up ? 0 : 180f;
+
+ ///
+ /// The relative area that should be completely covered. This does not include the fade.
+ ///
+ public float Coverage
+ {
+ set
+ {
+ filled.Height = value;
+ gradient.Y = -value;
+ }
+ }
+
+ ///
+ /// The direction in which the cover expands.
+ ///
+ public CoverExpandDirection Direction
+ {
+ set => cover.Scale = value == CoverExpandDirection.AlongScroll ? Vector2.One : new Vector2(1, -1);
+ }
+ }
+
+ public enum CoverExpandDirection
+ {
+ ///
+ /// The cover expands along the scrolling direction.
+ ///
+ AlongScroll,
+
+ ///
+ /// The cover expands against the scrolling direction.
+ ///
+ AgainstScroll
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
index 46769f65fe..dde02e873b 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
@@ -8,6 +8,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing.Input;
using osu.Game.Audio;
@@ -79,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException();
- public Texture GetTexture(string componentName)
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{
switch (componentName)
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
index f08f994b07..646f12f710 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs
@@ -4,62 +4,109 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
+using osu.Framework.Testing;
+using osu.Game.Configuration;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneDrawableJudgement : OsuSkinnableTestScene
{
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
+ private readonly List> pools;
+
public TestSceneDrawableJudgement()
{
- var pools = new List>();
+ pools = new List>();
foreach (HitResult result in Enum.GetValues(typeof(HitResult)).OfType().Skip(1))
+ showResult(result);
+ }
+
+ [Test]
+ public void TestHitLightingDisabled()
+ {
+ AddStep("hit lighting disabled", () => config.Set(OsuSetting.HitLighting, false));
+
+ showResult(HitResult.Great);
+
+ AddUntilStep("judgements shown", () => this.ChildrenOfType().Any());
+ AddAssert("judgement body immediately visible",
+ () => this.ChildrenOfType().All(judgement => judgement.JudgementBody.Alpha == 1));
+ AddAssert("hit lighting hidden",
+ () => this.ChildrenOfType().All(judgement => judgement.Lighting.Alpha == 0));
+ }
+
+ [Test]
+ public void TestHitLightingEnabled()
+ {
+ AddStep("hit lighting enabled", () => config.Set(OsuSetting.HitLighting, true));
+
+ showResult(HitResult.Great);
+
+ AddUntilStep("judgements shown", () => this.ChildrenOfType().Any());
+ AddAssert("judgement body not immediately visible",
+ () => this.ChildrenOfType().All(judgement => judgement.JudgementBody.Alpha > 0 && judgement.JudgementBody.Alpha < 1));
+ AddAssert("hit lighting shown",
+ () => this.ChildrenOfType().All(judgement => judgement.Lighting.Alpha > 0));
+ }
+
+ private void showResult(HitResult result)
+ {
+ AddStep("Show " + result.GetDescription(), () =>
{
- AddStep("Show " + result.GetDescription(), () =>
+ int poolIndex = 0;
+
+ SetContents(() =>
{
- int poolIndex = 0;
+ DrawablePool pool;
- SetContents(() =>
+ if (poolIndex >= pools.Count)
+ pools.Add(pool = new DrawablePool(1));
+ else
{
- DrawablePool pool;
+ pool = pools[poolIndex];
- if (poolIndex >= pools.Count)
- pools.Add(pool = new DrawablePool(1));
- else
+ // We need to make sure neither the pool nor the judgement get disposed when new content is set, and they both share the same parent.
+ ((Container)pool.Parent).Clear(false);
+ }
+
+ var container = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
{
- pool = pools[poolIndex];
-
- // We need to make sure neither the pool nor the judgement get disposed when new content is set, and they both share the same parent.
- ((Container)pool.Parent).Clear(false);
- }
-
- var container = new Container
- {
- RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
+ pool,
+ pool.Get(j => j.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)).With(j =>
{
- pool,
- pool.Get(j => j.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)).With(j =>
- {
- j.Anchor = Anchor.Centre;
- j.Origin = Anchor.Centre;
- })
- }
- };
+ j.Anchor = Anchor.Centre;
+ j.Origin = Anchor.Centre;
+ })
+ }
+ };
- poolIndex++;
- return container;
- });
+ poolIndex++;
+ return container;
});
- }
+ });
+ }
+
+ private class TestDrawableOsuJudgement : DrawableOsuJudgement
+ {
+ public new SkinnableSprite Lighting => base.Lighting;
+ public new Container JudgementBody => base.JudgementBody;
}
}
}
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/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
index b357e20ee8..075bf314bc 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
@@ -9,6 +9,7 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Timing;
@@ -131,7 +132,7 @@ namespace osu.Game.Rulesets.Osu.Tests
};
}
- public Texture GetTexture(string componentName) => null;
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;
public SampleChannel GetSample(ISampleInfo sampleInfo) => null;
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
index 8cb7f3f4b6..67afc45e32 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void testSingle(float circleSize, bool auto = false)
{
- var spinner = new Spinner { StartTime = Time.Current + 1000, EndTime = Time.Current + 4000 };
+ var spinner = new Spinner { StartTime = Time.Current + 2000, EndTime = Time.Current + 5000 };
spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize });
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index 6b1394d799..c36bec391f 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) * SpinnerTick.SCORE_PER_TICK;
+ });
+
+ 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/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
index d75f4c70d7..2263e2b2f4 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs
@@ -11,6 +11,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Play;
namespace osu.Game.Rulesets.Osu.Mods
{
@@ -30,6 +31,8 @@ namespace osu.Game.Rulesets.Osu.Mods
private OsuInputManager inputManager;
+ private GameplayClock gameplayClock;
+
private List replayFrames;
private int currentFrame;
@@ -38,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
if (currentFrame == replayFrames.Count - 1) return;
- double time = playfield.Time.Current;
+ double time = gameplayClock.CurrentTime;
// Very naive implementation of autopilot based on proximity to replay frames.
// TODO: this needs to be based on user interactions to better match stable (pausing until judgement is registered).
@@ -53,6 +56,8 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
+ gameplayClock = drawableRuleset.FrameStableClock;
+
// Grab the input manager to disable the user's cursor, and for future use
inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager;
inputManager.AllowUserCursorMovement = false;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs
index 73cb483ef0..ee6a7815e2 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs
@@ -1,7 +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.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Configuration;
namespace osu.Game.Rulesets.Osu.Mods
{
@@ -15,6 +17,14 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => "Hit them at the right size!";
- protected override float StartScale => 2f;
+ [SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
+ public override BindableNumber StartScale { get; } = new BindableFloat
+ {
+ MinValue = 1f,
+ MaxValue = 25f,
+ Default = 2f,
+ Value = 2f,
+ Precision = 0.1f,
+ };
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs
index 8228161008..ff995e38ce 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public BindableNumber CircleSize { get; } = new BindableFloat
{
Precision = 0.1f,
- MinValue = 1,
+ MinValue = 0,
MaxValue = 10,
Default = 5,
Value = 5,
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public BindableNumber ApproachRate { get; } = new BindableFloat
{
Precision = 0.1f,
- MinValue = 1,
+ MinValue = 0,
MaxValue = 10,
Default = 5,
Value = 5,
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs
index f08d4e8f5e..182d6eeb4b 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs
@@ -1,7 +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.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Configuration;
namespace osu.Game.Rulesets.Osu.Mods
{
@@ -15,6 +17,14 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => "Hit them at the right size!";
- protected override float StartScale => 0.5f;
+ [SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
+ public override BindableNumber StartScale { get; } = new BindableFloat
+ {
+ MinValue = 0f,
+ MaxValue = 0.99f,
+ Default = 0.5f,
+ Value = 0.5f,
+ Precision = 0.01f,
+ };
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
index 42ddddc4dd..06ba4cde4a 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 1;
- protected virtual float StartScale => 1;
+ public abstract BindableNumber StartScale { get; }
protected virtual float EndScale => 1;
@@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Mods
case DrawableHitCircle _:
{
using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt))
- drawable.ScaleTo(StartScale).Then().ScaleTo(EndScale, h.TimePreempt, Easing.OutSine);
+ drawable.ScaleTo(StartScale.Value).Then().ScaleTo(EndScale, h.TimePreempt, Easing.OutSine);
break;
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
index 1493ddfcf3..012d9f8878 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
@@ -16,9 +16,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableOsuJudgement : DrawableJudgement
{
- private SkinnableSprite lighting;
+ protected SkinnableSprite Lighting;
+
private Bindable lightingColour;
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
public DrawableOsuJudgement(JudgementResult result, DrawableHitObject judgedObject)
: base(result, judgedObject)
{
@@ -29,18 +33,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
[BackgroundDependencyLoader]
- private void load(OsuConfigManager config)
+ private void load()
{
- if (config.Get(OsuSetting.HitLighting))
+ AddInternal(Lighting = new SkinnableSprite("lighting")
{
- AddInternal(lighting = new SkinnableSprite("lighting")
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Blending = BlendingParameters.Additive,
- Depth = float.MaxValue
- });
- }
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Blending = BlendingParameters.Additive,
+ Depth = float.MaxValue,
+ Alpha = 0
+ });
}
public override void Apply(JudgementResult result, DrawableHitObject judgedObject)
@@ -60,33 +62,39 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
lightingColour?.UnbindAll();
- if (lighting != null)
- {
- lighting.ResetAnimation();
+ Lighting.ResetAnimation();
- if (JudgedObject != null)
- {
- lightingColour = JudgedObject.AccentColour.GetBoundCopy();
- lightingColour.BindValueChanged(colour => lighting.Colour = Result.Type == HitResult.Miss ? Color4.Transparent : colour.NewValue, true);
- }
- else
- {
- lighting.Colour = Color4.White;
- }
+ if (JudgedObject != null)
+ {
+ lightingColour = JudgedObject.AccentColour.GetBoundCopy();
+ lightingColour.BindValueChanged(colour => Lighting.Colour = Result.Type == HitResult.Miss ? Color4.Transparent : colour.NewValue, true);
+ }
+ else
+ {
+ Lighting.Colour = Color4.White;
}
}
- protected override double FadeOutDelay => lighting == null ? base.FadeOutDelay : 1400;
+ private double fadeOutDelay;
+ protected override double FadeOutDelay => fadeOutDelay;
protected override void ApplyHitAnimations()
{
- if (lighting != null)
+ bool hitLightingEnabled = config.Get(OsuSetting.HitLighting);
+
+ if (hitLightingEnabled)
{
JudgementBody.FadeIn().Delay(FadeInDuration).FadeOut(400);
- lighting.ScaleTo(0.8f).ScaleTo(1.2f, 600, Easing.Out);
- lighting.FadeIn(200).Then().Delay(200).FadeOut(1000);
+ Lighting.ScaleTo(0.8f).ScaleTo(1.2f, 600, Easing.Out);
+ Lighting.FadeIn(200).Then().Delay(200).FadeOut(1000);
}
+ else
+ {
+ JudgementBody.Alpha = 1;
+ }
+
+ fadeOutDelay = hitLightingEnabled ? 1400 : base.FadeOutDelay;
JudgementText?.TransformSpacingTo(Vector2.Zero).Then().TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint);
base.ApplyHitAnimations();
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/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
index 720ffcd51c..d79ecb7b4e 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
@@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly Drawable scaleContainer;
+ public override bool DisplayResult => false;
+
public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider)
: base(sliderRepeat)
{
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index be6766509c..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,
@@ -93,7 +98,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
Background = new SpinnerBackground
{
- Alpha = 0.6f,
+ Disc =
+ {
+ Alpha = 0f,
+ },
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
@@ -117,18 +125,56 @@ 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)
{
normalColour = baseColour;
+ completeColour = colours.YellowLight;
Background.AccentColour = normalColour;
-
- completeColour = colours.YellowLight.Opacity(0.75f);
+ Ticks.AccentColour = normalColour;
Disc.AccentColour = fillColour;
circle.Colour = colours.BlueDark;
@@ -147,21 +193,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (Progress >= 1 && !Disc.Complete)
{
Disc.Complete = true;
-
- const float duration = 200;
-
- Disc.FadeAccent(completeColour, duration);
-
- Background.FadeAccent(completeColour, duration);
- Background.FadeOut(duration);
-
- circle.FadeColour(completeColour, duration);
- glow.FadeColour(completeColour, duration);
+ transformFillColour(completeColour, 200);
}
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)
@@ -191,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)));
@@ -200,36 +244,95 @@ 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();
- circleContainer.ScaleTo(Spinner.Scale * 0.3f);
- circleContainer.ScaleTo(Spinner.Scale, HitObject.TimePreempt / 1.4f, Easing.OutQuint);
+ circleContainer.ScaleTo(0);
+ mainContainer.ScaleTo(0);
- mainContainer
- .ScaleTo(0)
- .ScaleTo(Spinner.Scale * circle.DrawHeight / DrawHeight * 1.4f, HitObject.TimePreempt - 150, Easing.OutQuint)
- .Then()
- .ScaleTo(1, 500, Easing.OutQuint);
+ using (BeginDelayedSequence(HitObject.TimePreempt / 2, true))
+ {
+ float phaseOneScale = Spinner.Scale * 0.7f;
+
+ circleContainer.ScaleTo(phaseOneScale, HitObject.TimePreempt / 4, Easing.OutQuint);
+
+ mainContainer
+ .ScaleTo(phaseOneScale * circle.DrawHeight / DrawHeight * 1.6f, HitObject.TimePreempt / 4, Easing.OutQuint)
+ .RotateTo((float)(25 * Spinner.Duration / 2000), HitObject.TimePreempt + Spinner.Duration);
+
+ using (BeginDelayedSequence(HitObject.TimePreempt / 2, true))
+ {
+ circleContainer.ScaleTo(Spinner.Scale, 400, Easing.OutQuint);
+ mainContainer.ScaleTo(1, 400, Easing.OutQuint);
+ }
+ }
}
protected override void UpdateStateTransforms(ArmedState state)
{
base.UpdateStateTransforms(state);
- var sequence = this.Delay(Spinner.Duration).FadeOut(160);
-
- switch (state)
+ using (BeginDelayedSequence(Spinner.Duration, true))
{
- case ArmedState.Hit:
- sequence.ScaleTo(Scale * 1.2f, 320, Easing.Out);
- break;
+ this.FadeOut(160);
- case ArmedState.Miss:
- sequence.ScaleTo(Scale * 0.8f, 320, Easing.In);
- break;
+ switch (state)
+ {
+ case ArmedState.Hit:
+ transformFillColour(completeColour, 0);
+ this.ScaleTo(Scale * 1.2f, 320, Easing.Out);
+ mainContainer.RotateTo(mainContainer.Rotation + 180, 320);
+ break;
+
+ case ArmedState.Miss:
+ this.ScaleTo(Scale * 0.8f, 320, Easing.In);
+ break;
+ }
}
}
+
+ private void transformFillColour(Colour4 colour, double duration)
+ {
+ Disc.FadeAccent(colour, duration);
+
+ Background.FadeAccent(colour.Darken(1), duration);
+ Ticks.FadeAccent(colour, duration);
+
+ circle.FadeColour(colour, duration);
+ glow.FadeColour(colour, duration);
+ }
}
}
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/SpinnerBackground.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs
index 77228e28af..944354abca 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs
@@ -1,18 +1,18 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osuTK.Graphics;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
+using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
public class SpinnerBackground : CircularContainer, IHasAccentColour
{
- protected Box Disc;
+ public readonly Box Disc;
public Color4 AccentColour
{
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..b499d7a92b
--- /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 = $"{SpinnerBonusTick.SCORE_PER_TICK * count}";
+ bonusCounter.FadeOutFromOne(1500);
+ bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs
index 676cefb236..ba7e8eae6f 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -9,10 +10,11 @@ using osu.Framework.Graphics.Effects;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
- public class SpinnerTicks : Container
+ public class SpinnerTicks : Container, IHasAccentColour
{
public SpinnerTicks()
{
@@ -20,28 +22,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Anchor = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
- const float count = 18;
+ const float count = 8;
for (float i = 0; i < count; i++)
{
Add(new Container
{
- Colour = Color4.Black,
Alpha = 0.4f,
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Radius = 10,
- Colour = Color4.Gray.Opacity(0.2f),
- },
+ Blending = BlendingParameters.Additive,
RelativePositionAxes = Axes.Both,
Masking = true,
CornerRadius = 5,
Size = new Vector2(60, 10),
Origin = Anchor.Centre,
Position = new Vector2(
- 0.5f + MathF.Sin(i / count * 2 * MathF.PI) / 2 * 0.86f,
- 0.5f + MathF.Cos(i / count * 2 * MathF.PI) / 2 * 0.86f
+ 0.5f + MathF.Sin(i / count * 2 * MathF.PI) / 2 * 0.83f,
+ 0.5f + MathF.Cos(i / count * 2 * MathF.PI) / 2 * 0.83f
),
Rotation = -i / count * 360 + 90,
Children = new[]
@@ -54,5 +50,25 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
});
}
}
+
+ public Color4 AccentColour
+ {
+ get => Colour;
+ set
+ {
+ Colour = value;
+
+ foreach (var c in Children.OfType())
+ {
+ c.EdgeEffect =
+ new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Radius = 20,
+ Colour = value.Opacity(0.8f),
+ };
+ }
+ }
+ }
}
}
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..9c4b6f774f
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs
@@ -0,0 +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 osu.Game.Audio;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Osu.Objects
+{
+ public class SpinnerBonusTick : SpinnerTick
+ {
+ public new const int SCORE_PER_TICK = 50;
+
+ public SpinnerBonusTick()
+ {
+ Samples.Add(new HitSampleInfo { Name = "spinnerbonus" });
+ }
+
+ public override Judgement CreateJudgement() => new OsuSpinnerBonusTickJudgement();
+
+ public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement
+ {
+ protected override int NumericResultFor(HitResult result) => SCORE_PER_TICK;
+
+ 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..de3ae27e55
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs
@@ -0,0 +1,27 @@
+// 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 const int SCORE_PER_TICK = 10;
+
+ 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) => SCORE_PER_TICK;
+
+ 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/Replays/OsuFramedReplayInputHandler.cs b/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs
index b42e9ac187..cf48dc053f 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs
@@ -36,19 +36,10 @@ namespace osu.Game.Rulesets.Osu.Replays
}
}
- public override List GetPendingInputs()
+ public override void CollectPendingInputs(List inputs)
{
- return new List
- {
- new MousePositionAbsoluteInput
- {
- Position = GamefieldToScreenSpace(Position ?? Vector2.Zero)
- },
- new ReplayState
- {
- PressedActions = CurrentFrame?.Actions ?? new List()
- }
- };
+ inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(Position ?? Vector2.Zero) });
+ inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() });
}
}
}
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.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
index 3e5758ca01..95ef2d58b1 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs
@@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
case OsuSkinComponents.HitCircleText:
var font = GetConfig(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default";
- var overlap = GetConfig(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? 0;
+ var overlap = GetConfig(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? -2;
return !hasFont(font)
? null
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/TaikoBeatmapConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs
index f7729138ff..d0c57b20c0 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs
@@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
[TestCase("basic")]
[TestCase("slider-generating-drumroll")]
[TestCase("sample-to-type-conversions")]
+ [TestCase("slider-conversion-v6")]
+ [TestCase("slider-conversion-v14")]
public void Test(string name) => base.Test(name);
protected override IEnumerable CreateConvertValue(HitObject hitObject)
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/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index 78550ed270..2a1aa5d1df 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps.Formats;
namespace osu.Game.Rulesets.Taiko.Beatmaps
{
@@ -82,37 +83,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{
case IHasDistance distanceData:
{
- // Number of spans of the object - one for the initial length and for each repeat
- int spans = (obj as IHasRepeats)?.SpanCount() ?? 1;
-
- TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
- DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime);
-
- double speedAdjustment = difficultyPoint.SpeedMultiplier;
- double speedAdjustedBeatLength = timingPoint.BeatLength / speedAdjustment;
-
- // The true distance, accounting for any repeats. This ends up being the drum roll distance later
- double distance = distanceData.Distance * spans * LEGACY_VELOCITY_MULTIPLIER;
-
- // The velocity of the taiko hit object - calculated as the velocity of a drum roll
- double taikoVelocity = taiko_base_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength;
- // The duration of the taiko hit object
- double taikoDuration = distance / taikoVelocity;
-
- // The velocity of the osu! hit object - calculated as the velocity of a slider
- double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength;
- // The duration of the osu! hit object
- double osuDuration = distance / osuVelocity;
-
- // osu-stable always uses the speed-adjusted beatlength to determine the velocities, but
- // only uses it for tick rate if beatmap version < 8
- if (beatmap.BeatmapInfo.BeatmapVersion >= 8)
- speedAdjustedBeatLength *= speedAdjustment;
-
- // If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat
- double tickSpacing = Math.Min(speedAdjustedBeatLength / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate, taikoDuration / spans);
-
- if (!isForCurrentRuleset && tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength)
+ if (shouldConvertSliderToHits(obj, beatmap, distanceData, out var taikoDuration, out var tickSpacing))
{
List> allSamples = obj is IHasPathWithRepeats curveData ? curveData.NodeSamples : new List>(new[] { samples });
@@ -184,6 +155,52 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
}
}
+ private bool shouldConvertSliderToHits(HitObject obj, IBeatmap beatmap, IHasDistance distanceData, out double taikoDuration, out double tickSpacing)
+ {
+ // DO NOT CHANGE OR REFACTOR ANYTHING IN HERE WITHOUT TESTING AGAINST _ALL_ BEATMAPS.
+ // Some of these calculations look redundant, but they are not - extremely small floating point errors are introduced to maintain 1:1 compatibility with stable.
+ // Rounding cannot be used as an alternative since the error deltas have been observed to be between 1e-2 and 1e-6.
+
+ // The true distance, accounting for any repeats. This ends up being the drum roll distance later
+ int spans = (obj as IHasRepeats)?.SpanCount() ?? 1;
+ double distance = distanceData.Distance * spans * LEGACY_VELOCITY_MULTIPLIER;
+
+ TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
+ DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime);
+
+ double beatLength;
+#pragma warning disable 618
+ if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint)
+#pragma warning restore 618
+ beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
+ else
+ beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier;
+
+ double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate;
+
+ // The velocity and duration of the taiko hit object - calculated as the velocity of a drum roll.
+ double taikoVelocity = sliderScoringPointDistance * beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate;
+ taikoDuration = distance / taikoVelocity * beatLength;
+
+ if (isForCurrentRuleset)
+ {
+ tickSpacing = 0;
+ return false;
+ }
+
+ double osuVelocity = taikoVelocity * (1000f / beatLength);
+
+ // osu-stable always uses the speed-adjusted beatlength to determine the osu! velocity, but only uses it for conversion if beatmap version < 8
+ if (beatmap.BeatmapInfo.BeatmapVersion >= 8)
+ beatLength = timingPoint.BeatLength;
+
+ // If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat
+ tickSpacing = Math.Min(beatLength / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate, taikoDuration / spans);
+
+ return tickSpacing > 0
+ && distance / osuVelocity * 1000 < 2 * beatLength;
+ }
+
protected override Beatmap CreateBeatmap() => new TaikoBeatmap();
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs
index c6fe273b50..2c1885ae1a 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
double addition = 1;
// We get an extra addition if we are not a slider or spinner
- if (current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000)
+ if (current.LastObject is Hit && current.BaseObject is Hit && current.BaseObject.StartTime - current.LastObject.StartTime < 1000)
{
if (hasColourChange(current))
addition += 0.75;
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/Replays/TaikoFramedReplayInputHandler.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs
index 97337acc45..138e8f9785 100644
--- a/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs
+++ b/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs
@@ -18,6 +18,9 @@ namespace osu.Game.Rulesets.Taiko.Replays
protected override bool IsImportant(TaikoReplayFrame frame) => frame.Actions.Any();
- public override List GetPendingInputs() => new List { new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() } };
+ public override void CollectPendingInputs(List inputs)
+ {
+ inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() });
+ }
}
}
diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v14-expected-conversion.json b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v14-expected-conversion.json
new file mode 100644
index 0000000000..6a6063cb74
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v14-expected-conversion.json
@@ -0,0 +1,379 @@
+{
+ "Mappings": [{
+ "StartTime": 2000,
+ "Objects": [{
+ "StartTime": 2000,
+ "EndTime": 2000,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": true
+ },
+ {
+ "StartTime": 2173,
+ "EndTime": 2173,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ }
+ ]
+ },
+ {
+ "StartTime": 4000,
+ "Objects": [{
+ "StartTime": 4000,
+ "EndTime": 4000,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 4173,
+ "EndTime": 4173,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ }
+ ]
+ },
+ {
+ "StartTime": 6000,
+ "Objects": [{
+ "StartTime": 6000,
+ "EndTime": 6000,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 6271,
+ "EndTime": 6271,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 6542,
+ "EndTime": 6542,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ }
+ ]
+ },
+ {
+ "StartTime": 8000,
+ "Objects": [{
+ "StartTime": 8000,
+ "EndTime": 8000,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8026,
+ "EndTime": 8026,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8053,
+ "EndTime": 8053,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8080,
+ "EndTime": 8080,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8107,
+ "EndTime": 8107,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8133,
+ "EndTime": 8133,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8160,
+ "EndTime": 8160,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8187,
+ "EndTime": 8187,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8214,
+ "EndTime": 8214,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8241,
+ "EndTime": 8241,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8267,
+ "EndTime": 8267,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8294,
+ "EndTime": 8294,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8321,
+ "EndTime": 8321,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8348,
+ "EndTime": 8348,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8374,
+ "EndTime": 8374,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8401,
+ "EndTime": 8401,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8428,
+ "EndTime": 8428,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8455,
+ "EndTime": 8455,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8482,
+ "EndTime": 8482,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8508,
+ "EndTime": 8508,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8535,
+ "EndTime": 8535,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8562,
+ "EndTime": 8562,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8589,
+ "EndTime": 8589,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8615,
+ "EndTime": 8615,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8642,
+ "EndTime": 8642,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8669,
+ "EndTime": 8669,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8696,
+ "EndTime": 8696,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8723,
+ "EndTime": 8723,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8749,
+ "EndTime": 8749,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8776,
+ "EndTime": 8776,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8803,
+ "EndTime": 8803,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8830,
+ "EndTime": 8830,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 8857,
+ "EndTime": 8857,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v14.osu b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v14.osu
new file mode 100644
index 0000000000..4c8fb1fde6
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v14.osu
@@ -0,0 +1,32 @@
+osu file format v14
+
+[General]
+Mode: 0
+
+[Difficulty]
+HPDrainRate:7
+CircleSize:4
+OverallDifficulty:8
+ApproachRate:9.2
+SliderMultiplier:2.3
+SliderTickRate:1
+
+[TimingPoints]
+0,333.333333333333,4,1,0,50,1,0
+2000,-100,4,2,0,80,0,0
+
+6000,389.61038961039,4,2,1,60,1,0
+
+8000,428.571428571429,4,3,1,65,1,0
+8000,-133.333333333333,4,1,1,45,0,0
+
+[HitObjects]
+// Should convert.
+48,32,2000,6,0,B|168:32,1,120,4|2
+312,68,4000,2,0,B|288:52|256:44|216:52|200:68,1,120,0|8
+
+// Should convert.
+184,224,6000,2,0,L|336:308,2,160,2|2|0,0:0|0:0|0:0,0:0:0:0:
+
+// Should convert.
+328,36,8000,6,0,L|332:16,32,10.7812504112721,0|0,0:0,0:0:0:0:
diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v6-expected-conversion.json b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v6-expected-conversion.json
new file mode 100644
index 0000000000..c3d3c52ebd
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v6-expected-conversion.json
@@ -0,0 +1,137 @@
+{
+ "Mappings": [{
+ "StartTime": 0,
+ "Objects": [{
+ "StartTime": 0,
+ "EndTime": 0,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": true
+ },
+ {
+ "StartTime": 162,
+ "EndTime": 162,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 325,
+ "EndTime": 325,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": true
+ },
+ {
+ "StartTime": 487,
+ "EndTime": 487,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 650,
+ "EndTime": 650,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": true
+ },
+ {
+ "StartTime": 813,
+ "EndTime": 813,
+ "IsRim": false,
+ "IsCentre": true,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 975,
+ "EndTime": 975,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": true
+ }
+ ]
+ },
+ {
+ "StartTime": 2000,
+ "Objects": [{
+ "StartTime": 2000,
+ "EndTime": 2000,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 2162,
+ "EndTime": 2162,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 2325,
+ "EndTime": 2325,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": true
+ },
+ {
+ "StartTime": 2487,
+ "EndTime": 2487,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 2650,
+ "EndTime": 2650,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ },
+ {
+ "StartTime": 2813,
+ "EndTime": 2813,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": true
+ },
+ {
+ "StartTime": 2975,
+ "EndTime": 2975,
+ "IsRim": true,
+ "IsCentre": false,
+ "IsDrumRoll": false,
+ "IsSwell": false,
+ "IsStrong": false
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v6.osu b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v6.osu
new file mode 100644
index 0000000000..c1e4c3bbd7
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v6.osu
@@ -0,0 +1,20 @@
+osu file format v6
+
+[General]
+Mode: 0
+
+[Difficulty]
+HPDrainRate:3
+CircleSize:4
+OverallDifficulty:1
+SliderMultiplier:1.2
+SliderTickRate:3
+
+[TimingPoints]
+0,487.884208814441,4,1,0,60,1,0
+2000,-100,4,1,0,65,0,1
+
+[HitObjects]
+// Should convert.
+376,64,0,6,0,B|256:32|136:64,1,240,6|0
+256,120,2000,6,8,C|264:192|336:192,2,120,8|8|6
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs
index 003d40af56..e29ea87d25 100644
--- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs
+++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs
@@ -7,6 +7,10 @@ namespace osu.Game.Rulesets.Taiko.Scoring
{
internal class TaikoScoreProcessor : ScoreProcessor
{
+ protected override double DefaultAccuracyPortion => 0.75;
+
+ protected override double DefaultComboPortion => 0.25;
+
public override HitWindows CreateHitWindows() => new TaikoHitWindows();
}
}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs
index 8531f3cefd..8223e3bc01 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs
@@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Skinning;
@@ -34,13 +35,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
- Texture = skin.GetTexture("taiko-roll-end"),
+ Texture = skin.GetTexture("taiko-roll-end", WrapMode.ClampToEdge, WrapMode.ClampToEdge),
FillMode = FillMode.Fit,
},
body = new Sprite
{
RelativeSizeAxes = Axes.Both,
- Texture = skin.GetTexture("taiko-roll-middle"),
+ Texture = skin.GetTexture("taiko-roll-middle", WrapMode.ClampToEdge, WrapMode.ClampToEdge),
},
headCircle = new LegacyCirclePiece
{
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/Gameplay/TestSceneHitObjectAccentColour.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
index 7a89642e11..2d5e4b911e 100644
--- a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
@@ -8,6 +8,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Game.Audio;
@@ -118,7 +119,7 @@ namespace osu.Game.Tests.Gameplay
public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException();
- public Texture GetTexture(string componentName) => throw new NotImplementedException();
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
diff --git a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs
index 1e77d50115..42948c3731 100644
--- a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs
+++ b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs
@@ -8,7 +8,6 @@ using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Tests.Visual;
-using osu.Game.Users;
namespace osu.Game.Tests.Online
{
@@ -55,7 +54,7 @@ namespace osu.Game.Tests.Online
AddStep("fire request", () =>
{
gotResponse = false;
- request = new LeaveChannelRequest(new Channel(), new User());
+ request = new LeaveChannelRequest(new Channel());
request.Success += () => gotResponse = true;
API.Queue(request);
});
@@ -74,7 +73,7 @@ namespace osu.Game.Tests.Online
AddStep("fire request", () =>
{
gotResponse = false;
- request = new LeaveChannelRequest(new Channel(), new User());
+ request = new LeaveChannelRequest(new Channel());
request.Success += () => gotResponse = true;
API.Perform(request);
});
@@ -93,7 +92,7 @@ namespace osu.Game.Tests.Online
AddStep("fire request", () =>
{
gotResponse = false;
- request = new LeaveChannelRequest(new Channel(), new User());
+ request = new LeaveChannelRequest(new Channel());
request.Success += () => gotResponse = true;
API.PerformAsync(request);
});
diff --git a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
index 8deed75a56..ad5b3ec0f6 100644
--- a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
+++ b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
@@ -10,6 +10,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Game.Audio;
@@ -216,7 +217,7 @@ namespace osu.Game.Tests.Skins
public Drawable GetDrawableComponent(ISkinComponent component) => skin.GetDrawableComponent(component);
- public Texture GetTexture(string componentName) => skin.GetTexture(componentName);
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => skin.GetTexture(componentName, wrapModeS, wrapModeT);
public SampleChannel GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo);
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs
index 41d1459103..3a19eabe81 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs
@@ -175,13 +175,13 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("Time = 50", () => Clock.CurrentTime == 50);
AddStep("Seek(49.999)", () => Clock.Seek(49.999));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
- AddAssert("Time = 50", () => Clock.CurrentTime == 50);
+ AddAssert("Time = 100", () => Clock.CurrentTime == 100);
AddStep("Seek(99)", () => Clock.Seek(99));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 100", () => Clock.CurrentTime == 100);
AddStep("Seek(99.999)", () => Clock.Seek(99.999));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
- AddAssert("Time = 100", () => Clock.CurrentTime == 100);
+ AddAssert("Time = 100", () => Clock.CurrentTime == 150);
AddStep("Seek(174)", () => Clock.Seek(174));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 175", () => Clock.CurrentTime == 175);
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs
index 2a7f9389d1..09f5ac2224 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs
@@ -3,6 +3,7 @@
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Timing;
@@ -13,6 +14,7 @@ namespace osu.Game.Tests.Visual.Editing
public class TestSceneTimingScreen : EditorClockTestScene
{
[Cached(typeof(EditorBeatmap))]
+ [Cached(typeof(IBeatSnapProvider))]
private readonly EditorBeatmap editorBeatmap;
public TestSceneTimingScreen()
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
index 0be949650e..4743317fdd 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs
@@ -11,6 +11,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.Break;
using osu.Game.Screens.Ranking;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
@@ -35,6 +36,18 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000));
AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0));
+ double? time = null;
+
+ AddStep("store time", () => time = Player.GameplayClockContainer.GameplayClock.CurrentTime);
+
+ // test seek via keyboard
+ AddStep("seek with right arrow key", () => press(Key.Right));
+ AddAssert("time seeked forward", () => Player.GameplayClockContainer.GameplayClock.CurrentTime > time + 2000);
+
+ AddStep("store time", () => time = Player.GameplayClockContainer.GameplayClock.CurrentTime);
+ AddStep("seek with left arrow key", () => press(Key.Left));
+ AddAssert("time seeked backward", () => Player.GameplayClockContainer.GameplayClock.CurrentTime < time);
+
seekToBreak(0);
seekToBreak(1);
@@ -54,5 +67,11 @@ namespace osu.Game.Tests.Visual.Gameplay
BreakPeriod destBreak() => Beatmap.Value.Beatmap.Breaks.ElementAt(breakIndex);
}
+
+ private void press(Key key)
+ {
+ InputManager.PressKey(key);
+ InputManager.ReleaseKey(key);
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
index 1961a224c1..420bf29429 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
@@ -11,6 +11,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Rulesets;
using osu.Game.Screens.Play;
+using osu.Game.Skinning;
using osuTK;
using osuTK.Input;
@@ -221,6 +222,31 @@ namespace osu.Game.Tests.Visual.Gameplay
confirmExited();
}
+ [Test]
+ public void TestPauseSoundLoop()
+ {
+ AddStep("seek before gameplay", () => Player.GameplayClockContainer.Seek(-5000));
+
+ SkinnableSound getLoop() => Player.ChildrenOfType().FirstOrDefault()?.ChildrenOfType().FirstOrDefault();
+
+ pauseAndConfirm();
+ AddAssert("loop is playing", () => getLoop().IsPlaying);
+
+ resumeAndConfirm();
+ AddUntilStep("loop is stopped", () => !getLoop().IsPlaying);
+
+ AddUntilStep("pause again", () =>
+ {
+ Player.Pause();
+ return !Player.GameplayClockContainer.GameplayClock.IsRunning;
+ });
+
+ AddAssert("loop is playing", () => getLoop().IsPlaying);
+
+ resumeAndConfirm();
+ AddUntilStep("loop is stopped", () => !getLoop().IsPlaying);
+ }
+
private void pauseAndConfirm()
{
pause();
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
index c7455583e4..bc1c10e59d 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs
@@ -173,19 +173,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{
}
- public override List GetPendingInputs()
+ public override void CollectPendingInputs(List inputs)
{
- return new List
- {
- new MousePositionAbsoluteInput
- {
- Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero)
- },
- new ReplayState
- {
- PressedActions = CurrentFrame?.Actions ?? new List()
- }
- };
+ inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) });
+ inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() });
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs
index 7822f07957..c0f99db85d 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs
@@ -113,19 +113,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{
}
- public override List GetPendingInputs()
+ public override void CollectPendingInputs(List inputs)
{
- return new List
- {
- new MousePositionAbsoluteInput
- {
- Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero)
- },
- new ReplayState
- {
- PressedActions = CurrentFrame?.Actions ?? new List()
- }
- };
+ inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) });
+ inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() });
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
index 3b91243fee..bed48f3d86 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
@@ -10,6 +10,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
@@ -295,7 +296,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}
: null;
- public Texture GetTexture(string componentName) => throw new NotImplementedException();
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
@@ -306,7 +307,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public Drawable GetDrawableComponent(ISkinComponent componentName) => new SecondarySourceBox();
- public Texture GetTexture(string componentName) => throw new NotImplementedException();
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
@@ -318,7 +319,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public Drawable GetDrawableComponent(ISkinComponent componentName) => new BaseSourceBox();
- public Texture GetTexture(string componentName) => throw new NotImplementedException();
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
new file mode 100644
index 0000000000..e0a1f947ec
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
@@ -0,0 +1,105 @@
+// 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 NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Audio;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Framework.Timing;
+using osu.Game.Audio;
+using osu.Game.Screens.Play;
+using osu.Game.Skinning;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneSkinnableSound : OsuTestScene
+ {
+ [Cached]
+ private GameplayClock gameplayClock = new GameplayClock(new FramedClock());
+
+ private SkinnableSound skinnableSound;
+
+ [SetUp]
+ public void SetUp() => Schedule(() =>
+ {
+ gameplayClock.IsPaused.Value = false;
+
+ Children = new Drawable[]
+ {
+ new Container
+ {
+ Clock = gameplayClock,
+ RelativeSizeAxes = Axes.Both,
+ Child = skinnableSound = new SkinnableSound(new SampleInfo("normal-sliderslide"))
+ },
+ };
+ });
+
+ [Test]
+ public void TestStoppedSoundDoesntResumeAfterPause()
+ {
+ DrawableSample sample = null;
+ AddStep("start sample with looping", () =>
+ {
+ sample = skinnableSound.ChildrenOfType().First();
+
+ skinnableSound.Looping = true;
+ skinnableSound.Play();
+ });
+
+ AddUntilStep("wait for sample to start playing", () => sample.Playing);
+
+ AddStep("stop sample", () => skinnableSound.Stop());
+
+ AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
+
+ AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
+ AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false);
+
+ AddWaitStep("wait a bit", 5);
+ AddAssert("sample not playing", () => !sample.Playing);
+ }
+
+ [Test]
+ public void TestLoopingSoundResumesAfterPause()
+ {
+ DrawableSample sample = null;
+ AddStep("start sample with looping", () =>
+ {
+ skinnableSound.Looping = true;
+ skinnableSound.Play();
+ sample = skinnableSound.ChildrenOfType().First();
+ });
+
+ AddUntilStep("wait for sample to start playing", () => sample.Playing);
+
+ AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
+ AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
+ }
+
+ [Test]
+ public void TestNonLoopingStopsWithPause()
+ {
+ DrawableSample sample = null;
+ AddStep("start sample", () =>
+ {
+ skinnableSound.Play();
+ sample = skinnableSound.ChildrenOfType().First();
+ });
+
+ AddAssert("sample playing", () => sample.Playing);
+
+ AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
+ AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
+
+ AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false);
+
+ AddAssert("sample not playing", () => !sample.Playing);
+ AddAssert("sample not playing", () => !sample.Playing);
+ AddAssert("sample not playing", () => !sample.Playing);
+ }
+ }
+}
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/Online/TestSceneRankingsSpotlightSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsSpotlightSelector.cs
index 997db827f3..d60222fa0b 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneRankingsSpotlightSelector.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsSpotlightSelector.cs
@@ -30,12 +30,6 @@ namespace osu.Game.Tests.Visual.Online
Add(selector = new SpotlightSelector());
}
- [Test]
- public void TestVisibility()
- {
- AddStep("Toggle Visibility", selector.ToggleVisibility);
- }
-
[Test]
public void TestLocalSpotlights()
{
diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsTables.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsTables.cs
index a3b102dc76..ee109189c7 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneRankingsTables.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsTables.cs
@@ -15,6 +15,7 @@ using osu.Game.Rulesets.Catch;
using osu.Framework.Allocation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
+using osu.Game.Overlays.Rankings;
namespace osu.Game.Tests.Visual.Online
{
@@ -105,7 +106,7 @@ namespace osu.Game.Tests.Visual.Online
{
onLoadStarted();
- request = new GetSpotlightRankingsRequest(ruleset, spotlight);
+ request = new GetSpotlightRankingsRequest(ruleset, spotlight, RankingsSortCriteria.All);
((GetSpotlightRankingsRequest)request).Success += rankings => Schedule(() =>
{
var table = new ScoresTable(1, rankings.Users);
diff --git a/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs
deleted file mode 100644
index 77e77d90c1..0000000000
--- a/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs
+++ /dev/null
@@ -1,84 +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 NUnit.Framework;
-using osu.Game.Overlays;
-using osu.Game.Users;
-
-namespace osu.Game.Tests.Visual.Online
-{
- [TestFixture]
- public class TestSceneSocialOverlay : OsuTestScene
- {
- protected override bool UseOnlineAPI => true;
-
- public TestSceneSocialOverlay()
- {
- SocialOverlay s = new SocialOverlay
- {
- Users = new[]
- {
- new User
- {
- Username = @"flyte",
- Id = 3103765,
- Country = new Country { FlagName = @"JP" },
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg",
- },
- new User
- {
- Username = @"Cookiezi",
- Id = 124493,
- Country = new Country { FlagName = @"KR" },
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg",
- },
- new User
- {
- Username = @"Angelsim",
- Id = 1777162,
- Country = new Country { FlagName = @"KR" },
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
- },
- new User
- {
- Username = @"Rafis",
- Id = 2558286,
- Country = new Country { FlagName = @"PL" },
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c4.jpg",
- },
- new User
- {
- Username = @"hvick225",
- Id = 50265,
- Country = new Country { FlagName = @"TW" },
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c5.jpg",
- },
- new User
- {
- Username = @"peppy",
- Id = 2,
- Country = new Country { FlagName = @"AU" },
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
- },
- new User
- {
- Username = @"filsdelama",
- Id = 2831793,
- Country = new Country { FlagName = @"FR" },
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c7.jpg"
- },
- new User
- {
- Username = @"_index",
- Id = 652457,
- Country = new Country { FlagName = @"RU" },
- CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c8.jpg"
- },
- },
- };
- Add(s);
-
- AddStep(@"toggle", s.ToggleVisibility);
- }
- }
-}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs
index f763e50067..c2e9945c99 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs
@@ -42,6 +42,19 @@ namespace osu.Game.Tests.Visual.Online
Spacing = new Vector2(10f),
Children = new Drawable[]
{
+ new UserBrickPanel(new User
+ {
+ Username = @"flyte",
+ Id = 3103765,
+ CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
+ }),
+ new UserBrickPanel(new User
+ {
+ Username = @"peppy",
+ Id = 2,
+ Colour = "99EB47",
+ CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ }),
flyte = new UserGridPanel(new User
{
Username = @"flyte",
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs
new file mode 100644
index 0000000000..c2ac5179c9
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs
@@ -0,0 +1,67 @@
+// 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.Overlays.Comments.Buttons;
+using osu.Framework.Graphics;
+using osu.Framework.Allocation;
+using osu.Game.Overlays;
+using osu.Framework.Graphics.Containers;
+using osuTK;
+using NUnit.Framework;
+using System.Linq;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Testing;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneCommentRepliesButton : OsuTestScene
+ {
+ [Cached]
+ private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
+
+ private readonly TestButton button;
+
+ public TestSceneCommentRepliesButton()
+ {
+ Child = new FillFlowContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 10),
+ Children = new Drawable[]
+ {
+ button = new TestButton(),
+ new LoadRepliesButton
+ {
+ Action = () => { }
+ },
+ new ShowRepliesButton(1),
+ new ShowRepliesButton(2)
+ }
+ };
+ }
+
+ [Test]
+ public void TestArrowDirection()
+ {
+ AddStep("Set upwards", () => button.SetIconDirection(true));
+ AddAssert("Icon facing upwards", () => button.Icon.Scale.Y == -1);
+ AddStep("Set downwards", () => button.SetIconDirection(false));
+ AddAssert("Icon facing downwards", () => button.Icon.Scale.Y == 1);
+ }
+
+ private class TestButton : CommentRepliesButton
+ {
+ public SpriteIcon Icon => this.ChildrenOfType().First();
+
+ public TestButton()
+ {
+ Text = "sample text";
+ }
+
+ public new void SetIconDirection(bool upwards) => base.SetIconDirection(upwards);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLogoAnimation.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoAnimation.cs
new file mode 100644
index 0000000000..155d043bf9
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoAnimation.cs
@@ -0,0 +1,46 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ [TestFixture]
+ public class TestSceneLogoAnimation : OsuTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load(LargeTextureStore textures)
+ {
+ LogoAnimation anim2;
+
+ Add(anim2 = new LogoAnimation
+ {
+ RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fit,
+ Texture = textures.Get("Intro/Triangles/logo-highlight"),
+ Colour = Colour4.White,
+ });
+
+ LogoAnimation anim;
+
+ Add(anim = new LogoAnimation
+ {
+ RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fit,
+ Texture = textures.Get("Intro/Triangles/logo-background"),
+ Colour = OsuColour.Gray(0.6f),
+ });
+
+ AddSliderStep("Progress", 0f, 1f, 0f, newValue =>
+ {
+ anim2.AnimationProgress = newValue;
+ anim.AnimationProgress = newValue;
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
index ce691bff70..6f083f4ab6 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
@@ -13,6 +13,8 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mania;
+using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
@@ -99,6 +101,12 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestManiaMods()
{
changeRuleset(3);
+
+ var mania = new ManiaRuleset();
+
+ testModsWithSameBaseType(
+ mania.GetAllMods().Single(m => m.GetType() == typeof(ManiaModFadeIn)),
+ mania.GetAllMods().Single(m => m.GetType() == typeof(ManiaModHidden)));
}
[Test]
@@ -197,6 +205,18 @@ namespace osu.Game.Tests.Visual.UserInterface
checkLabelColor(() => Color4.White);
}
+ private void testModsWithSameBaseType(Mod modA, Mod modB)
+ {
+ selectNext(modA);
+ checkSelected(modA);
+ selectNext(modB);
+ checkSelected(modB);
+
+ // Backwards
+ selectPrevious(modA);
+ checkSelected(modA);
+ }
+
private void selectNext(Mod mod) => AddStep($"left click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectNext(1));
private void selectPrevious(Mod mod) => AddStep($"right click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectNext(-1));
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/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs
index 99e0bf4e33..11fee030f8 100644
--- a/osu.Game/Beatmaps/BeatmapConverter.cs
+++ b/osu.Game/Beatmaps/BeatmapConverter.cs
@@ -57,7 +57,7 @@ namespace osu.Game.Beatmaps
beatmap.BeatmapInfo = original.BeatmapInfo;
beatmap.ControlPointInfo = original.ControlPointInfo;
- beatmap.HitObjects = convertHitObjects(original.HitObjects, original);
+ beatmap.HitObjects = convertHitObjects(original.HitObjects, original).OrderBy(s => s.StartTime).ToList();
beatmap.Breaks = original.Breaks;
return beatmap;
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/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
index 3106d1143e..16207c7d2a 100644
--- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
+++ b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
@@ -48,16 +48,13 @@ namespace osu.Game.Beatmaps
public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken)
{
- if (api?.State != APIState.Online)
- return Task.CompletedTask;
-
LogForModel(beatmapSet, "Performing online lookups...");
return Task.WhenAll(beatmapSet.Beatmaps.Select(b => UpdateAsync(beatmapSet, b, cancellationToken)).ToArray());
}
// todo: expose this when we need to do individual difficulty lookups.
protected Task UpdateAsync(BeatmapSetInfo beatmapSet, BeatmapInfo beatmap, CancellationToken cancellationToken)
- => Task.Factory.StartNew(() => lookup(beatmapSet, beatmap), cancellationToken, TaskCreationOptions.HideScheduler, updateScheduler);
+ => Task.Factory.StartNew(() => lookup(beatmapSet, beatmap), cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler);
private void lookup(BeatmapSetInfo set, BeatmapInfo beatmap)
{
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
index af6ca24165..e7788b75f3 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
@@ -64,49 +64,49 @@ namespace osu.Game.Beatmaps.ControlPoints
///
/// The time to find the difficulty control point at.
/// The difficulty control point.
- public DifficultyControlPoint DifficultyPointAt(double time) => binarySearchWithFallback(DifficultyPoints, time);
+ public DifficultyControlPoint DifficultyPointAt(double time) => binarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT);
///
/// Finds the effect control point that is active at .
///
/// The time to find the effect control point at.
/// The effect control point.
- public EffectControlPoint EffectPointAt(double time) => binarySearchWithFallback(EffectPoints, time);
+ public EffectControlPoint EffectPointAt(double time) => binarySearchWithFallback(EffectPoints, time, EffectControlPoint.DEFAULT);
///
/// Finds the sound control point that is active at .
///
/// The time to find the sound control point at.
/// The sound control point.
- public SampleControlPoint SamplePointAt(double time) => binarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : null);
+ public SampleControlPoint SamplePointAt(double time) => binarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : SampleControlPoint.DEFAULT);
///
/// Finds the timing control point that is active at .
///
/// The time to find the timing control point at.
/// The timing control point.
- public TimingControlPoint TimingPointAt(double time) => binarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : null);
+ public TimingControlPoint TimingPointAt(double time) => binarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : TimingControlPoint.DEFAULT);
///
/// Finds the maximum BPM represented by any timing control point.
///
[JsonIgnore]
public double BPMMaximum =>
- 60000 / (TimingPoints.OrderBy(c => c.BeatLength).FirstOrDefault() ?? new TimingControlPoint()).BeatLength;
+ 60000 / (TimingPoints.OrderBy(c => c.BeatLength).FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength;
///
/// Finds the minimum BPM represented by any timing control point.
///
[JsonIgnore]
public double BPMMinimum =>
- 60000 / (TimingPoints.OrderByDescending(c => c.BeatLength).FirstOrDefault() ?? new TimingControlPoint()).BeatLength;
+ 60000 / (TimingPoints.OrderByDescending(c => c.BeatLength).FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength;
///
/// Finds the mode BPM (most common BPM) represented by the control points.
///
[JsonIgnore]
public double BPMMode =>
- 60000 / (TimingPoints.GroupBy(c => c.BeatLength).OrderByDescending(grp => grp.Count()).FirstOrDefault()?.FirstOrDefault() ?? new TimingControlPoint()).BeatLength;
+ 60000 / (TimingPoints.GroupBy(c => c.BeatLength).OrderByDescending(grp => grp.Count()).FirstOrDefault()?.FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength;
///
/// Remove all s and return to a pristine state.
@@ -170,12 +170,12 @@ namespace osu.Game.Beatmaps.ControlPoints
///
/// The list to search.
/// The time to find the control point at.
- /// The control point to use when is before any control points. If null, a new control point will be constructed.
+ /// The control point to use when is before any control points.
/// The active control point at , or a fallback if none found.
- private T binarySearchWithFallback(IReadOnlyList list, double time, T prePoint = null)
- where T : ControlPoint, new()
+ private T binarySearchWithFallback(IReadOnlyList list, double time, T fallback)
+ where T : ControlPoint
{
- return binarySearch(list, time) ?? prePoint ?? new T();
+ return binarySearch(list, time) ?? fallback;
}
///
diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
index 2448b2b25c..1d38790f87 100644
--- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
@@ -7,6 +7,11 @@ namespace osu.Game.Beatmaps.ControlPoints
{
public class DifficultyControlPoint : ControlPoint
{
+ public static readonly DifficultyControlPoint DEFAULT = new DifficultyControlPoint
+ {
+ SpeedMultiplierBindable = { Disabled = true },
+ };
+
///
/// The speed multiplier at this control point.
///
diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
index 9b69147468..9e8e3978be 100644
--- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
@@ -7,6 +7,12 @@ namespace osu.Game.Beatmaps.ControlPoints
{
public class EffectControlPoint : ControlPoint
{
+ public static readonly EffectControlPoint DEFAULT = new EffectControlPoint
+ {
+ KiaiModeBindable = { Disabled = true },
+ OmitFirstBarLineBindable = { Disabled = true }
+ };
+
///
/// Whether the first bar line of this control point is ignored.
///
diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
index 61851a00d7..c052c04ea0 100644
--- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
@@ -10,6 +10,12 @@ namespace osu.Game.Beatmaps.ControlPoints
{
public const string DEFAULT_BANK = "normal";
+ public static readonly SampleControlPoint DEFAULT = new SampleControlPoint
+ {
+ SampleBankBindable = { Disabled = true },
+ SampleVolumeBindable = { Disabled = true }
+ };
+
///
/// The default sample bank at this control point.
///
diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
index 1927dd6575..9345299c3a 100644
--- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
@@ -13,6 +13,21 @@ namespace osu.Game.Beatmaps.ControlPoints
///
public readonly Bindable TimeSignatureBindable = new Bindable(TimeSignatures.SimpleQuadruple) { Default = TimeSignatures.SimpleQuadruple };
+ ///
+ /// Default length of a beat in milliseconds. Used whenever there is no beatmap or track playing.
+ ///
+ private const double default_beat_length = 60000.0 / 60.0;
+
+ public static readonly TimingControlPoint DEFAULT = new TimingControlPoint
+ {
+ BeatLengthBindable =
+ {
+ Value = default_beat_length,
+ Disabled = true
+ },
+ TimeSignatureBindable = { Disabled = true }
+ };
+
///
/// The time signature at this control point.
///
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index be5cd78dc8..b30ec0ca2c 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -369,7 +369,9 @@ namespace osu.Game.Beatmaps.Formats
addControlPoint(time, controlPoint, true);
}
- addControlPoint(time, new LegacyDifficultyControlPoint
+#pragma warning disable 618
+ addControlPoint(time, new LegacyDifficultyControlPoint(beatLength)
+#pragma warning restore 618
{
SpeedMultiplier = speedMultiplier,
}, timingChange);
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index a0e83554a3..44ef9bcacc 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -159,11 +159,20 @@ namespace osu.Game.Beatmaps.Formats
Mania,
}
- internal class LegacyDifficultyControlPoint : DifficultyControlPoint
+ [Obsolete("Do not use unless you're a legacy ruleset and 100% sure.")]
+ public class LegacyDifficultyControlPoint : DifficultyControlPoint
{
- public LegacyDifficultyControlPoint()
+ ///
+ /// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it.
+ /// DO NOT USE THIS UNLESS 100% SURE.
+ ///
+ public readonly float BpmMultiplier;
+
+ public LegacyDifficultyControlPoint(double beatLength)
{
SpeedMultiplierBindable.Precision = double.Epsilon;
+
+ BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100f : 1;
}
}
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index 9d31bc9bba..a8a8794320 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -91,6 +91,7 @@ namespace osu.Game.Configuration
Set(OsuSetting.FadePlayfieldWhenHealthLow, true);
Set(OsuSetting.KeyOverlay, false);
Set(OsuSetting.PositionalHitSounds, true);
+ Set(OsuSetting.AlwaysPlayFirstComboBreak, true);
Set(OsuSetting.ScoreMeter, ScoreMeterType.HitErrorBoth);
Set(OsuSetting.FloatingComments, false);
@@ -98,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);
@@ -180,6 +182,7 @@ namespace osu.Game.Configuration
ShowStoryboard,
KeyOverlay,
PositionalHitSounds,
+ AlwaysPlayFirstComboBreak,
ScoreMeter,
FloatingComments,
ShowInterface,
@@ -227,6 +230,7 @@ namespace osu.Game.Configuration
IntroSequence,
UIHoldActivationDelay,
HitLighting,
- MenuBackgroundSource
+ MenuBackgroundSource,
+ GameplayDisableWinKey
}
}
diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs
index dd5c41285a..df063f57d5 100644
--- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs
+++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs
@@ -43,14 +43,6 @@ namespace osu.Game.Graphics.Containers
///
public double MinimumBeatLength { get; set; }
- ///
- /// Default length of a beat in milliseconds. Used whenever there is no beatmap or track playing.
- ///
- private const double default_beat_length = 60000.0 / 60.0;
-
- private TimingControlPoint defaultTiming;
- private EffectControlPoint defaultEffect;
-
protected bool IsBeatSyncedWithTrack { get; private set; }
protected override void Update()
@@ -81,8 +73,8 @@ namespace osu.Game.Graphics.Containers
if (timingPoint == null || !IsBeatSyncedWithTrack)
{
currentTrackTime = Clock.CurrentTime;
- timingPoint = defaultTiming;
- effectPoint = defaultEffect;
+ timingPoint = TimingControlPoint.DEFAULT;
+ effectPoint = EffectControlPoint.DEFAULT;
}
double beatLength = timingPoint.BeatLength / Divisor;
@@ -116,17 +108,6 @@ namespace osu.Game.Graphics.Containers
private void load(IBindable beatmap)
{
Beatmap.BindTo(beatmap);
-
- defaultTiming = new TimingControlPoint
- {
- BeatLength = default_beat_length,
- };
-
- defaultEffect = new EffectControlPoint
- {
- KiaiMode = false,
- OmitFirstBarLine = false
- };
}
protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
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/Graphics/Sprites/LogoAnimation.cs b/osu.Game/Graphics/Sprites/LogoAnimation.cs
new file mode 100644
index 0000000000..b1383065fe
--- /dev/null
+++ b/osu.Game/Graphics/Sprites/LogoAnimation.cs
@@ -0,0 +1,69 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.OpenGL.Vertices;
+using osu.Framework.Graphics.Shaders;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+
+namespace osu.Game.Graphics.Sprites
+{
+ public class LogoAnimation : Sprite
+ {
+ [BackgroundDependencyLoader]
+ private void load(ShaderManager shaders, TextureStore textures)
+ {
+ TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"LogoAnimation");
+ RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"LogoAnimation"); // Masking isn't supported for now
+ }
+
+ private float animationProgress;
+
+ public float AnimationProgress
+ {
+ get => animationProgress;
+ set
+ {
+ if (animationProgress == value) return;
+
+ animationProgress = value;
+ Invalidate(Invalidation.DrawInfo);
+ }
+ }
+
+ public override bool IsPresent => true;
+
+ protected override DrawNode CreateDrawNode() => new LogoAnimationDrawNode(this);
+
+ private class LogoAnimationDrawNode : SpriteDrawNode
+ {
+ private LogoAnimation source => (LogoAnimation)Source;
+
+ private float progress;
+
+ public LogoAnimationDrawNode(LogoAnimation source)
+ : base(source)
+ {
+ }
+
+ public override void ApplyState()
+ {
+ base.ApplyState();
+
+ progress = source.animationProgress;
+ }
+
+ protected override void Blit(Action vertexAction)
+ {
+ Shader.GetUniform("progress").UpdateValue(ref progress);
+
+ base.Blit(vertexAction);
+ }
+
+ protected override bool CanDrawOpaqueInterior => false;
+ }
+ }
+}
diff --git a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs
index 8977f014b6..f77a3109c9 100644
--- a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs
+++ b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs
@@ -67,6 +67,8 @@ namespace osu.Game.Graphics.UserInterface
public bool OnPressed(GlobalAction action)
{
+ if (!HasFocus) return false;
+
if (action == GlobalAction.Back)
{
if (Text.Length > 0)
diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs
index 8a5acbadbc..6ae420b162 100644
--- a/osu.Game/Input/Bindings/GlobalActionContainer.cs
+++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs
@@ -57,6 +57,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.Tilde }, GlobalAction.QuickExit),
new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed),
new KeyBinding(new[] { InputKey.Control, InputKey.Minus }, GlobalAction.DecreaseScrollSpeed),
+ new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay),
};
public IEnumerable AudioControlKeyBindings => new[]
@@ -160,6 +161,9 @@ namespace osu.Game.Input.Bindings
Home,
[Description("Toggle notifications")]
- ToggleNotifications
+ ToggleNotifications,
+
+ [Description("Pause")]
+ PauseGameplay,
}
}
diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs
index 0f8acbb7af..2115326cc2 100644
--- a/osu.Game/Online/API/APIRequest.cs
+++ b/osu.Game/Online/API/APIRequest.cs
@@ -5,6 +5,7 @@ using System;
using Newtonsoft.Json;
using osu.Framework.IO.Network;
using osu.Framework.Logging;
+using osu.Game.Users;
namespace osu.Game.Online.API
{
@@ -61,6 +62,11 @@ namespace osu.Game.Online.API
protected APIAccess API;
protected WebRequest WebRequest;
+ ///
+ /// The currently logged in user. Note that this will only be populated during .
+ ///
+ protected User User { get; private set; }
+
///
/// Invoked on successful completion of an API request.
/// This will be scheduled to the API's internal scheduler (run on update thread automatically).
@@ -86,6 +92,7 @@ namespace osu.Game.Online.API
}
API = apiAccess;
+ User = apiAccess.LocalUser.Value;
if (checkAndScheduleFailure())
return;
diff --git a/osu.Game/Online/API/Requests/GetSpotlightRankingsRequest.cs b/osu.Game/Online/API/Requests/GetSpotlightRankingsRequest.cs
index a279db134f..25e6b3f1af 100644
--- a/osu.Game/Online/API/Requests/GetSpotlightRankingsRequest.cs
+++ b/osu.Game/Online/API/Requests/GetSpotlightRankingsRequest.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.IO.Network;
+using osu.Game.Overlays.Rankings;
using osu.Game.Rulesets;
namespace osu.Game.Online.API.Requests
@@ -9,11 +10,13 @@ namespace osu.Game.Online.API.Requests
public class GetSpotlightRankingsRequest : GetRankingsRequest
{
private readonly int spotlight;
+ private readonly RankingsSortCriteria sort;
- public GetSpotlightRankingsRequest(RulesetInfo ruleset, int spotlight)
+ public GetSpotlightRankingsRequest(RulesetInfo ruleset, int spotlight, RankingsSortCriteria sort)
: base(ruleset, 1)
{
this.spotlight = spotlight;
+ this.sort = sort;
}
protected override WebRequest CreateWebRequest()
@@ -21,6 +24,7 @@ namespace osu.Game.Online.API.Requests
var req = base.CreateWebRequest();
req.AddParameter("spotlight", spotlight.ToString());
+ req.AddParameter("filter", sort.ToString().ToLower());
return req;
}
diff --git a/osu.Game/Online/API/Requests/JoinChannelRequest.cs b/osu.Game/Online/API/Requests/JoinChannelRequest.cs
index f6ed5f22c9..33eab7e355 100644
--- a/osu.Game/Online/API/Requests/JoinChannelRequest.cs
+++ b/osu.Game/Online/API/Requests/JoinChannelRequest.cs
@@ -4,19 +4,16 @@
using System.Net.Http;
using osu.Framework.IO.Network;
using osu.Game.Online.Chat;
-using osu.Game.Users;
namespace osu.Game.Online.API.Requests
{
public class JoinChannelRequest : APIRequest
{
private readonly Channel channel;
- private readonly User user;
- public JoinChannelRequest(Channel channel, User user)
+ public JoinChannelRequest(Channel channel)
{
this.channel = channel;
- this.user = user;
}
protected override WebRequest CreateWebRequest()
@@ -26,6 +23,6 @@ namespace osu.Game.Online.API.Requests
return req;
}
- protected override string Target => $@"chat/channels/{channel.Id}/users/{user.Id}";
+ protected override string Target => $@"chat/channels/{channel.Id}/users/{User.Id}";
}
}
diff --git a/osu.Game/Online/API/Requests/LeaveChannelRequest.cs b/osu.Game/Online/API/Requests/LeaveChannelRequest.cs
index f2ae3926bd..7dfc9a0aed 100644
--- a/osu.Game/Online/API/Requests/LeaveChannelRequest.cs
+++ b/osu.Game/Online/API/Requests/LeaveChannelRequest.cs
@@ -4,19 +4,16 @@
using System.Net.Http;
using osu.Framework.IO.Network;
using osu.Game.Online.Chat;
-using osu.Game.Users;
namespace osu.Game.Online.API.Requests
{
public class LeaveChannelRequest : APIRequest
{
private readonly Channel channel;
- private readonly User user;
- public LeaveChannelRequest(Channel channel, User user)
+ public LeaveChannelRequest(Channel channel)
{
this.channel = channel;
- this.user = user;
}
protected override WebRequest CreateWebRequest()
@@ -26,6 +23,6 @@ namespace osu.Game.Online.API.Requests
return req;
}
- protected override string Target => $@"chat/channels/{channel.Id}/users/{user.Id}";
+ protected override string Target => $@"chat/channels/{channel.Id}/users/{User.Id}";
}
}
diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs
index 3b336fef4f..f7ed57f207 100644
--- a/osu.Game/Online/Chat/ChannelManager.cs
+++ b/osu.Game/Online/Chat/ChannelManager.cs
@@ -381,7 +381,7 @@ namespace osu.Game.Online.Chat
break;
default:
- var req = new JoinChannelRequest(channel, api.LocalUser.Value);
+ var req = new JoinChannelRequest(channel);
req.Success += () => joinChannel(channel, fetchInitialMessages);
req.Failure += ex => LeaveChannel(channel);
api.Queue(req);
@@ -410,7 +410,7 @@ namespace osu.Game.Online.Chat
if (channel.Joined.Value)
{
- api.Queue(new LeaveChannelRequest(channel, api.LocalUser.Value));
+ api.Queue(new LeaveChannelRequest(channel));
channel.Joined.Value = false;
}
}
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 71%
rename from osu.Game/Online/API/Requests/JoinRoomRequest.cs
rename to osu.Game/Online/Multiplayer/JoinRoomRequest.cs
index 36b275236c..74375af856 100644
--- a/osu.Game/Online/API/Requests/JoinRoomRequest.cs
+++ b/osu.Game/Online/Multiplayer/JoinRoomRequest.cs
@@ -3,20 +3,17 @@
using System.Net.Http;
using osu.Framework.IO.Network;
-using osu.Game.Online.Multiplayer;
-using osu.Game.Users;
+using osu.Game.Online.API;
-namespace osu.Game.Online.API.Requests
+namespace osu.Game.Online.Multiplayer
{
public class JoinRoomRequest : APIRequest
{
private readonly Room room;
- private readonly User user;
- public JoinRoomRequest(Room room, User user)
+ public JoinRoomRequest(Room room)
{
this.room = room;
- this.user = user;
}
protected override WebRequest CreateWebRequest()
@@ -26,6 +23,6 @@ namespace osu.Game.Online.API.Requests
return req;
}
- protected override string Target => $"rooms/{room.RoomID.Value}/users/{user.Id}";
+ protected override string Target => $"rooms/{room.RoomID.Value}/users/{User.Id}";
}
}
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 71%
rename from osu.Game/Online/API/Requests/PartRoomRequest.cs
rename to osu.Game/Online/Multiplayer/PartRoomRequest.cs
index e1550cb2e0..54bb005d96 100644
--- a/osu.Game/Online/API/Requests/PartRoomRequest.cs
+++ b/osu.Game/Online/Multiplayer/PartRoomRequest.cs
@@ -3,20 +3,17 @@
using System.Net.Http;
using osu.Framework.IO.Network;
-using osu.Game.Online.Multiplayer;
-using osu.Game.Users;
+using osu.Game.Online.API;
-namespace osu.Game.Online.API.Requests
+namespace osu.Game.Online.Multiplayer
{
public class PartRoomRequest : APIRequest
{
private readonly Room room;
- private readonly User user;
- public PartRoomRequest(Room room, User user)
+ public PartRoomRequest(Room room)
{
this.room = room;
- this.user = user;
}
protected override WebRequest CreateWebRequest()
@@ -26,6 +23,6 @@ namespace osu.Game.Online.API.Requests
return req;
}
- protected override string Target => $"rooms/{room.RoomID.Value}/users/{user.Id}";
+ protected override string Target => $"rooms/{room.RoomID.Value}/users/{User.Id}";
}
}
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 47a7c2ae11..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;
@@ -63,7 +64,8 @@ namespace osu.Game
private ChannelManager channelManager;
- private NotificationOverlay notifications;
+ [NotNull]
+ private readonly NotificationOverlay notifications = new NotificationOverlay();
private NowPlayingOverlay nowPlaying;
@@ -82,7 +84,7 @@ namespace osu.Game
public virtual Storage GetStorageForStableInstall() => null;
- public float ToolbarOffset => Toolbar.Position.Y + Toolbar.DrawHeight;
+ public float ToolbarOffset => (Toolbar?.Position.Y ?? 0) + (Toolbar?.DrawHeight ?? 0);
private IdleTracker idleTracker;
@@ -250,7 +252,7 @@ namespace osu.Game
case LinkAction.OpenEditorTimestamp:
case LinkAction.JoinMultiplayerMatch:
case LinkAction.Spectate:
- waitForReady(() => notifications, _ => notifications?.Post(new SimpleNotification
+ waitForReady(() => notifications, _ => notifications.Post(new SimpleNotification
{
Text = @"This link type is not yet supported!",
Icon = FontAwesome.Solid.LifeRing,
@@ -536,14 +538,14 @@ namespace osu.Game
MenuCursorContainer.CanShowCursor = menuScreen?.CursorVisible ?? false;
// todo: all archive managers should be able to be looped here.
- SkinManager.PostNotification = n => notifications?.Post(n);
+ SkinManager.PostNotification = n => notifications.Post(n);
SkinManager.GetStableStorage = GetStorageForStableInstall;
- BeatmapManager.PostNotification = n => notifications?.Post(n);
+ BeatmapManager.PostNotification = n => notifications.Post(n);
BeatmapManager.GetStableStorage = GetStorageForStableInstall;
BeatmapManager.PresentImport = items => PresentBeatmap(items.First());
- ScoreManager.PostNotification = n => notifications?.Post(n);
+ ScoreManager.PostNotification = n => notifications.Post(n);
ScoreManager.GetStableStorage = GetStorageForStableInstall;
ScoreManager.PresentImport = items => PresentScore(items.First());
@@ -573,7 +575,9 @@ namespace osu.Game
Origin = Anchor.BottomLeft,
Action = () =>
{
- if ((ScreenStack.CurrentScreen as IOsuScreen)?.AllowBackButton == true)
+ var currentScreen = ScreenStack.CurrentScreen as IOsuScreen;
+
+ if (currentScreen?.AllowBackButton == true && !currentScreen.OnBackButton())
ScreenStack.Exit();
}
},
@@ -613,12 +617,12 @@ namespace osu.Game
loadComponentSingleFile(MusicController = new MusicController(), Add, true);
- loadComponentSingleFile(notifications = new NotificationOverlay
+ loadComponentSingleFile(notifications.With(d =>
{
- GetToolbarHeight = () => ToolbarOffset,
- Anchor = Anchor.TopRight,
- Origin = Anchor.TopRight,
- }, rightFloatingOverlayContent.Add, true);
+ d.GetToolbarHeight = () => ToolbarOffset;
+ d.Anchor = Anchor.TopRight;
+ d.Origin = Anchor.TopRight;
+ }), rightFloatingOverlayContent.Add, true);
loadComponentSingleFile(screenshotManager, Add);
@@ -756,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 c79f710151..fe5c0704b7 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -152,6 +152,7 @@ namespace osu.Game
AddFont(Resources, @"Fonts/Noto-Hangul");
AddFont(Resources, @"Fonts/Noto-CJK-Basic");
AddFont(Resources, @"Fonts/Noto-CJK-Compatibility");
+ AddFont(Resources, @"Fonts/Noto-Thai");
AddFont(Resources, @"Fonts/Venera-Light");
AddFont(Resources, @"Fonts/Venera-Bold");
@@ -198,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/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs
index 496986dc56..4eb348ae33 100644
--- a/osu.Game/Overlays/Chat/ChatLine.cs
+++ b/osu.Game/Overlays/Chat/ChatLine.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
@@ -114,21 +115,26 @@ namespace osu.Game.Overlays.Chat
Colour = Color4.Black.Opacity(0.3f),
Type = EdgeEffectType.Shadow,
},
- // Drop shadow effect
Child = new Container
{
AutoSizeAxes = Axes.Both,
+ Y = 3,
Masking = true,
CornerRadius = 4,
- EdgeEffect = new EdgeEffectParameters
+ Children = new Drawable[]
{
- Radius = 1,
- Colour = Color4Extensions.FromHex(message.Sender.Colour),
- Type = EdgeEffectType.Shadow,
- },
- Padding = new MarginPadding { Left = 3, Right = 3, Bottom = 1, Top = -3 },
- Y = 3,
- Child = username,
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4Extensions.FromHex(message.Sender.Colour),
+ },
+ new Container
+ {
+ AutoSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Left = 4, Right = 4, Bottom = 1, Top = -2 },
+ Child = username
+ }
+ }
}
};
}
diff --git a/osu.Game/Overlays/Comments/Buttons/ChevronButton.cs b/osu.Game/Overlays/Comments/Buttons/ChevronButton.cs
new file mode 100644
index 0000000000..48f34e8f59
--- /dev/null
+++ b/osu.Game/Overlays/Comments/Buttons/ChevronButton.cs
@@ -0,0 +1,48 @@
+// 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.Game.Graphics.Containers;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics.Sprites;
+using osuTK;
+using osu.Framework.Allocation;
+
+namespace osu.Game.Overlays.Comments.Buttons
+{
+ public class ChevronButton : OsuHoverContainer
+ {
+ public readonly BindableBool Expanded = new BindableBool(true);
+
+ private readonly SpriteIcon icon;
+
+ public ChevronButton()
+ {
+ Size = new Vector2(40, 22);
+ Child = icon = new SpriteIcon
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(12),
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
+ {
+ IdleColour = HoverColour = colourProvider.Foreground1;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ Action = Expanded.Toggle;
+ Expanded.BindValueChanged(onExpandedChanged, true);
+ }
+
+ private void onExpandedChanged(ValueChangedEvent expanded)
+ {
+ icon.Icon = expanded.NewValue ? FontAwesome.Solid.ChevronUp : FontAwesome.Solid.ChevronDown;
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs
new file mode 100644
index 0000000000..53438ca421
--- /dev/null
+++ b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs
@@ -0,0 +1,113 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osuTK;
+
+namespace osu.Game.Overlays.Comments.Buttons
+{
+ public abstract class CommentRepliesButton : CompositeDrawable
+ {
+ protected string Text
+ {
+ get => text.Text;
+ set => text.Text = value;
+ }
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; }
+
+ private readonly SpriteIcon icon;
+ private readonly Box background;
+ private readonly OsuSpriteText text;
+
+ protected CommentRepliesButton()
+ {
+ AutoSizeAxes = Axes.Both;
+ InternalChildren = new Drawable[]
+ {
+ new CircularContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Masking = true,
+ Children = new Drawable[]
+ {
+ background = new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ new Container
+ {
+ AutoSizeAxes = Axes.Both,
+ Margin = new MarginPadding
+ {
+ Vertical = 5,
+ Horizontal = 10,
+ },
+ Child = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(15, 0),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ text = new OsuSpriteText
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ AlwaysPresent = true,
+ Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold)
+ },
+ icon = new SpriteIcon
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Size = new Vector2(7.5f),
+ Icon = FontAwesome.Solid.ChevronDown
+ }
+ }
+ }
+ }
+ }
+ },
+ new HoverClickSounds(),
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ background.Colour = colourProvider.Background2;
+ icon.Colour = colourProvider.Foreground1;
+ }
+
+ protected void SetIconDirection(bool upwards) => icon.ScaleTo(new Vector2(1, upwards ? -1 : 1));
+
+ public void ToggleTextVisibility(bool visible) => text.FadeTo(visible ? 1 : 0, 200, Easing.OutQuint);
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ base.OnHover(e);
+ background.FadeColour(colourProvider.Background1, 200, Easing.OutQuint);
+ icon.FadeColour(colourProvider.Light1, 200, Easing.OutQuint);
+ return true;
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ base.OnHoverLost(e);
+ background.FadeColour(colourProvider.Background2, 200, Easing.OutQuint);
+ icon.FadeColour(colourProvider.Foreground1, 200, Easing.OutQuint);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Comments/Buttons/LoadRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/LoadRepliesButton.cs
new file mode 100644
index 0000000000..4998e5391e
--- /dev/null
+++ b/osu.Game/Overlays/Comments/Buttons/LoadRepliesButton.cs
@@ -0,0 +1,32 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Graphics.UserInterface;
+
+namespace osu.Game.Overlays.Comments.Buttons
+{
+ public class LoadRepliesButton : LoadingButton
+ {
+ private ButtonContent content;
+
+ public LoadRepliesButton()
+ {
+ AutoSizeAxes = Axes.Both;
+ }
+
+ protected override Drawable CreateContent() => content = new ButtonContent();
+
+ protected override void OnLoadStarted() => content.ToggleTextVisibility(false);
+
+ protected override void OnLoadFinished() => content.ToggleTextVisibility(true);
+
+ private class ButtonContent : CommentRepliesButton
+ {
+ public ButtonContent()
+ {
+ Text = "load replies";
+ }
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Comments/GetCommentRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/ShowMoreButton.cs
similarity index 69%
rename from osu.Game/Overlays/Comments/GetCommentRepliesButton.cs
rename to osu.Game/Overlays/Comments/Buttons/ShowMoreButton.cs
index a3817ba416..2c363564d2 100644
--- a/osu.Game/Overlays/Comments/GetCommentRepliesButton.cs
+++ b/osu.Game/Overlays/Comments/Buttons/ShowMoreButton.cs
@@ -8,38 +8,42 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using System.Collections.Generic;
using osuTK;
+using osu.Framework.Allocation;
-namespace osu.Game.Overlays.Comments
+namespace osu.Game.Overlays.Comments.Buttons
{
- public abstract class GetCommentRepliesButton : LoadingButton
+ public class ShowMoreButton : LoadingButton
{
- private const int duration = 200;
-
protected override IEnumerable EffectTargets => new[] { text };
private OsuSpriteText text;
- protected GetCommentRepliesButton()
+ public ShowMoreButton()
{
AutoSizeAxes = Axes.Both;
LoadingAnimationSize = new Vector2(8);
}
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
+ {
+ IdleColour = colourProvider.Light2;
+ HoverColour = colourProvider.Light1;
+ }
+
protected override Drawable CreateContent() => new Container
{
AutoSizeAxes = Axes.Both,
Child = text = new OsuSpriteText
{
AlwaysPresent = true,
- Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
- Text = GetText()
+ Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
+ Text = "show more"
}
};
- protected abstract string GetText();
+ protected override void OnLoadStarted() => text.FadeOut(200, Easing.OutQuint);
- protected override void OnLoadStarted() => text.FadeOut(duration, Easing.OutQuint);
-
- protected override void OnLoadFinished() => text.FadeIn(duration, Easing.OutQuint);
+ protected override void OnLoadFinished() => text.FadeIn(200, Easing.OutQuint);
}
}
diff --git a/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs
new file mode 100644
index 0000000000..04e7e25cc5
--- /dev/null
+++ b/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs
@@ -0,0 +1,31 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Humanizer;
+using osu.Framework.Bindables;
+using osu.Framework.Input.Events;
+
+namespace osu.Game.Overlays.Comments.Buttons
+{
+ public class ShowRepliesButton : CommentRepliesButton
+ {
+ public readonly BindableBool Expanded = new BindableBool(true);
+
+ public ShowRepliesButton(int count)
+ {
+ Text = "reply".ToQuantity(count);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ Expanded.BindValueChanged(expanded => SetIconDirection(expanded.NewValue), true);
+ }
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ Expanded.Toggle();
+ return true;
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs
index c54d06e44e..0a8cafc3d0 100644
--- a/osu.Game/Overlays/Comments/CommentsContainer.cs
+++ b/osu.Game/Overlays/Comments/CommentsContainer.cs
@@ -81,21 +81,22 @@ namespace osu.Game.Overlays.Comments
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = colourProvider.Background4
- },
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
+ Margin = new MarginPadding { Bottom = 20 },
Children = new Drawable[]
{
deletedCommentsCounter = new DeletedCommentsCounter
{
- ShowDeleted = { BindTarget = ShowDeleted }
+ ShowDeleted = { BindTarget = ShowDeleted },
+ Margin = new MarginPadding
+ {
+ Horizontal = 70,
+ Vertical = 10
+ }
},
new Container
{
@@ -105,7 +106,10 @@ namespace osu.Game.Overlays.Comments
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Margin = new MarginPadding(5),
+ Margin = new MarginPadding
+ {
+ Vertical = 10
+ },
Action = getComments,
IsLoading = true,
}
diff --git a/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs b/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs
index f22086bf23..56588ef0a8 100644
--- a/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs
+++ b/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs
@@ -23,8 +23,6 @@ namespace osu.Game.Overlays.Comments
public DeletedCommentsCounter()
{
AutoSizeAxes = Axes.Both;
- Margin = new MarginPadding { Vertical = 10, Left = 80 };
-
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs
index 46f600615a..9c0a48ec29 100644
--- a/osu.Game/Overlays/Comments/DrawableComment.cs
+++ b/osu.Game/Overlays/Comments/DrawableComment.cs
@@ -16,19 +16,18 @@ using System.Linq;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Chat;
using osu.Framework.Allocation;
-using osuTK.Graphics;
using System.Collections.Generic;
using System;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Extensions.IEnumerableExtensions;
using System.Collections.Specialized;
+using osu.Game.Overlays.Comments.Buttons;
namespace osu.Game.Overlays.Comments
{
public class DrawableComment : CompositeDrawable
{
private const int avatar_size = 40;
- private const int margin = 10;
public Action RepliesRequested;
@@ -46,9 +45,9 @@ namespace osu.Game.Overlays.Comments
private FillFlowContainer childCommentsVisibilityContainer;
private FillFlowContainer childCommentsContainer;
- private LoadMoreCommentsButton loadMoreCommentsButton;
+ private LoadRepliesButton loadRepliesButton;
private ShowMoreButton showMoreButton;
- private RepliesButton repliesButton;
+ private ShowRepliesButton showRepliesButton;
private ChevronButton chevronButton;
private DeletedCommentsCounter deletedCommentsCounter;
@@ -58,7 +57,7 @@ namespace osu.Game.Overlays.Comments
}
[BackgroundDependencyLoader]
- private void load()
+ private void load(OverlayColourProvider colourProvider)
{
LinkFlowContainer username;
FillFlowContainer info;
@@ -70,25 +69,25 @@ namespace osu.Game.Overlays.Comments
AutoSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
{
- new FillFlowContainer
+ new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
+ Padding = getPadding(Comment.IsTopLevel),
+ Child = new FillFlowContainer
{
- new Container
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding(margin) { Left = margin + 5 },
- Child = content = new GridContainer
+ content = new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
ColumnDimensions = new[]
{
- new Dimension(GridSizeMode.AutoSize),
+ new Dimension(GridSizeMode.Absolute, size: avatar_size + 10),
new Dimension(),
},
RowDimensions = new[]
@@ -99,69 +98,65 @@ namespace osu.Game.Overlays.Comments
{
new Drawable[]
{
- new FillFlowContainer
+ new Container
{
- AutoSizeAxes = Axes.Both,
- Margin = new MarginPadding { Horizontal = margin },
- Direction = FillDirection.Horizontal,
- Spacing = new Vector2(5, 0),
+ Size = new Vector2(avatar_size),
Children = new Drawable[]
{
- new Container
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Width = 40,
- AutoSizeAxes = Axes.Y,
- Child = votePill = new VotePill(Comment)
- {
- Anchor = Anchor.CentreRight,
- Origin = Anchor.CentreRight,
- }
- },
new UpdateableAvatar(Comment.User)
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
Size = new Vector2(avatar_size),
Masking = true,
CornerRadius = avatar_size / 2f,
CornerExponent = 2,
},
+ votePill = new VotePill(Comment)
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreRight,
+ Margin = new MarginPadding
+ {
+ Right = 5
+ }
+ }
}
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Spacing = new Vector2(0, 3),
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 4),
+ Margin = new MarginPadding
+ {
+ Vertical = 2
+ },
Children = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
- Spacing = new Vector2(7, 0),
+ Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
- username = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, italics: true))
+ username = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold))
{
- AutoSizeAxes = Axes.Both,
+ AutoSizeAxes = Axes.Both
},
new ParentUsername(Comment),
new OsuSpriteText
{
Alpha = Comment.IsDeleted ? 1 : 0,
- Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, italics: true),
- Text = @"deleted",
+ Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold),
+ Text = "deleted"
}
}
},
message = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 14))
{
RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding { Right = 40 }
+ AutoSizeAxes = Axes.Y
},
info = new FillFlowContainer
{
@@ -170,19 +165,22 @@ namespace osu.Game.Overlays.Comments
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
- new OsuSpriteText
+ new DrawableDate(Comment.CreatedAt, 12, false)
{
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Font = OsuFont.GetFont(size: 12),
- Colour = OsuColour.Gray(0.7f),
- Text = HumanizerUtils.Humanize(Comment.CreatedAt)
- },
- repliesButton = new RepliesButton(Comment.RepliesCount)
+ Colour = colourProvider.Foreground1
+ }
+ }
+ },
+ new Container
+ {
+ AutoSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ showRepliesButton = new ShowRepliesButton(Comment.RepliesCount)
{
Expanded = { BindTarget = childrenExpanded }
},
- loadMoreCommentsButton = new LoadMoreCommentsButton
+ loadRepliesButton = new LoadRepliesButton
{
Action = () => RepliesRequested(this, ++currentPage)
}
@@ -192,41 +190,51 @@ namespace osu.Game.Overlays.Comments
}
}
}
- }
- },
- childCommentsVisibilityContainer = new FillFlowContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
+ },
+ childCommentsVisibilityContainer = new FillFlowContainer
{
- childCommentsContainer = new FillFlowContainer
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Padding = new MarginPadding { Left = 20 },
+ Children = new Drawable[]
{
- Padding = new MarginPadding { Left = 20 },
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical
- },
- deletedCommentsCounter = new DeletedCommentsCounter
- {
- ShowDeleted = { BindTarget = ShowDeleted }
- },
- showMoreButton = new ShowMoreButton
- {
- Action = () => RepliesRequested(this, ++currentPage)
+ childCommentsContainer = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical
+ },
+ deletedCommentsCounter = new DeletedCommentsCounter
+ {
+ ShowDeleted = { BindTarget = ShowDeleted },
+ Margin = new MarginPadding
+ {
+ Top = 10
+ }
+ },
+ showMoreButton = new ShowMoreButton
+ {
+ Action = () => RepliesRequested(this, ++currentPage)
+ }
}
- }
- },
+ },
+ }
}
},
- chevronButton = new ChevronButton
+ new Container
{
+ Size = new Vector2(70, 40),
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
- Margin = new MarginPadding { Right = 30, Top = margin },
- Expanded = { BindTarget = childrenExpanded },
- Alpha = 0
+ Margin = new MarginPadding { Horizontal = 5 },
+ Child = chevronButton = new ChevronButton
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Expanded = { BindTarget = childrenExpanded },
+ Alpha = 0
+ }
}
};
@@ -239,10 +247,9 @@ namespace osu.Game.Overlays.Comments
{
info.Add(new OsuSpriteText
{
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Font = OsuFont.GetFont(size: 12),
- Text = $@"edited {HumanizerUtils.Humanize(Comment.EditedAt.Value)} by {Comment.EditedUser.Username}"
+ Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular),
+ Text = $@"edited {HumanizerUtils.Humanize(Comment.EditedAt.Value)} by {Comment.EditedUser.Username}",
+ Colour = colourProvider.Foreground1
});
}
@@ -339,77 +346,31 @@ namespace osu.Game.Overlays.Comments
var loadedReplesCount = loadedReplies.Count;
var hasUnloadedReplies = loadedReplesCount != Comment.RepliesCount;
- loadMoreCommentsButton.FadeTo(hasUnloadedReplies && loadedReplesCount == 0 ? 1 : 0);
+ loadRepliesButton.FadeTo(hasUnloadedReplies && loadedReplesCount == 0 ? 1 : 0);
showMoreButton.FadeTo(hasUnloadedReplies && loadedReplesCount > 0 ? 1 : 0);
- repliesButton.FadeTo(loadedReplesCount != 0 ? 1 : 0);
+ showRepliesButton.FadeTo(loadedReplesCount != 0 ? 1 : 0);
if (Comment.IsTopLevel)
chevronButton.FadeTo(loadedReplesCount != 0 ? 1 : 0);
- showMoreButton.IsLoading = loadMoreCommentsButton.IsLoading = false;
+ showMoreButton.IsLoading = loadRepliesButton.IsLoading = false;
}
- private class ChevronButton : ShowChildrenButton
+ private MarginPadding getPadding(bool isTopLevel)
{
- private readonly SpriteIcon icon;
-
- public ChevronButton()
+ if (isTopLevel)
{
- Child = icon = new SpriteIcon
+ return new MarginPadding
{
- Size = new Vector2(12),
+ Horizontal = 70,
+ Vertical = 15
};
}
- protected override void OnExpandedChanged(ValueChangedEvent expanded)
+ return new MarginPadding
{
- icon.Icon = expanded.NewValue ? FontAwesome.Solid.ChevronUp : FontAwesome.Solid.ChevronDown;
- }
- }
-
- private class RepliesButton : ShowChildrenButton
- {
- private readonly SpriteText text;
- private readonly int count;
-
- public RepliesButton(int count)
- {
- this.count = count;
-
- Child = text = new OsuSpriteText
- {
- Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
- };
- }
-
- protected override void OnExpandedChanged(ValueChangedEvent expanded)
- {
- text.Text = $@"{(expanded.NewValue ? "[-]" : "[+]")} replies ({count})";
- }
- }
-
- private class LoadMoreCommentsButton : GetCommentRepliesButton
- {
- public LoadMoreCommentsButton()
- {
- IdleColour = OsuColour.Gray(0.7f);
- HoverColour = Color4.White;
- }
-
- protected override string GetText() => @"[+] load replies";
- }
-
- private class ShowMoreButton : GetCommentRepliesButton
- {
- [BackgroundDependencyLoader]
- private void load(OverlayColourProvider colourProvider)
- {
- Margin = new MarginPadding { Vertical = 10, Left = 80 };
- IdleColour = colourProvider.Light2;
- HoverColour = colourProvider.Light1;
- }
-
- protected override string GetText() => @"Show More";
+ Top = 10
+ };
}
private class ParentUsername : FillFlowContainer, IHasTooltip
diff --git a/osu.Game/Overlays/Comments/ShowChildrenButton.cs b/osu.Game/Overlays/Comments/ShowChildrenButton.cs
deleted file mode 100644
index 5ec7c1d471..0000000000
--- a/osu.Game/Overlays/Comments/ShowChildrenButton.cs
+++ /dev/null
@@ -1,33 +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 osu.Framework.Graphics;
-using osu.Game.Graphics.Containers;
-using osu.Framework.Bindables;
-using osuTK.Graphics;
-using osu.Game.Graphics;
-
-namespace osu.Game.Overlays.Comments
-{
- public abstract class ShowChildrenButton : OsuHoverContainer
- {
- public readonly BindableBool Expanded = new BindableBool(true);
-
- protected ShowChildrenButton()
- {
- AutoSizeAxes = Axes.Both;
- IdleColour = OsuColour.Gray(0.7f);
- HoverColour = Color4.White;
- }
-
- protected override void LoadComplete()
- {
- Action = Expanded.Toggle;
-
- Expanded.BindValueChanged(OnExpandedChanged, true);
- base.LoadComplete();
- }
-
- protected abstract void OnExpandedChanged(ValueChangedEvent expanded);
- }
-}
diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs
index 79fda99c73..41b25ee1a5 100644
--- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs
+++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs
@@ -225,6 +225,9 @@ namespace osu.Game.Overlays.Dashboard.Friends
case OverlayPanelDisplayStyle.List:
return new UserListPanel(user);
+
+ case OverlayPanelDisplayStyle.Brick:
+ return new UserBrickPanel(user);
}
}
diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs
index 7235a18a23..3701f9ecab 100644
--- a/osu.Game/Overlays/Mods/ModSection.cs
+++ b/osu.Game/Overlays/Mods/ModSection.cs
@@ -132,7 +132,7 @@ namespace osu.Game.Overlays.Mods
{
foreach (var button in buttons)
{
- int i = Array.FindIndex(button.Mods, m => modTypes.Any(t => t.IsInstanceOfType(m)));
+ int i = Array.FindIndex(button.Mods, m => modTypes.Any(t => t == m.GetType()));
if (i >= 0)
button.SelectAt(i);
diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
index 3d0ad1a594..8a5e4d2683 100644
--- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs
+++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs
@@ -19,6 +19,7 @@ using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Input.Bindings;
using osu.Game.Overlays.Mods.Sections;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens;
@@ -403,6 +404,8 @@ namespace osu.Game.Overlays.Mods
return base.OnKeyDown(e);
}
+ public override bool OnPressed(GlobalAction action) => false; // handled by back button
+
private void availableModsChanged(ValueChangedEvent>> mods)
{
if (mods.NewValue == null) return;
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/OverlayPanelDisplayStyleControl.cs b/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs
index 7269007b41..87b9d89d4d 100644
--- a/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs
+++ b/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs
@@ -34,6 +34,10 @@ namespace osu.Game.Overlays
{
Icon = FontAwesome.Solid.Bars
});
+ AddTabItem(new PanelDisplayTabItem(OverlayPanelDisplayStyle.Brick)
+ {
+ Icon = FontAwesome.Solid.Th
+ });
}
protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer
@@ -96,6 +100,7 @@ namespace osu.Game.Overlays
public enum OverlayPanelDisplayStyle
{
Card,
- List
+ List,
+ Brick
}
}
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/Rankings/SpotlightSelector.cs b/osu.Game/Overlays/Rankings/SpotlightSelector.cs
index f019b50ae8..422373d099 100644
--- a/osu.Game/Overlays/Rankings/SpotlightSelector.cs
+++ b/osu.Game/Overlays/Rankings/SpotlightSelector.cs
@@ -18,14 +18,10 @@ using osu.Game.Online.API.Requests;
namespace osu.Game.Overlays.Rankings
{
- public class SpotlightSelector : VisibilityContainer, IHasCurrentValue
+ public class SpotlightSelector : CompositeDrawable, IHasCurrentValue
{
- private const int duration = 300;
-
- private readonly Box background;
- private readonly SpotlightsDropdown dropdown;
-
private readonly BindableWithCurrent current = new BindableWithCurrent();
+ public readonly Bindable Sort = new Bindable();
public Bindable Current
{
@@ -39,60 +35,79 @@ namespace osu.Game.Overlays.Rankings
set => dropdown.Items = value;
}
- protected override bool StartHidden => true;
-
+ private readonly Box background;
+ private readonly SpotlightsDropdown dropdown;
private readonly InfoColumn startDateColumn;
private readonly InfoColumn endDateColumn;
private readonly InfoColumn mapCountColumn;
private readonly InfoColumn participantsColumn;
- private readonly Container content;
public SpotlightSelector()
{
RelativeSizeAxes = Axes.X;
- Height = 100;
- Add(content = new Container
+ AutoSizeAxes = Axes.Y;
+ InternalChildren = new Drawable[]
{
- RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
+ background = new Box
{
- background = new Box
+ RelativeSizeAxes = Axes.Both,
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN },
+ Child = new FillFlowContainer
{
- RelativeSizeAxes = Axes.Both,
- },
- new Container
- {
- RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN, Vertical = 10 },
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
Children = new Drawable[]
{
- dropdown = new SpotlightsDropdown
+ new Container
{
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
+ Margin = new MarginPadding { Vertical = 20 },
RelativeSizeAxes = Axes.X,
- Current = Current,
- Depth = -float.MaxValue
+ Height = 40,
+ Depth = -float.MaxValue,
+ Child = dropdown = new SpotlightsDropdown
+ {
+ RelativeSizeAxes = Axes.X,
+ Current = Current
+ }
},
- new FillFlowContainer
+ new Container
{
- Anchor = Anchor.BottomRight,
- Origin = Anchor.BottomRight,
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Horizontal,
- Spacing = new Vector2(15, 0),
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
- startDateColumn = new InfoColumn(@"Start Date"),
- endDateColumn = new InfoColumn(@"End Date"),
- mapCountColumn = new InfoColumn(@"Map Count"),
- participantsColumn = new InfoColumn(@"Participants")
+ new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(10, 0),
+ Margin = new MarginPadding { Bottom = 5 },
+ Children = new Drawable[]
+ {
+ startDateColumn = new InfoColumn(@"Start Date"),
+ endDateColumn = new InfoColumn(@"End Date"),
+ mapCountColumn = new InfoColumn(@"Map Count"),
+ participantsColumn = new InfoColumn(@"Participants")
+ }
+ },
+ new RankingsSortTabControl
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ Current = Sort
+ }
}
}
}
}
}
- });
+ };
}
[BackgroundDependencyLoader]
@@ -109,10 +124,6 @@ namespace osu.Game.Overlays.Rankings
participantsColumn.Value = response.Spotlight.Participants?.ToString("N0");
}
- protected override void PopIn() => content.FadeIn(duration, Easing.OutQuint);
-
- protected override void PopOut() => content.FadeOut(duration, Easing.OutQuint);
-
private string dateToString(DateTimeOffset date) => date.ToString("yyyy-MM-dd");
private class InfoColumn : FillFlowContainer
@@ -128,12 +139,13 @@ namespace osu.Game.Overlays.Rankings
{
AutoSizeAxes = Axes.Both;
Direction = FillDirection.Vertical;
+ Margin = new MarginPadding { Vertical = 10 };
Children = new Drawable[]
{
new OsuSpriteText
{
Text = name,
- Font = OsuFont.GetFont(size: 10),
+ Font = OsuFont.GetFont(size: 10, weight: FontWeight.Regular),
},
new Container
{
@@ -143,7 +155,7 @@ namespace osu.Game.Overlays.Rankings
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
- Font = OsuFont.GetFont(size: 18, weight: FontWeight.Light),
+ Font = OsuFont.GetFont(size: 20, weight: FontWeight.Light),
}
}
};
diff --git a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs
index 917509e842..61339df76f 100644
--- a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs
+++ b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs
@@ -24,6 +24,7 @@ namespace osu.Game.Overlays.Rankings
public readonly Bindable Ruleset = new Bindable();
private readonly Bindable selectedSpotlight = new Bindable();
+ private readonly Bindable sort = new Bindable();
[Resolved]
private IAPIProvider api { get; set; }
@@ -72,15 +73,16 @@ namespace osu.Game.Overlays.Rankings
}
}
};
+
+ sort.BindTo(selector.Sort);
}
protected override void LoadComplete()
{
base.LoadComplete();
- selector.Show();
-
- selectedSpotlight.BindValueChanged(onSpotlightChanged);
+ selectedSpotlight.BindValueChanged(_ => onSpotlightChanged());
+ sort.BindValueChanged(_ => onSpotlightChanged());
Ruleset.BindValueChanged(onRulesetChanged);
getSpotlights();
@@ -101,14 +103,14 @@ namespace osu.Game.Overlays.Rankings
selectedSpotlight.TriggerChange();
}
- private void onSpotlightChanged(ValueChangedEvent spotlight)
+ private void onSpotlightChanged()
{
loading.Show();
cancellationToken?.Cancel();
getRankingsRequest?.Cancel();
- getRankingsRequest = new GetSpotlightRankingsRequest(Ruleset.Value, spotlight.NewValue.Id);
+ getRankingsRequest = new GetSpotlightRankingsRequest(Ruleset.Value, selectedSpotlight.Value.Id, sort.Value);
getRankingsRequest.Success += onSuccess;
api.Queue(getRankingsRequest);
}
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/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs
index 52b712a40e..34e5da4ef4 100644
--- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
[Resolved]
private OsuColour colours { get; set; }
- private UserPanel panel;
+ private UserGridPanel panel;
private UserDropdown dropdown;
///
diff --git a/osu.Game/Overlays/Social/FilterControl.cs b/osu.Game/Overlays/Social/FilterControl.cs
deleted file mode 100644
index 93fcc3c401..0000000000
--- a/osu.Game/Overlays/Social/FilterControl.cs
+++ /dev/null
@@ -1,33 +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 osu.Framework.Extensions.Color4Extensions;
-using osuTK.Graphics;
-using osu.Framework.Graphics;
-using osu.Game.Overlays.SearchableList;
-
-namespace osu.Game.Overlays.Social
-{
- public class FilterControl : SearchableListFilterControl
- {
- protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"47253a");
- protected override SocialSortCriteria DefaultTab => SocialSortCriteria.Rank;
- protected override SortDirection DefaultCategory => SortDirection.Ascending;
-
- public FilterControl()
- {
- Tabs.Margin = new MarginPadding { Top = 10 };
- }
- }
-
- public enum SocialSortCriteria
- {
- Rank,
- Name,
- Location,
- //[Description("Time Zone")]
- //TimeZone,
- //[Description("World Map")]
- //WorldMap,
- }
-}
diff --git a/osu.Game/Overlays/Social/Header.cs b/osu.Game/Overlays/Social/Header.cs
deleted file mode 100644
index 22e0fdcd56..0000000000
--- a/osu.Game/Overlays/Social/Header.cs
+++ /dev/null
@@ -1,67 +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 osu.Game.Overlays.SearchableList;
-using osuTK.Graphics;
-using osu.Framework.Graphics;
-using osu.Game.Graphics.Sprites;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Graphics;
-using osu.Framework.Allocation;
-using System.ComponentModel;
-using osu.Framework.Extensions.Color4Extensions;
-using osu.Framework.Graphics.Sprites;
-
-namespace osu.Game.Overlays.Social
-{
- public class Header : SearchableListHeader
- {
- private OsuSpriteText browser;
-
- protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"38202e");
-
- protected override SocialTab DefaultTab => SocialTab.AllPlayers;
- protected override IconUsage Icon => FontAwesome.Solid.Users;
-
- protected override Drawable CreateHeaderText()
- {
- return new FillFlowContainer
- {
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Horizontal,
- Children = new[]
- {
- new OsuSpriteText
- {
- Text = "social ",
- Font = OsuFont.GetFont(size: 25),
- },
- browser = new OsuSpriteText
- {
- Text = "browser",
- Font = OsuFont.GetFont(size: 25, weight: FontWeight.Light),
- },
- },
- };
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- browser.Colour = colours.Pink;
- }
- }
-
- public enum SocialTab
- {
- [Description("All Players")]
- AllPlayers,
-
- [Description("Friends")]
- Friends,
- //[Description("Team Members")]
- //TeamMembers,
- //[Description("Chat Channels")]
- //ChatChannels,
- }
-}
diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs
deleted file mode 100644
index 1b05142192..0000000000
--- a/osu.Game/Overlays/SocialOverlay.cs
+++ /dev/null
@@ -1,242 +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;
-using System.Collections.Generic;
-using System.Linq;
-using osu.Framework.Bindables;
-using osuTK;
-using osuTK.Graphics;
-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.Overlays.SearchableList;
-using osu.Game.Overlays.Social;
-using osu.Game.Users;
-using System.Threading;
-using osu.Framework.Allocation;
-using osu.Framework.Extensions.Color4Extensions;
-using osu.Framework.Threading;
-
-namespace osu.Game.Overlays
-{
- public class SocialOverlay : SearchableListOverlay
- {
- private readonly LoadingSpinner loading;
- private FillFlowContainer panels;
-
- protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"60284b");
- protected override Color4 TrianglesColourLight => Color4Extensions.FromHex(@"672b51");
- protected override Color4 TrianglesColourDark => Color4Extensions.FromHex(@"5c2648");
-
- protected override SearchableListHeader CreateHeader() => new Header();
- protected override SearchableListFilterControl CreateFilterControl() => new FilterControl();
-
- private User[] users = Array.Empty();
-
- public User[] Users
- {
- get => users;
- set
- {
- if (users == value)
- return;
-
- users = value ?? Array.Empty();
-
- if (LoadState >= LoadState.Ready)
- recreatePanels();
- }
- }
-
- public SocialOverlay()
- : base(OverlayColourScheme.Pink)
- {
- Add(loading = new LoadingSpinner());
-
- Filter.Search.Current.ValueChanged += text =>
- {
- if (!string.IsNullOrEmpty(text.NewValue))
- {
- // force searching in players until searching for friends is supported
- Header.Tabs.Current.Value = SocialTab.AllPlayers;
-
- if (Filter.Tabs.Current.Value != SocialSortCriteria.Rank)
- Filter.Tabs.Current.Value = SocialSortCriteria.Rank;
- }
- };
-
- Header.Tabs.Current.ValueChanged += _ => queueUpdate();
- Filter.Tabs.Current.ValueChanged += _ => onFilterUpdate();
-
- Filter.DisplayStyleControl.DisplayStyle.ValueChanged += _ => recreatePanels();
- Filter.Dropdown.Current.ValueChanged += _ => recreatePanels();
-
- currentQuery.BindTo(Filter.Search.Current);
- currentQuery.ValueChanged += query =>
- {
- queryChangedDebounce?.Cancel();
-
- if (string.IsNullOrEmpty(query.NewValue))
- queueUpdate();
- else
- queryChangedDebounce = Scheduler.AddDelayed(updateSearch, 500);
- };
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- recreatePanels();
- }
-
- private APIRequest getUsersRequest;
-
- private readonly Bindable currentQuery = new Bindable();
-
- private ScheduledDelegate queryChangedDebounce;
-
- private void queueUpdate() => Scheduler.AddOnce(updateSearch);
-
- private CancellationTokenSource loadCancellation;
-
- private void updateSearch()
- {
- queryChangedDebounce?.Cancel();
-
- if (!IsLoaded)
- return;
-
- Users = null;
- clearPanels();
- getUsersRequest?.Cancel();
-
- if (API?.IsLoggedIn != true)
- return;
-
- switch (Header.Tabs.Current.Value)
- {
- case SocialTab.Friends:
- var friendRequest = new GetFriendsRequest(); // TODO filter arguments?
- friendRequest.Success += users => Users = users.ToArray();
- API.Queue(getUsersRequest = friendRequest);
- break;
-
- default:
- var userRequest = new GetUsersRequest(); // TODO filter arguments!
- userRequest.Success += res => Users = res.Users.Select(r => r.User).ToArray();
- API.Queue(getUsersRequest = userRequest);
- break;
- }
- }
-
- private void recreatePanels()
- {
- clearPanels();
-
- if (Users == null)
- {
- loading.Hide();
- return;
- }
-
- IEnumerable sortedUsers = Users;
-
- switch (Filter.Tabs.Current.Value)
- {
- case SocialSortCriteria.Location:
- sortedUsers = sortedUsers.OrderBy(u => u.Country.FullName);
- break;
-
- case SocialSortCriteria.Name:
- sortedUsers = sortedUsers.OrderBy(u => u.Username);
- break;
- }
-
- if (Filter.Dropdown.Current.Value == SortDirection.Descending)
- sortedUsers = sortedUsers.Reverse();
-
- var newPanels = new FillFlowContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Spacing = new Vector2(10f),
- Margin = new MarginPadding { Top = 10 },
- ChildrenEnumerable = sortedUsers.Select(u =>
- {
- UserPanel panel;
-
- switch (Filter.DisplayStyleControl.DisplayStyle.Value)
- {
- case PanelDisplayStyle.Grid:
- panel = new UserGridPanel(u)
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Width = 290,
- };
- break;
-
- default:
- panel = new UserListPanel(u);
- break;
- }
-
- panel.Status.BindTo(u.Status);
- panel.Activity.BindTo(u.Activity);
- return panel;
- })
- };
-
- LoadComponentAsync(newPanels, f =>
- {
- if (panels != null)
- ScrollFlow.Remove(panels);
-
- loading.Hide();
- ScrollFlow.Add(panels = newPanels);
- }, (loadCancellation = new CancellationTokenSource()).Token);
- }
-
- private void onFilterUpdate()
- {
- if (Filter.Tabs.Current.Value == SocialSortCriteria.Rank)
- {
- queueUpdate();
- return;
- }
-
- recreatePanels();
- }
-
- private void clearPanels()
- {
- loading.Show();
-
- loadCancellation?.Cancel();
-
- if (panels != null)
- {
- panels.Expire();
- panels = null;
- }
- }
-
- public override void APIStateChanged(IAPIProvider api, APIState state)
- {
- switch (state)
- {
- case APIState.Online:
- queueUpdate();
- break;
-
- default:
- Users = null;
- clearPanels();
- break;
- }
- }
- }
-}
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/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs
index 052aaa3c65..d24c81536e 100644
--- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs
+++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs
@@ -130,7 +130,11 @@ namespace osu.Game.Rulesets.Judgements
if (type == currentDrawableType)
return;
- InternalChild = JudgementBody = new Container
+ // sub-classes might have added their own children that would be removed here if .InternalChild was used.
+ if (JudgementBody != null)
+ RemoveInternal(JudgementBody);
+
+ AddInternal(JudgementBody = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -142,7 +146,7 @@ namespace osu.Game.Rulesets.Judgements
Colour = colours.ForHitResult(type),
Scale = new Vector2(0.85f, 1),
}, confineMode: ConfineMode.NoScaling)
- };
+ });
currentDrawableType = type;
}
diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs
index c3a8efdd66..165644edbe 100644
--- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs
+++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mods
public BindableNumber DrainRate { get; } = new BindableFloat
{
Precision = 0.1f,
- MinValue = 1,
+ MinValue = 0,
MaxValue = 10,
Default = 5,
Value = 5,
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mods
public BindableNumber OverallDifficulty { get; } = new BindableFloat
{
Precision = 0.1f,
- MinValue = 1,
+ MinValue = 0,
MaxValue = 10,
Default = 5,
Value = 5,
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 44afb7a227..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,13 +145,19 @@ namespace osu.Game.Rulesets.Objects.Drawables
}
samplesBindable = HitObject.SamplesBindable.GetBoundCopy();
- samplesBindable.CollectionChanged += (_, __) => loadSamples();
+ samplesBindable.CollectionChanged += (_, __) => LoadSamples();
- updateState(ArmedState.Idle, true);
apply(HitObject);
}
- private void loadSamples()
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ updateState(ArmedState.Idle, true);
+ }
+
+ protected virtual void LoadSamples()
{
if (Samples != null)
{
@@ -352,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/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs
index 55d82c4083..cf5c88b8fd 100644
--- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs
+++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs
@@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
-using osu.Framework.Input.StateChanges;
using osu.Game.Input.Handlers;
using osu.Game.Replays;
@@ -69,8 +68,6 @@ namespace osu.Game.Rulesets.Replays
return true;
}
- public override List GetPendingInputs() => new List();
-
private const double sixty_frame_time = 1000.0 / 60;
protected virtual double AllowedImportantTimeSpan => sixty_frame_time * 1.2;
diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
index eb49638d59..f1cdfd93c8 100644
--- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
@@ -7,6 +7,7 @@ using System.Diagnostics;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
+using osu.Framework.Utils;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@@ -16,8 +17,6 @@ namespace osu.Game.Rulesets.Scoring
{
public class ScoreProcessor : JudgementProcessor
{
- private const double base_portion = 0.3;
- private const double combo_portion = 0.7;
private const double max_score = 1000000;
///
@@ -55,8 +54,20 @@ namespace osu.Game.Rulesets.Scoring
///
public readonly Bindable Mode = new Bindable();
- private double maxHighestCombo;
+ ///
+ /// The default portion of awarded for hitting s accurately. Defaults to 30%.
+ ///
+ protected virtual double DefaultAccuracyPortion => 0.3;
+ ///
+ /// The default portion of awarded for achieving a high combo. Default to 70%.
+ ///
+ protected virtual double DefaultComboPortion => 0.7;
+
+ private readonly double accuracyPortion;
+ private readonly double comboPortion;
+
+ private double maxHighestCombo;
private double maxBaseScore;
private double rollingMaxBaseScore;
private double baseScore;
@@ -69,7 +80,11 @@ namespace osu.Game.Rulesets.Scoring
public ScoreProcessor()
{
- Debug.Assert(base_portion + combo_portion == 1.0);
+ accuracyPortion = DefaultAccuracyPortion;
+ comboPortion = DefaultComboPortion;
+
+ if (!Precision.AlmostEquals(1.0, accuracyPortion + comboPortion))
+ throw new InvalidOperationException($"{nameof(DefaultAccuracyPortion)} + {nameof(DefaultComboPortion)} must equal 1.");
Combo.ValueChanged += combo => HighestCombo.Value = Math.Max(HighestCombo.Value, combo.NewValue);
Accuracy.ValueChanged += accuracy =>
@@ -189,7 +204,10 @@ namespace osu.Game.Rulesets.Scoring
{
default:
case ScoringMode.Standardised:
- return (max_score * (base_portion * baseScore / maxBaseScore + combo_portion * HighestCombo.Value / maxHighestCombo) + bonusScore) * scoreMultiplier;
+ double accuracyScore = accuracyPortion * baseScore / maxBaseScore;
+ double comboScore = comboPortion * HighestCombo.Value / maxHighestCombo;
+
+ return (max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier;
case ScoringMode.Classic:
// should emulate osu-stable's scoring as closely as we can (https://osu.ppy.sh/help/wiki/Score/ScoreV1)
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/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
index 980a127cf4..ef41c5be3d 100644
--- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
+++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
@@ -21,12 +21,10 @@ namespace osu.Game.Screens.Backgrounds
private int currentDisplay;
private const int background_count = 7;
-
- private string backgroundName => $@"Menu/menu-background-{currentDisplay % background_count + 1}";
-
private Bindable user;
private Bindable skin;
private Bindable mode;
+ private Bindable introSequence;
[Resolved]
private IBindable beatmap { get; set; }
@@ -42,11 +40,13 @@ namespace osu.Game.Screens.Backgrounds
user = api.LocalUser.GetBoundCopy();
skin = skinManager.CurrentSkin.GetBoundCopy();
mode = config.GetBindable(OsuSetting.MenuBackgroundSource);
+ introSequence = config.GetBindable(OsuSetting.IntroSequence);
user.ValueChanged += _ => Next();
skin.ValueChanged += _ => Next();
mode.ValueChanged += _ => Next();
beatmap.ValueChanged += _ => Next();
+ introSequence.ValueChanged += _ => Next();
currentDisplay = RNG.Next(0, background_count);
@@ -73,6 +73,18 @@ namespace osu.Game.Screens.Backgrounds
private Background createBackground()
{
Background newBackground;
+ string backgroundName;
+
+ switch (introSequence.Value)
+ {
+ case IntroSequence.Welcome:
+ backgroundName = "Intro/Welcome/menu-background";
+ break;
+
+ default:
+ backgroundName = $@"Menu/menu-background-{currentDisplay % background_count + 1}";
+ break;
+ }
if (user.Value?.IsSupporter ?? false)
{
diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
index c6e228262f..fcff672045 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
@@ -64,6 +64,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
DragBox = CreateDragBox(select),
selectionHandler,
SelectionBlueprints = CreateSelectionBlueprintContainer(),
+ selectionHandler.CreateProxy(),
DragBox.CreateProxy().With(p => p.Depth = float.MinValue)
});
@@ -121,14 +122,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
return e.Button == MouseButton.Left;
}
+ private SelectionBlueprint clickedBlueprint;
+
protected override bool OnClick(ClickEvent e)
{
if (e.Button == MouseButton.Right)
return false;
+ // store for double-click handling
+ clickedBlueprint = selectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered);
+
// Deselection should only occur if no selected blueprints are hovered
// A special case for when a blueprint was selected via this click is added since OnClick() may occur outside the hitobject and should not trigger deselection
- if (endClickSelection() || selectionHandler.SelectedBlueprints.Any(b => b.IsHovered))
+ if (endClickSelection() || clickedBlueprint != null)
return true;
deselectAll();
@@ -140,9 +146,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (e.Button == MouseButton.Right)
return false;
- SelectionBlueprint clickedBlueprint = selectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered);
-
- if (clickedBlueprint == null)
+ // ensure the blueprint which was hovered for the first click is still the hovered blueprint.
+ if (clickedBlueprint == null || selectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != clickedBlueprint)
return false;
editorClock?.SeekTo(clickedBlueprint.HitObject.StartTime);
diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
index 38893f90a8..9700cb8c8e 100644
--- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
@@ -15,6 +15,7 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Input.States;
using osu.Game.Audio;
using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
@@ -35,7 +36,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
public IEnumerable SelectedHitObjects => selectedBlueprints.Select(b => b.HitObject);
- private Drawable outline;
+ private Drawable content;
+
+ private OsuSpriteText selectionDetailsText;
[Resolved(CanBeNull = true)]
protected EditorBeatmap EditorBeatmap { get; private set; }
@@ -55,16 +58,42 @@ namespace osu.Game.Screens.Edit.Compose.Components
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- InternalChild = outline = new Container
+ InternalChild = content = new Container
{
- Masking = true,
- BorderThickness = BORDER_RADIUS,
- BorderColour = colours.Yellow,
- Child = new Box
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.Both,
- AlwaysPresent = true,
- Alpha = 0
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ BorderThickness = BORDER_RADIUS,
+ BorderColour = colours.YellowDark,
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ AlwaysPresent = true,
+ Alpha = 0
+ }
+ },
+ new Container
+ {
+ Name = "info text",
+ AutoSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = colours.YellowDark,
+ RelativeSizeAxes = Axes.Both,
+ },
+ selectionDetailsText = new OsuSpriteText
+ {
+ Padding = new MarginPadding(2),
+ Colour = colours.Gray0,
+ Font = OsuFont.Default.With(size: 11)
+ }
+ }
+ }
}
};
}
@@ -131,9 +160,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
selectedBlueprints.Remove(blueprint);
EditorBeatmap.SelectedHitObjects.Remove(blueprint.HitObject);
- // We don't want to update visibility if > 0, since we may be deselecting blueprints during drag-selection
- if (selectedBlueprints.Count == 0)
- UpdateVisibility();
+ UpdateVisibility();
}
///
@@ -179,7 +206,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
///
internal void UpdateVisibility()
{
- if (selectedBlueprints.Count > 0)
+ int count = selectedBlueprints.Count;
+
+ selectionDetailsText.Text = count > 0 ? count.ToString() : string.Empty;
+
+ if (count > 0)
Show();
else
Hide();
@@ -205,8 +236,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
topLeft -= new Vector2(5);
bottomRight += new Vector2(5);
- outline.Size = bottomRight - topLeft;
- outline.Position = topLeft;
+ content.Size = bottomRight - topLeft;
+ content.Position = topLeft;
}
#endregion
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 9f61589c36..d92f3922c3 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -275,11 +275,22 @@ namespace osu.Game.Screens.Edit
protected override bool OnScroll(ScrollEvent e)
{
- scrollAccumulation += (e.ScrollDelta.X + e.ScrollDelta.Y) * (e.IsPrecise ? 0.1 : 1);
+ const double precision = 1;
- const int precision = 1;
+ double scrollComponent = e.ScrollDelta.X + e.ScrollDelta.Y;
- while (Math.Abs(scrollAccumulation) > precision)
+ double scrollDirection = Math.Sign(scrollComponent);
+
+ // this is a special case to handle the "pivot" scenario.
+ // if we are precise scrolling in one direction then change our mind and scroll backwards,
+ // the existing accumulation should be applied in the inverse direction to maintain responsiveness.
+ if (Math.Sign(scrollAccumulation) != scrollDirection)
+ scrollAccumulation = scrollDirection * (precision - Math.Abs(scrollAccumulation));
+
+ scrollAccumulation += scrollComponent * (e.IsPrecise ? 0.1 : 1);
+
+ // because we are doing snapped seeking, we need to add up precise scrolls until they accumulate to an arbitrary cut-off.
+ while (Math.Abs(scrollAccumulation) >= precision)
{
if (scrollAccumulation > 0)
seek(e, -1);
diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs
index dd934c10cd..d4d0feb813 100644
--- a/osu.Game/Screens/Edit/EditorClock.cs
+++ b/osu.Game/Screens/Edit/EditorClock.cs
@@ -118,9 +118,14 @@ namespace osu.Game.Screens.Edit
seekTime = timingPoint.Time + closestBeat * seekAmount;
+ // limit forward seeking to only up to the next timing point's start time.
+ var nextTimingPoint = ControlPointInfo.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time);
+ if (seekTime > nextTimingPoint?.Time)
+ seekTime = nextTimingPoint.Time;
+
// Due to the rounding above, we may end up on the current beat. This will effectively cause 0 seeking to happen, but we don't want this.
// Instead, we'll go to the next beat in the direction when this is the case
- if (Precision.AlmostEquals(current, seekTime))
+ if (Precision.AlmostEquals(current, seekTime, 0.5f))
{
closestBeat += direction > 0 ? 1 : -1;
seekTime = timingPoint.Time + closestBeat * seekAmount;
@@ -129,10 +134,6 @@ namespace osu.Game.Screens.Edit
if (seekTime < timingPoint.Time && timingPoint != ControlPointInfo.TimingPoints.First())
seekTime = timingPoint.Time;
- var nextTimingPoint = ControlPointInfo.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time);
- if (seekTime > nextTimingPoint?.Time)
- seekTime = nextTimingPoint.Time;
-
// Ensure the sought point is within the boundaries
seekTime = Math.Clamp(seekTime, 0, TrackLength);
SeekTo(seekTime);
diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs
index 22fe0ad816..761f842c22 100644
--- a/osu.Game/Screens/IOsuScreen.cs
+++ b/osu.Game/Screens/IOsuScreen.cs
@@ -56,5 +56,14 @@ namespace osu.Game.Screens
/// Whether mod rate adjustments are allowed to be applied.
///
bool AllowRateAdjustments { get; }
+
+ ///
+ /// Invoked when the back button has been pressed to close any overlays before exiting this .
+ ///
+ ///
+ /// Return true to block this from being exited after closing an overlay.
+ /// Return false if this should continue exiting.
+ ///
+ bool OnBackButton();
}
}
diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs
index 9be74a0fd9..a9ef20436f 100644
--- a/osu.Game/Screens/Menu/IntroTriangles.cs
+++ b/osu.Game/Screens/Menu/IntroTriangles.cs
@@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
-using System.IO;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@@ -12,7 +11,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures;
-using osu.Framework.Graphics.Video;
using osu.Framework.Utils;
using osu.Framework.Timing;
using osu.Game.Graphics;
@@ -88,7 +86,7 @@ namespace osu.Game.Screens.Menu
private RulesetFlow rulesets;
private Container rulesetsScale;
private Container logoContainerSecondary;
- private Drawable lazerLogo;
+ private LazerLogo lazerLogo;
private GlitchingTriangles triangles;
@@ -139,10 +137,10 @@ namespace osu.Game.Screens.Menu
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Child = lazerLogo = new LazerLogo(textures.GetStream("Menu/logo-triangles.mp4"))
+ Child = lazerLogo = new LazerLogo
{
Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
+ Origin = Anchor.Centre
}
},
};
@@ -218,6 +216,9 @@ namespace osu.Game.Screens.Menu
// matching flyte curve y = 0.25x^2 + (max(0, x - 0.7) / 0.3) ^ 5
lazerLogo.FadeIn().ScaleTo(scale_start).Then().Delay(logo_scale_duration * 0.7f).ScaleTo(scale_start - scale_adjust, logo_scale_duration * 0.3f, Easing.InQuint);
+
+ lazerLogo.TransformTo(nameof(LazerLogo.Progress), 1f, logo_scale_duration);
+
logoContainerSecondary.ScaleTo(scale_start).Then().ScaleTo(scale_start - scale_adjust * 0.25f, logo_scale_duration, Easing.InQuad);
}
@@ -259,20 +260,40 @@ namespace osu.Game.Screens.Menu
private class LazerLogo : CompositeDrawable
{
- private readonly Stream videoStream;
+ private LogoAnimation highlight, background;
- public LazerLogo(Stream videoStream)
+ public float Progress
+ {
+ get => background.AnimationProgress;
+ set
+ {
+ background.AnimationProgress = value;
+ highlight.AnimationProgress = value;
+ }
+ }
+
+ public LazerLogo()
{
- this.videoStream = videoStream;
Size = new Vector2(960);
}
[BackgroundDependencyLoader]
- private void load()
+ private void load(TextureStore textures)
{
- InternalChild = new Video(videoStream)
+ InternalChildren = new Drawable[]
{
- RelativeSizeAxes = Axes.Both,
+ highlight = new LogoAnimation
+ {
+ RelativeSizeAxes = Axes.Both,
+ Texture = textures.Get(@"Intro/Triangles/logo-highlight"),
+ Colour = Color4.White,
+ },
+ background = new LogoAnimation
+ {
+ RelativeSizeAxes = Axes.Both,
+ Texture = textures.Get(@"Intro/Triangles/logo-background"),
+ Colour = OsuColour.Gray(0.6f),
+ },
};
}
}
diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs
index 089906c342..f5e4b078da 100644
--- a/osu.Game/Screens/Menu/OsuLogo.cs
+++ b/osu.Game/Screens/Menu/OsuLogo.cs
@@ -330,7 +330,7 @@ namespace osu.Game.Screens.Menu
if (Beatmap.Value.Track.IsRunning)
{
var maxAmplitude = lastBeatIndex >= 0 ? Beatmap.Value.Track.CurrentAmplitudes.Maximum : 0;
- logoAmplitudeContainer.ScaleTo(1 - Math.Max(0, maxAmplitude - scale_adjust_cutoff) * 0.04f, 75, Easing.OutQuint);
+ logoAmplitudeContainer.Scale = new Vector2((float)Interpolation.Damp(logoAmplitudeContainer.Scale.X, 1 - Math.Max(0, maxAmplitude - scale_adjust_cutoff) * 0.04f, 0.9f, Time.Elapsed));
if (maxAmplitude > velocity_adjust_cutoff)
triangles.Velocity = 1 + Math.Max(0, maxAmplitude - velocity_adjust_cutoff) * 50;
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/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs
index 3178e35581..269eab5772 100644
--- a/osu.Game/Screens/Multi/Multiplayer.cs
+++ b/osu.Game/Screens/Multi/Multiplayer.cs
@@ -250,12 +250,6 @@ namespace osu.Game.Screens.Multi
{
roomManager.PartRoom();
- if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen))
- {
- screenStack.Exit();
- return true;
- }
-
waves.Hide();
this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut();
@@ -269,6 +263,20 @@ namespace osu.Game.Screens.Multi
return false;
}
+ public override bool OnBackButton()
+ {
+ if ((screenStack.CurrentScreen as IMultiplayerSubScreen)?.OnBackButton() == true)
+ return true;
+
+ if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen))
+ {
+ screenStack.Exit();
+ return true;
+ }
+
+ return false;
+ }
+
protected override void LogoExiting(OsuLogo logo)
{
base.LogoExiting(logo);
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 ac1f74b6a6..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;
@@ -114,7 +113,7 @@ namespace osu.Game.Screens.Multi
public void JoinRoom(Room room, Action onSuccess = null, Action onError = null)
{
currentJoinRoomRequest?.Cancel();
- currentJoinRoomRequest = new JoinRoomRequest(room, api.LocalUser.Value);
+ currentJoinRoomRequest = new JoinRoomRequest(room);
currentJoinRoomRequest.Success += () =>
{
@@ -139,7 +138,7 @@ namespace osu.Game.Screens.Multi
if (joinedRoom == null)
return;
- api.Queue(new PartRoomRequest(joinedRoom, api.LocalUser.Value));
+ api.Queue(new PartRoomRequest(joinedRoom));
joinedRoom = null;
}
diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs
index 35bb4fa34f..872a1cd39a 100644
--- a/osu.Game/Screens/OsuScreen.cs
+++ b/osu.Game/Screens/OsuScreen.cs
@@ -258,5 +258,7 @@ namespace osu.Game.Screens
/// Note that the instance created may not be the used instance if it matches the BackgroundMode equality clause.
///
protected virtual BackgroundScreen CreateBackground() => null;
+
+ public virtual bool OnBackButton() => false;
}
}
diff --git a/osu.Game/Screens/Play/ComboEffects.cs b/osu.Game/Screens/Play/ComboEffects.cs
index 1c4ac921f0..5bcda50399 100644
--- a/osu.Game/Screens/Play/ComboEffects.cs
+++ b/osu.Game/Screens/Play/ComboEffects.cs
@@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
+using osu.Game.Configuration;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
@@ -16,27 +17,34 @@ namespace osu.Game.Screens.Play
private SkinnableSound comboBreakSample;
+ private Bindable alwaysPlay;
+ private bool firstTime = true;
+
public ComboEffects(ScoreProcessor processor)
{
this.processor = processor;
}
[BackgroundDependencyLoader]
- private void load()
+ private void load(OsuConfigManager config)
{
InternalChild = comboBreakSample = new SkinnableSound(new SampleInfo("combobreak"));
+ alwaysPlay = config.GetBindable(OsuSetting.AlwaysPlayFirstComboBreak);
}
protected override void LoadComplete()
{
base.LoadComplete();
- processor.Combo.BindValueChanged(onComboChange, true);
+ processor.Combo.BindValueChanged(onComboChange);
}
private void onComboChange(ValueChangedEvent combo)
{
- if (combo.NewValue == 0 && combo.OldValue > 20)
+ if (combo.NewValue == 0 && (combo.OldValue > 20 || (alwaysPlay.Value && firstTime)))
+ {
comboBreakSample?.Play();
+ firstTime = false;
+ }
}
}
}
diff --git a/osu.Game/Screens/Play/GameplayMenuOverlay.cs b/osu.Game/Screens/Play/GameplayMenuOverlay.cs
index 6b37135c86..57403a0987 100644
--- a/osu.Game/Screens/Play/GameplayMenuOverlay.cs
+++ b/osu.Game/Screens/Play/GameplayMenuOverlay.cs
@@ -24,7 +24,8 @@ namespace osu.Game.Screens.Play
{
public abstract class GameplayMenuOverlay : OverlayContainer, IKeyBindingHandler
{
- private const int transition_duration = 200;
+ protected const int TRANSITION_DURATION = 200;
+
private const int button_height = 70;
private const float background_alpha = 0.75f;
@@ -156,8 +157,8 @@ namespace osu.Game.Screens.Play
}
}
- protected override void PopIn() => this.FadeIn(transition_duration, Easing.In);
- protected override void PopOut() => this.FadeOut(transition_duration, Easing.In);
+ protected override void PopIn() => this.FadeIn(TRANSITION_DURATION, Easing.In);
+ protected override void PopOut() => this.FadeOut(TRANSITION_DURATION, Easing.In);
// Don't let mouse down events through the overlay or people can click circles while paused.
protected override bool OnMouseDown(MouseDownEvent e) => true;
diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs
index 84dbb35f68..847b8a53cf 100644
--- a/osu.Game/Screens/Play/HUD/FailingLayer.cs
+++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Screens.Play.HUD
private const float max_alpha = 0.4f;
private const int fade_time = 400;
- private const float gradient_size = 0.3f;
+ private const float gradient_size = 0.2f;
///
/// The threshold under which the current player life should be considered low and the layer should start fading in.
@@ -56,16 +56,16 @@ namespace osu.Game.Screens.Play.HUD
new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = ColourInfo.GradientVertical(Color4.White, Color4.White.Opacity(0)),
- Height = gradient_size,
+ Colour = ColourInfo.GradientHorizontal(Color4.White, Color4.White.Opacity(0)),
+ Width = gradient_size,
},
new Box
{
RelativeSizeAxes = Axes.Both,
- Height = gradient_size,
- Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0), Color4.White),
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomLeft,
+ Width = gradient_size,
+ Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0), Color4.White),
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
},
}
},
diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs
index 684834123b..387c0e587b 100644
--- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs
+++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs
@@ -251,6 +251,7 @@ namespace osu.Game.Screens.Play.HUD
switch (action)
{
case GlobalAction.Back:
+ case GlobalAction.PauseGameplay: // in the future this behaviour will differ for replays etc.
if (!pendingAnimation)
BeginConfirm();
return true;
@@ -264,6 +265,7 @@ namespace osu.Game.Screens.Play.HUD
switch (action)
{
case GlobalAction.Back:
+ case GlobalAction.PauseGameplay:
AbortConfirm();
break;
}
diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs
index 6cc6027a03..fa917cda32 100644
--- a/osu.Game/Screens/Play/PauseOverlay.cs
+++ b/osu.Game/Screens/Play/PauseOverlay.cs
@@ -4,7 +4,10 @@
using System;
using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Audio;
using osu.Game.Graphics;
+using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Screens.Play
@@ -13,17 +16,46 @@ namespace osu.Game.Screens.Play
{
public Action OnResume;
+ public override bool IsPresent => base.IsPresent || pauseLoop.IsPlaying;
+
public override string Header => "paused";
public override string Description => "you're not going to do what i think you're going to do, are ya?";
+ private SkinnableSound pauseLoop;
+
protected override Action BackAction => () => InternalButtons.Children.First().Click();
+ private const float minimum_volume = 0.0001f;
+
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AddButton("Continue", colours.Green, () => OnResume?.Invoke());
AddButton("Retry", colours.YellowDark, () => OnRetry?.Invoke());
AddButton("Quit", new Color4(170, 27, 39, 255), () => OnQuit?.Invoke());
+
+ AddInternal(pauseLoop = new SkinnableSound(new SampleInfo("pause-loop"))
+ {
+ Looping = true,
+ });
+
+ // SkinnableSound only plays a sound if its aggregate volume is > 0, so the volume must be turned up before playing it
+ pauseLoop.VolumeTo(minimum_volume);
+ }
+
+ protected override void PopIn()
+ {
+ base.PopIn();
+
+ pauseLoop.VolumeTo(1.0f, TRANSITION_DURATION, Easing.InQuint);
+ pauseLoop.Play();
+ }
+
+ protected override void PopOut()
+ {
+ base.PopOut();
+
+ pauseLoop.VolumeTo(minimum_volume, TRANSITION_DURATION, Easing.OutQuad).Finally(_ => pauseLoop.Stop());
}
}
}
diff --git a/osu.Game/Screens/Play/SongProgressBar.cs b/osu.Game/Screens/Play/SongProgressBar.cs
index 5052b32335..939b5fad1f 100644
--- a/osu.Game/Screens/Play/SongProgressBar.cs
+++ b/osu.Game/Screens/Play/SongProgressBar.cs
@@ -57,6 +57,8 @@ namespace osu.Game.Screens.Play
set => CurrentNumber.Value = value;
}
+ protected override bool AllowKeyboardInputWhenNotHovered => true;
+
public SongProgressBar(float barHeight, float handleBarHeight, Vector2 handleSize)
{
CurrentNumber.MinValue = 0;
diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs
index 8343716e7e..cc732382f4 100644
--- a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs
+++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs
@@ -77,11 +77,10 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
Origin = Anchor.Centre,
BlurSigma = new Vector2(35),
BypassAutoSizeAxes = Axes.Both,
- RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(200),
CacheDrawnFrameBuffer = true,
Blending = BlendingParameters.Additive,
Alpha = 0,
- Size = new Vector2(2f), // increase buffer size to allow for scale
Scale = new Vector2(1.8f),
Children = new[]
{
@@ -122,15 +121,18 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
}
flash.Colour = OsuColour.ForRank(rank);
- flash.FadeIn().Then().FadeOut(1200, Easing.OutQuint);
if (rank >= ScoreRank.S)
rankText.ScaleTo(1.05f).ScaleTo(1, 3000, Easing.OutQuint);
if (rank >= ScoreRank.X)
{
- flash.FadeIn().Then().FadeOut(3000);
- superFlash.FadeIn().Then().FadeOut(800, Easing.OutQuint);
+ flash.FadeOutFromOne(3000);
+ superFlash.FadeOutFromOne(800, Easing.OutQuint);
+ }
+ else
+ {
+ flash.FadeOutFromOne(1200, Easing.OutQuint);
}
}
}
diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs
index 49ce07b708..44458d8c8e 100644
--- a/osu.Game/Screens/Ranking/ResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/ResultsScreen.cs
@@ -194,6 +194,13 @@ namespace osu.Game.Screens.Ranking
}
public override bool OnExiting(IScreen next)
+ {
+ Background.FadeTo(1, 250);
+
+ return base.OnExiting(next);
+ }
+
+ public override bool OnBackButton()
{
if (statisticsPanel.State.Value == Visibility.Visible)
{
@@ -201,9 +208,7 @@ namespace osu.Game.Screens.Ranking
return true;
}
- Background.FadeTo(1, 250);
-
- return base.OnExiting(next);
+ return false;
}
private void addScore(ScoreInfo score)
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/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index e3705b15fa..74a5ee8309 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -599,12 +599,6 @@ namespace osu.Game.Screens.Select
public override bool OnExiting(IScreen next)
{
- if (ModSelect.State.Value == Visibility.Visible)
- {
- ModSelect.Hide();
- return true;
- }
-
if (base.OnExiting(next))
return true;
@@ -620,6 +614,17 @@ namespace osu.Game.Screens.Select
return false;
}
+ public override bool OnBackButton()
+ {
+ if (ModSelect.State.Value == Visibility.Visible)
+ {
+ ModSelect.Hide();
+ return true;
+ }
+
+ return false;
+ }
+
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs
index 6b4af21b37..61d0112c89 100644
--- a/osu.Game/Skinning/DefaultSkin.cs
+++ b/osu.Game/Skinning/DefaultSkin.cs
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
using osuTK.Graphics;
@@ -21,7 +22,7 @@ namespace osu.Game.Skinning
public override Drawable GetDrawableComponent(ISkinComponent component) => null;
- public override Texture GetTexture(string componentName) => null;
+ public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;
public override SampleChannel GetSample(ISampleInfo sampleInfo) => null;
diff --git a/osu.Game/Skinning/IAnimationTimeReference.cs b/osu.Game/Skinning/IAnimationTimeReference.cs
index 4ed5ef64c3..7e52bb8176 100644
--- a/osu.Game/Skinning/IAnimationTimeReference.cs
+++ b/osu.Game/Skinning/IAnimationTimeReference.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Timing;
namespace osu.Game.Skinning
@@ -11,7 +12,7 @@ namespace osu.Game.Skinning
///
///
/// This should not be used to start an animation immediately at the current time.
- /// To do so, use with startAtCurrentTime = true
instead.
+ /// To do so, use with startAtCurrentTime = true
instead.
///
[Cached]
public interface IAnimationTimeReference
diff --git a/osu.Game/Skinning/ISkin.cs b/osu.Game/Skinning/ISkin.cs
index cb2a379b8e..5abd963773 100644
--- a/osu.Game/Skinning/ISkin.cs
+++ b/osu.Game/Skinning/ISkin.cs
@@ -5,6 +5,7 @@ using JetBrains.Annotations;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
@@ -29,7 +30,17 @@ namespace osu.Game.Skinning
/// The requested texture.
/// A matching texture, or null if unavailable.
[CanBeNull]
- Texture GetTexture(string componentName);
+ Texture GetTexture(string componentName) => GetTexture(componentName, default, default);
+
+ ///
+ /// Retrieve a .
+ ///
+ /// The requested texture.
+ /// The texture wrap mode in horizontal direction.
+ /// The texture wrap mode in vertical direction.
+ /// A matching texture, or null if unavailable.
+ [CanBeNull]
+ Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT);
///
/// Retrieve a .
diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index 4b70ccc6ad..3bbeff9918 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -11,6 +11,7 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Game.Audio;
@@ -311,17 +312,17 @@ namespace osu.Game.Skinning
return this.GetAnimation(component.LookupName, false, false);
}
- public override Texture GetTexture(string componentName)
+ public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{
foreach (var name in getFallbackNames(componentName))
{
float ratio = 2;
- var texture = Textures?.Get($"{name}@2x");
+ var texture = Textures?.Get($"{name}@2x", wrapModeS, wrapModeT);
if (texture == null)
{
ratio = 1;
- texture = Textures?.Get(name);
+ texture = Textures?.Get(name, wrapModeS, wrapModeT);
}
if (texture == null)
diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs
index 549571dec4..7cf41ef3c1 100644
--- a/osu.Game/Skinning/LegacySkinExtensions.cs
+++ b/osu.Game/Skinning/LegacySkinExtensions.cs
@@ -6,6 +6,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
@@ -15,6 +16,11 @@ namespace osu.Game.Skinning
{
public static Drawable GetAnimation(this ISkin source, string componentName, bool animatable, bool looping, bool applyConfigFrameRate = false, string animationSeparator = "-",
bool startAtCurrentTime = true, double? frameLength = null)
+ => source.GetAnimation(componentName, default, default, animatable, looping, applyConfigFrameRate, animationSeparator, startAtCurrentTime, frameLength);
+
+ public static Drawable GetAnimation(this ISkin source, string componentName, WrapMode wrapModeS, WrapMode wrapModeT, bool animatable, bool looping, bool applyConfigFrameRate = false,
+ string animationSeparator = "-",
+ bool startAtCurrentTime = true, double? frameLength = null)
{
Texture texture;
@@ -38,7 +44,7 @@ namespace osu.Game.Skinning
}
// if an animation was not allowed or not found, fall back to a sprite retrieval.
- if ((texture = source.GetTexture(componentName)) != null)
+ if ((texture = source.GetTexture(componentName, wrapModeS, wrapModeT)) != null)
return new Sprite { Texture = texture };
return null;
@@ -47,7 +53,7 @@ namespace osu.Game.Skinning
{
for (int i = 0; true; i++)
{
- if ((texture = source.GetTexture($"{componentName}{animationSeparator}{i}")) == null)
+ if ((texture = source.GetTexture($"{componentName}{animationSeparator}{i}", wrapModeS, wrapModeT)) == null)
break;
yield return texture;
diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs
index 94a7a32f05..786056b932 100644
--- a/osu.Game/Skinning/LegacySkinTransformer.cs
+++ b/osu.Game/Skinning/LegacySkinTransformer.cs
@@ -4,6 +4,7 @@
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Legacy;
@@ -27,7 +28,10 @@ namespace osu.Game.Skinning
public abstract Drawable GetDrawableComponent(ISkinComponent component);
- public Texture GetTexture(string componentName) => Source.GetTexture(componentName);
+ public Texture GetTexture(string componentName) => GetTexture(componentName, default, default);
+
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
+ => Source.GetTexture(componentName, wrapModeS, wrapModeT);
public virtual SampleChannel GetSample(ISampleInfo sampleInfo)
{
diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs
index fa4aebd8a5..4b0cf02c0a 100644
--- a/osu.Game/Skinning/Skin.cs
+++ b/osu.Game/Skinning/Skin.cs
@@ -5,6 +5,7 @@ using System;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
@@ -20,7 +21,9 @@ namespace osu.Game.Skinning
public abstract SampleChannel GetSample(ISampleInfo sampleInfo);
- public abstract Texture GetTexture(string componentName);
+ public Texture GetTexture(string componentName) => GetTexture(componentName, default, default);
+
+ public abstract Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT);
public abstract IBindable GetConfig(TLookup lookup);
diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs
index d65c74ef62..e1f713882a 100644
--- a/osu.Game/Skinning/SkinManager.cs
+++ b/osu.Game/Skinning/SkinManager.cs
@@ -13,6 +13,7 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Platform;
@@ -130,7 +131,7 @@ namespace osu.Game.Skinning
public Drawable GetDrawableComponent(ISkinComponent component) => CurrentSkin.Value.GetDrawableComponent(component);
- public Texture GetTexture(string componentName) => CurrentSkin.Value.GetTexture(componentName);
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => CurrentSkin.Value.GetTexture(componentName, wrapModeS, wrapModeT);
public SampleChannel GetSample(ISampleInfo sampleInfo) => CurrentSkin.Value.GetSample(sampleInfo);
diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs
index 1c01bbf1ab..adf62ed452 100644
--- a/osu.Game/Skinning/SkinProvidingContainer.cs
+++ b/osu.Game/Skinning/SkinProvidingContainer.cs
@@ -7,6 +7,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
@@ -47,13 +48,13 @@ namespace osu.Game.Skinning
return fallbackSource?.GetDrawableComponent(component);
}
- public Texture GetTexture(string componentName)
+ public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{
Texture sourceTexture;
- if (AllowTextureLookup(componentName) && (sourceTexture = skin?.GetTexture(componentName)) != null)
+ if (AllowTextureLookup(componentName) && (sourceTexture = skin?.GetTexture(componentName, wrapModeS, wrapModeT)) != null)
return sourceTexture;
- return fallbackSource?.GetTexture(componentName);
+ return fallbackSource?.GetTexture(componentName, wrapModeS, wrapModeT);
}
public SampleChannel GetSample(ISampleInfo sampleInfo)
diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs
index 24d6648273..27f6c37895 100644
--- a/osu.Game/Skinning/SkinnableSound.cs
+++ b/osu.Game/Skinning/SkinnableSound.cs
@@ -12,6 +12,7 @@ using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Transforms;
using osu.Game.Audio;
+using osu.Game.Screens.Play;
namespace osu.Game.Skinning
{
@@ -22,6 +23,13 @@ namespace osu.Game.Skinning
[Resolved]
private ISampleStore samples { get; set; }
+ private bool requestedPlaying;
+
+ public override bool RemoveWhenNotAlive => false;
+ public override bool RemoveCompletedTransforms => false;
+
+ private readonly AudioContainer samplesContainer;
+
public SkinnableSound(ISampleInfo hitSamples)
: this(new[] { hitSamples })
{
@@ -33,9 +41,99 @@ namespace osu.Game.Skinning
InternalChild = samplesContainer = new AudioContainer();
}
+ private Bindable gameplayClockPaused;
+
+ [BackgroundDependencyLoader(true)]
+ private void load(GameplayClock gameplayClock)
+ {
+ // if in a gameplay context, pause sample playback when gameplay is paused.
+ gameplayClockPaused = gameplayClock?.IsPaused.GetBoundCopy();
+ gameplayClockPaused?.BindValueChanged(paused =>
+ {
+ if (requestedPlaying)
+ {
+ if (paused.NewValue)
+ stop();
+ // it's not easy to know if a sample has finished playing (to end).
+ // to keep things simple only resume playing looping samples.
+ else if (Looping)
+ play();
+ }
+ });
+ }
+
private bool looping;
- private readonly AudioContainer samplesContainer;
+ public bool Looping
+ {
+ get => looping;
+ set
+ {
+ if (value == looping) return;
+
+ looping = value;
+
+ samplesContainer.ForEach(c => c.Looping = looping);
+ }
+ }
+
+ public void Play()
+ {
+ requestedPlaying = true;
+ play();
+ }
+
+ private void play()
+ {
+ samplesContainer.ForEach(c =>
+ {
+ if (c.AggregateVolume.Value > 0)
+ c.Play();
+ });
+ }
+
+ public void Stop()
+ {
+ requestedPlaying = false;
+ stop();
+ }
+
+ private void stop()
+ {
+ samplesContainer.ForEach(c => c.Stop());
+ }
+
+ protected override void SkinChanged(ISkinSource skin, bool allowFallback)
+ {
+ var channels = hitSamples.Select(s =>
+ {
+ var ch = skin.GetSample(s);
+
+ if (ch == null && allowFallback)
+ {
+ foreach (var lookup in s.LookupNames)
+ {
+ if ((ch = samples.Get($"Gameplay/{lookup}")) != null)
+ break;
+ }
+ }
+
+ if (ch != null)
+ {
+ ch.Looping = looping;
+ ch.Volume.Value = s.Volume / 100.0;
+ }
+
+ return ch;
+ }).Where(c => c != null);
+
+ samplesContainer.ChildrenEnumerable = channels.Select(c => new DrawableSample(c));
+
+ if (requestedPlaying)
+ Play();
+ }
+
+ #region Re-expose AudioContainer
public BindableNumber Volume => samplesContainer.Volume;
@@ -45,6 +143,8 @@ namespace osu.Game.Skinning
public BindableNumber Tempo => samplesContainer.Tempo;
+ public bool IsPlaying => samplesContainer.Any(s => s.Playing);
+
///
/// Smoothly adjusts over time.
///
@@ -73,54 +173,6 @@ namespace osu.Game.Skinning
public TransformSequence TempoTo(double newTempo, double duration = 0, Easing easing = Easing.None) =>
samplesContainer.TempoTo(newTempo, duration, easing);
- public bool Looping
- {
- get => looping;
- set
- {
- if (value == looping) return;
-
- looping = value;
-
- samplesContainer.ForEach(c => c.Looping = looping);
- }
- }
-
- public void Play() => samplesContainer.ForEach(c =>
- {
- if (c.AggregateVolume.Value > 0)
- c.Play();
- });
-
- public void Stop() => samplesContainer.ForEach(c => c.Stop());
-
- public override bool IsPresent => Scheduler.HasPendingTasks;
-
- protected override void SkinChanged(ISkinSource skin, bool allowFallback)
- {
- var channels = hitSamples.Select(s =>
- {
- var ch = skin.GetSample(s);
-
- if (ch == null && allowFallback)
- {
- foreach (var lookup in s.LookupNames)
- {
- if ((ch = samples.Get($"Gameplay/{lookup}")) != null)
- break;
- }
- }
-
- if (ch != null)
- {
- ch.Looping = looping;
- ch.Volume.Value = s.Volume / 100.0;
- }
-
- return ch;
- }).Where(c => c != null);
-
- samplesContainer.ChildrenEnumerable = channels.Select(c => new DrawableSample(c));
- }
+ #endregion
}
}
diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs
index a85936edf7..4ea582ca4a 100644
--- a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs
+++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs
@@ -55,10 +55,11 @@ namespace osu.Game.Storyboards.Drawables
if (video == null) return;
- video.PlaybackPosition = Clock.CurrentTime - Video.StartTime;
-
- using (video.BeginAbsoluteSequence(0))
+ using (video.BeginAbsoluteSequence(Video.StartTime))
+ {
+ Schedule(() => video.PlaybackPosition = Time.Current - Video.StartTime);
video.FadeIn(500);
+ }
}
}
}
diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs
index cb9ed40b00..866fc215d6 100644
--- a/osu.Game/Tests/Visual/OsuTestScene.cs
+++ b/osu.Game/Tests/Visual/OsuTestScene.cs
@@ -305,8 +305,10 @@ namespace osu.Game.Tests.Visual
{
double refTime = referenceClock.CurrentTime;
- if (lastReferenceTime.HasValue)
- accumulated += (refTime - lastReferenceTime.Value) * Rate;
+ double? lastRefTime = lastReferenceTime;
+
+ if (lastRefTime != null)
+ accumulated += (refTime - lastRefTime.Value) * Rate;
lastReferenceTime = refTime;
}
diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs
index ea7cdaaac6..81c13112d0 100644
--- a/osu.Game/Tests/Visual/SkinnableTestScene.cs
+++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs
@@ -9,6 +9,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
@@ -157,7 +158,7 @@ namespace osu.Game.Tests.Visual
this.extrapolateAnimations = extrapolateAnimations;
}
- public override Texture GetTexture(string componentName)
+ public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{
// extrapolate frames to test longer animations
if (extrapolateAnimations)
@@ -165,10 +166,10 @@ namespace osu.Game.Tests.Visual
var match = Regex.Match(componentName, "-([0-9]*)");
if (match.Length > 0 && int.TryParse(match.Groups[1].Value, out var number) && number < 60)
- return base.GetTexture(componentName.Replace($"-{number}", $"-{number % 2}"));
+ return base.GetTexture(componentName.Replace($"-{number}", $"-{number % 2}"), wrapModeS, wrapModeT);
}
- return base.GetTexture(componentName);
+ return base.GetTexture(componentName, wrapModeS, wrapModeT);
}
}
}
diff --git a/osu.Game/Users/ExtendedUserPanel.cs b/osu.Game/Users/ExtendedUserPanel.cs
new file mode 100644
index 0000000000..2604815751
--- /dev/null
+++ b/osu.Game/Users/ExtendedUserPanel.cs
@@ -0,0 +1,148 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osuTK;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Users.Drawables;
+using osu.Framework.Input.Events;
+
+namespace osu.Game.Users
+{
+ public abstract class ExtendedUserPanel : UserPanel
+ {
+ public readonly Bindable Status = new Bindable();
+
+ public readonly IBindable Activity = new Bindable();
+
+ protected TextFlowContainer LastVisitMessage { get; private set; }
+
+ private SpriteIcon statusIcon;
+ private OsuSpriteText statusMessage;
+
+ protected ExtendedUserPanel(User user)
+ : base(user)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ BorderColour = ColourProvider?.Light1 ?? Colours.GreyVioletLighter;
+
+ Status.ValueChanged += status => displayStatus(status.NewValue, Activity.Value);
+ Activity.ValueChanged += activity => displayStatus(Status.Value, activity.NewValue);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ Status.TriggerChange();
+
+ // Colour should be applied immediately on first load.
+ statusIcon.FinishTransforms();
+ }
+
+ protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar
+ {
+ User = User,
+ OpenOnClick = { Value = false }
+ };
+
+ protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.Country)
+ {
+ Size = new Vector2(39, 26)
+ };
+
+ protected SpriteIcon CreateStatusIcon() => statusIcon = new SpriteIcon
+ {
+ Icon = FontAwesome.Regular.Circle,
+ Size = new Vector2(25)
+ };
+
+ protected FillFlowContainer CreateStatusMessage(bool rightAlignedChildren)
+ {
+ var statusContainer = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical
+ };
+
+ var alignment = rightAlignedChildren ? Anchor.CentreRight : Anchor.CentreLeft;
+
+ statusContainer.Add(LastVisitMessage = new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold)).With(text =>
+ {
+ text.Anchor = alignment;
+ text.Origin = alignment;
+ text.AutoSizeAxes = Axes.Both;
+ text.Alpha = 0;
+
+ if (User.LastVisit.HasValue)
+ {
+ text.AddText(@"Last seen ");
+ text.AddText(new DrawableDate(User.LastVisit.Value, italic: false)
+ {
+ Shadow = false
+ });
+ }
+ }));
+
+ statusContainer.Add(statusMessage = new OsuSpriteText
+ {
+ Anchor = alignment,
+ Origin = alignment,
+ Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold)
+ });
+
+ return statusContainer;
+ }
+
+ private void displayStatus(UserStatus status, UserActivity activity = null)
+ {
+ if (status != null)
+ {
+ LastVisitMessage.FadeTo(status is UserStatusOffline && User.LastVisit.HasValue ? 1 : 0);
+
+ // Set status message based on activity (if we have one) and status is not offline
+ if (activity != null && !(status is UserStatusOffline))
+ {
+ statusMessage.Text = activity.Status;
+ statusIcon.FadeColour(activity.GetAppropriateColour(Colours), 500, Easing.OutQuint);
+ return;
+ }
+
+ // Otherwise use only status
+ statusMessage.Text = status.Message;
+ statusIcon.FadeColour(status.GetAppropriateColour(Colours), 500, Easing.OutQuint);
+
+ return;
+ }
+
+ // Fallback to web status if local one is null
+ if (User.IsOnline)
+ {
+ Status.Value = new UserStatusOnline();
+ return;
+ }
+
+ Status.Value = new UserStatusOffline();
+ }
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ BorderThickness = 2;
+ return base.OnHover(e);
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ BorderThickness = 0;
+ base.OnHoverLost(e);
+ }
+ }
+}
diff --git a/osu.Game/Users/UserBrickPanel.cs b/osu.Game/Users/UserBrickPanel.cs
new file mode 100644
index 0000000000..9ca7768187
--- /dev/null
+++ b/osu.Game/Users/UserBrickPanel.cs
@@ -0,0 +1,65 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osuTK;
+
+namespace osu.Game.Users
+{
+ public class UserBrickPanel : UserPanel
+ {
+ public UserBrickPanel(User user)
+ : base(user)
+ {
+ AutoSizeAxes = Axes.Both;
+ CornerRadius = 6;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Background.FadeTo(0.2f);
+ }
+
+ protected override Drawable CreateLayout() => new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(5, 0),
+ Margin = new MarginPadding
+ {
+ Horizontal = 10,
+ Vertical = 3,
+ },
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ new CircularContainer
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Masking = true,
+ Width = 4,
+ Height = 13,
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = string.IsNullOrEmpty(User.Colour) ? Color4Extensions.FromHex("0087ca") : Color4Extensions.FromHex(User.Colour)
+ }
+ },
+ CreateUsername().With(u =>
+ {
+ u.Anchor = Anchor.CentreLeft;
+ u.Origin = Anchor.CentreLeft;
+ u.Font = OsuFont.GetFont(size: 13, weight: FontWeight.Bold);
+ })
+ }
+ };
+ }
+}
diff --git a/osu.Game/Users/UserGridPanel.cs b/osu.Game/Users/UserGridPanel.cs
index e62a834d6d..44dcbc305d 100644
--- a/osu.Game/Users/UserGridPanel.cs
+++ b/osu.Game/Users/UserGridPanel.cs
@@ -9,7 +9,7 @@ using osuTK;
namespace osu.Game.Users
{
- public class UserGridPanel : UserPanel
+ public class UserGridPanel : ExtendedUserPanel
{
private const int margin = 10;
diff --git a/osu.Game/Users/UserListPanel.cs b/osu.Game/Users/UserListPanel.cs
index 1c3ae20577..9c95eff739 100644
--- a/osu.Game/Users/UserListPanel.cs
+++ b/osu.Game/Users/UserListPanel.cs
@@ -12,7 +12,7 @@ using osu.Game.Overlays.Profile.Header.Components;
namespace osu.Game.Users
{
- public class UserListPanel : UserPanel
+ public class UserListPanel : ExtendedUserPanel
{
public UserListPanel(User user)
: base(user)
diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs
index 6f59f9e443..94c0c31cfc 100644
--- a/osu.Game/Users/UserPanel.cs
+++ b/osu.Game/Users/UserPanel.cs
@@ -2,9 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using osuTK;
using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -14,11 +12,8 @@ using osu.Game.Overlays;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
using osu.Framework.Graphics.Cursor;
-using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Containers;
-using osu.Game.Users.Drawables;
using JetBrains.Annotations;
-using osu.Framework.Input.Events;
namespace osu.Game.Users
{
@@ -26,21 +21,12 @@ namespace osu.Game.Users
{
public readonly User User;
- public readonly Bindable Status = new Bindable();
-
- public readonly IBindable Activity = new Bindable();
-
public new Action Action;
protected Action ViewProfile { get; private set; }
protected DelayedLoadUnloadWrapper Background { get; private set; }
- protected TextFlowContainer LastVisitMessage { get; private set; }
-
- private SpriteIcon statusIcon;
- private OsuSpriteText statusMessage;
-
protected UserPanel(User user)
{
if (user == null)
@@ -53,23 +39,22 @@ namespace osu.Game.Users
private UserProfileOverlay profileOverlay { get; set; }
[Resolved(canBeNull: true)]
- private OverlayColourProvider colourProvider { get; set; }
+ protected OverlayColourProvider ColourProvider { get; private set; }
[Resolved]
- private OsuColour colours { get; set; }
+ protected OsuColour Colours { get; private set; }
[BackgroundDependencyLoader]
private void load()
{
Masking = true;
- BorderColour = colourProvider?.Light1 ?? colours.GreyVioletLighter;
AddRange(new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = colourProvider?.Background5 ?? colours.Gray1
+ Colour = ColourProvider?.Background5 ?? Colours.Gray1
},
Background = new DelayedLoadUnloadWrapper(() => new UserCoverBackground
{
@@ -86,9 +71,6 @@ namespace osu.Game.Users
CreateLayout()
});
- Status.ValueChanged += status => displayStatus(status.NewValue, Activity.Value);
- Activity.ValueChanged += activity => displayStatus(Status.Value, activity.NewValue);
-
base.Action = ViewProfile = () =>
{
Action?.Invoke();
@@ -96,41 +78,9 @@ namespace osu.Game.Users
};
}
- protected override void LoadComplete()
- {
- base.LoadComplete();
- Status.TriggerChange();
-
- // Colour should be applied immediately on first load.
- statusIcon.FinishTransforms();
- }
-
- protected override bool OnHover(HoverEvent e)
- {
- BorderThickness = 2;
- return base.OnHover(e);
- }
-
- protected override void OnHoverLost(HoverLostEvent e)
- {
- BorderThickness = 0;
- base.OnHoverLost(e);
- }
-
[NotNull]
protected abstract Drawable CreateLayout();
- protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar
- {
- User = User,
- OpenOnClick = { Value = false }
- };
-
- protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.Country)
- {
- Size = new Vector2(39, 26)
- };
-
protected OsuSpriteText CreateUsername() => new OsuSpriteText
{
Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold),
@@ -138,80 +88,6 @@ namespace osu.Game.Users
Text = User.Username,
};
- protected SpriteIcon CreateStatusIcon() => statusIcon = new SpriteIcon
- {
- Icon = FontAwesome.Regular.Circle,
- Size = new Vector2(25)
- };
-
- protected FillFlowContainer CreateStatusMessage(bool rightAlignedChildren)
- {
- var statusContainer = new FillFlowContainer
- {
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Vertical
- };
-
- var alignment = rightAlignedChildren ? Anchor.CentreRight : Anchor.CentreLeft;
-
- statusContainer.Add(LastVisitMessage = new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold)).With(text =>
- {
- text.Anchor = alignment;
- text.Origin = alignment;
- text.AutoSizeAxes = Axes.Both;
- text.Alpha = 0;
-
- if (User.LastVisit.HasValue)
- {
- text.AddText(@"Last seen ");
- text.AddText(new DrawableDate(User.LastVisit.Value, italic: false)
- {
- Shadow = false
- });
- }
- }));
-
- statusContainer.Add(statusMessage = new OsuSpriteText
- {
- Anchor = alignment,
- Origin = alignment,
- Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold)
- });
-
- return statusContainer;
- }
-
- private void displayStatus(UserStatus status, UserActivity activity = null)
- {
- if (status != null)
- {
- LastVisitMessage.FadeTo(status is UserStatusOffline && User.LastVisit.HasValue ? 1 : 0);
-
- // Set status message based on activity (if we have one) and status is not offline
- if (activity != null && !(status is UserStatusOffline))
- {
- statusMessage.Text = activity.Status;
- statusIcon.FadeColour(activity.GetAppropriateColour(colours), 500, Easing.OutQuint);
- return;
- }
-
- // Otherwise use only status
- statusMessage.Text = status.Message;
- statusIcon.FadeColour(status.GetAppropriateColour(colours), 500, Easing.OutQuint);
-
- return;
- }
-
- // Fallback to web status if local one is null
- if (User.IsOnline)
- {
- Status.Value = new UserStatusOnline();
- return;
- }
-
- Status.Value = new UserStatusOffline();
- }
-
public MenuItem[] ContextMenuItems => new MenuItem[]
{
new OsuMenuItem("View Profile", MenuItemType.Highlighted, ViewProfile),
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 4295e02d24..5ac54a853f 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -24,10 +24,10 @@
-
-
-
-
+
+
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 3627cc032e..8b2d1346be 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,8 +70,8 @@
-
-
+
+
@@ -80,8 +80,8 @@
-
-
+
+
diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings
index 85d5fce29a..29ca385275 100644
--- a/osu.sln.DotSettings
+++ b/osu.sln.DotSettings
@@ -783,6 +783,7 @@ See the LICENCE file in the repository root for full licence text.
True
True
True
+ TestFolder
True
True
o!f – Object Initializer: Anchor&Origin