diff --git a/osu.Android.props b/osu.Android.props
index 71d4e5aacf..13b4b6ebbb 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/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/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs
index 0b3d1d23e0..4ecfb7b16d 100644
--- a/osu.Game.Rulesets.Catch/Objects/Banana.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs
@@ -1,6 +1,8 @@
// 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.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Judgements;
@@ -8,8 +10,27 @@ namespace osu.Game.Rulesets.Catch.Objects
{
public class Banana : Fruit
{
+ ///
+ /// Index of banana in current shower.
+ ///
+ public int BananaIndex;
+
public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana;
public override Judgement CreateJudgement() => new CatchBananaJudgement();
+
+ private static readonly List samples = new List { new BananaHitSampleInfo() };
+
+ public Banana()
+ {
+ Samples = samples;
+ }
+
+ private class BananaHitSampleInfo : HitSampleInfo
+ {
+ private static string[] lookupNames { get; } = { "metronomelow", "catch-banana" };
+
+ public override IEnumerable LookupNames => lookupNames;
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
index 04a995c77e..89c51459a6 100644
--- a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
+++ b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
@@ -30,15 +30,21 @@ namespace osu.Game.Rulesets.Catch.Objects
if (spacing <= 0)
return;
- for (double i = StartTime; i <= EndTime; i += spacing)
+ double time = StartTime;
+ int i = 0;
+
+ while (time <= EndTime)
{
cancellationToken.ThrowIfCancellationRequested();
AddNested(new Banana
{
- Samples = Samples,
- StartTime = i
+ StartTime = time,
+ BananaIndex = i,
});
+
+ time += spacing;
+ i++;
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
index 01b76ceed9..a865984d45 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs
@@ -40,6 +40,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
float getRandomAngle() => 180 * (RNG.NextSingle() * 2 - 1);
}
+ public override void PlaySamples()
+ {
+ base.PlaySamples();
+ if (Samples != null)
+ Samples.Frequency.Value = 0.77f + ((Banana)HitObject).BananaIndex * 0.006f;
+ }
+
private Color4 getBananaColour()
{
switch (RNG.Next(0, 3))
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.Mania.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs
index a692c0b697..0c56f7bcf4 100644
--- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs
@@ -1,23 +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 System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
-using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Pooling;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Mania.UI;
-using osu.Game.Skinning;
+using osu.Game.Rulesets.Objects;
using osuTK;
-using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
[TestFixture]
public class TestSceneHitExplosion : ManiaSkinnableTestScene
{
+ private readonly List> hitExplosionPools = new List>();
+
public TestSceneHitExplosion()
{
int runcount = 0;
@@ -29,28 +33,40 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
if (runcount % 15 > 12)
return;
- CreatedDrawables.OfType().ForEach(c =>
+ int poolIndex = 0;
+
+ foreach (var c in CreatedDrawables.OfType())
{
- c.Add(new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, 0),
- _ => new DefaultHitExplosion((runcount / 15) % 2 == 0 ? new Color4(94, 0, 57, 255) : new Color4(6, 84, 0, 255), runcount % 6 != 0)
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- }));
- });
+ c.Add(hitExplosionPools[poolIndex].Get(e =>
+ {
+ e.Apply(new JudgementResult(new HitObject(), runcount % 6 == 0 ? new HoldNoteTickJudgement() : new ManiaJudgement()));
+
+ e.Anchor = Anchor.Centre;
+ e.Origin = Anchor.Centre;
+ }));
+
+ poolIndex++;
+ }
}, 100);
}
[BackgroundDependencyLoader]
private void load()
{
- SetContents(() => new ColumnTestContainer(0, ManiaAction.Key1)
+ SetContents(() =>
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativePositionAxes = Axes.Y,
- Y = -0.25f,
- Size = new Vector2(Column.COLUMN_WIDTH, DefaultNotePiece.NOTE_HEIGHT),
+ var pool = new DrawablePool(5);
+ hitExplosionPools.Add(pool);
+
+ return new ColumnTestContainer(0, ManiaAction.Key1)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativePositionAxes = Axes.Y,
+ Y = -0.25f,
+ Size = new Vector2(Column.COLUMN_WIDTH, DefaultNotePiece.NOTE_HEIGHT),
+ Child = pool
+ };
});
}
}
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/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/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 ba84c21845..4b2f643333 100644
--- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
+++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
@@ -7,9 +7,9 @@ namespace osu.Game.Rulesets.Mania.Scoring
{
internal class ManiaScoreProcessor : ScoreProcessor
{
- protected override double DefaultAccuracyPortion => 0.8;
+ protected override double DefaultAccuracyPortion => 0.95;
- protected override double DefaultComboPortion => 0.2;
+ protected override double DefaultComboPortion => 0.05;
public override HitWindows CreateHitWindows() => new ManiaHitWindows();
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs
index bc93bb2615..12747924de 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs
@@ -6,13 +6,15 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Mania.Skinning
{
- public class LegacyHitExplosion : LegacyManiaColumnElement
+ public class LegacyHitExplosion : LegacyManiaColumnElement, IHitExplosion
{
private readonly IBindable direction = new Bindable();
@@ -62,9 +64,9 @@ namespace osu.Game.Rulesets.Mania.Skinning
explosion.Anchor = direction.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
}
- protected override void LoadComplete()
+ public void Animate(JudgementResult result)
{
- base.LoadComplete();
+ (explosion as IFramedAnimation)?.GotoFrame(0);
explosion?.FadeInFromZero(80)
.Then().FadeOut(120);
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index 642353bd0b..255ce4c064 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -9,9 +9,9 @@ using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Graphics.Pooling;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Judgements;
-using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
@@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Mania.UI
public readonly Bindable Action = new Bindable();
public readonly ColumnHitObjectArea HitObjectArea;
-
internal readonly Container TopLevelContainer;
+ private readonly DrawablePool hitExplosionPool;
public Container UnderlayElements => HitObjectArea.UnderlayElements;
@@ -53,6 +53,7 @@ namespace osu.Game.Rulesets.Mania.UI
InternalChildren = new[]
{
+ hitExplosionPool = new DrawablePool(5),
// 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 },
@@ -108,15 +109,7 @@ namespace osu.Game.Rulesets.Mania.UI
if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
- var explosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, Index), _ =>
- new DefaultHitExplosion(judgedObject.AccentColour.Value, judgedObject is DrawableHoldNoteTick))
- {
- RelativeSizeAxes = Axes.Both
- };
-
- HitObjectArea.Explosions.Add(explosion);
-
- explosion.Delay(200).Expire(true);
+ HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result)));
}
public bool OnPressed(ManiaAction action)
diff --git a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
index 7a047ed121..225269cf48 100644
--- a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs
@@ -8,6 +8,8 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Utils;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
@@ -15,35 +17,36 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.UI
{
- public class DefaultHitExplosion : CompositeDrawable
+ public class DefaultHitExplosion : CompositeDrawable, IHitExplosion
{
+ private const float default_large_faint_size = 0.8f;
+
public override bool RemoveWhenNotAlive => true;
+ [Resolved]
+ private Column column { get; set; }
+
private readonly IBindable direction = new Bindable();
- private readonly CircularContainer largeFaint;
- private readonly CircularContainer mainGlow1;
+ private CircularContainer largeFaint;
+ private CircularContainer mainGlow1;
- public DefaultHitExplosion(Color4 objectColour, bool isSmall = false)
+ public DefaultHitExplosion()
{
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.X;
Height = DefaultNotePiece.NOTE_HEIGHT;
+ }
- // scale roughly in-line with visual appearance of notes
- Scale = new Vector2(1f, 0.6f);
-
- if (isSmall)
- Scale *= 0.5f;
-
+ [BackgroundDependencyLoader]
+ private void load(IScrollingInfo scrollingInfo)
+ {
const float angle_variangle = 15; // should be less than 45
-
const float roundness = 80;
-
const float initial_height = 10;
- var colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1);
+ var colour = Interpolation.ValueAt(0.4f, column.AccentColour, Color4.White, 0, 1);
InternalChildren = new Drawable[]
{
@@ -54,12 +57,12 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Both,
Masking = true,
// we want our size to be very small so the glow dominates it.
- Size = new Vector2(0.8f),
+ Size = new Vector2(default_large_faint_size),
Blending = BlendingParameters.Additive,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
- Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f),
+ Colour = Interpolation.ValueAt(0.1f, column.AccentColour, Color4.White, 0, 1).Opacity(0.3f),
Roundness = 160,
Radius = 200,
},
@@ -74,7 +77,7 @@ namespace osu.Game.Rulesets.Mania.UI
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
- Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1),
+ Colour = Interpolation.ValueAt(0.6f, column.AccentColour, Color4.White, 0, 1),
Roundness = 20,
Radius = 50,
},
@@ -114,30 +117,11 @@ namespace osu.Game.Rulesets.Mania.UI
},
}
};
- }
- [BackgroundDependencyLoader]
- private void load(IScrollingInfo scrollingInfo)
- {
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
}
- protected override void LoadComplete()
- {
- const double duration = 200;
-
- base.LoadComplete();
-
- largeFaint
- .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint)
- .FadeOut(duration * 2);
-
- mainGlow1.ScaleTo(1.4f, duration, Easing.OutQuint);
-
- this.FadeOut(duration, Easing.Out);
- }
-
private void onDirectionChanged(ValueChangedEvent direction)
{
if (direction.NewValue == ScrollingDirection.Up)
@@ -151,5 +135,29 @@ namespace osu.Game.Rulesets.Mania.UI
Y = -DefaultNotePiece.NOTE_HEIGHT / 2;
}
}
+
+ public void Animate(JudgementResult result)
+ {
+ // scale roughly in-line with visual appearance of notes
+ Vector2 scale = new Vector2(1, 0.6f);
+
+ if (result.Judgement is HoldNoteTickJudgement)
+ scale *= 0.5f;
+
+ this.ScaleTo(scale);
+
+ largeFaint
+ .ResizeTo(default_large_faint_size)
+ .Then()
+ .ResizeTo(default_large_faint_size * new Vector2(5, 1), PoolableHitExplosion.DURATION, Easing.OutQuint)
+ .FadeOut(PoolableHitExplosion.DURATION * 2);
+
+ mainGlow1
+ .ScaleTo(1)
+ .Then()
+ .ScaleTo(1.4f, PoolableHitExplosion.DURATION, Easing.OutQuint);
+
+ this.FadeOutFromOne(PoolableHitExplosion.DURATION, Easing.Out);
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
index 8797f014df..d99f6cb8d3 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
@@ -15,6 +15,10 @@ namespace osu.Game.Rulesets.Mania.UI
{
}
+ public DrawableManiaJudgement()
+ {
+ }
+
[BackgroundDependencyLoader]
private void load()
{
diff --git a/osu.Game.Rulesets.Mania/UI/IHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/IHitExplosion.cs
new file mode 100644
index 0000000000..3252dcc276
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/UI/IHitExplosion.cs
@@ -0,0 +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 osu.Game.Rulesets.Judgements;
+
+namespace osu.Game.Rulesets.Mania.UI
+{
+ ///
+ /// Common interface for all hit explosion bodies.
+ ///
+ public interface IHitExplosion
+ {
+ ///
+ /// Begins animating this .
+ ///
+ /// The type of that caused this explosion.
+ void Animate(JudgementResult result);
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs
new file mode 100644
index 0000000000..64b7d7d550
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs
@@ -0,0 +1,51 @@
+// 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.Pooling;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Mania.UI
+{
+ public class PoolableHitExplosion : PoolableDrawable
+ {
+ public const double DURATION = 200;
+
+ public JudgementResult Result { get; private set; }
+
+ [Resolved]
+ private Column column { get; set; }
+
+ private SkinnableDrawable skinnableExplosion;
+
+ public PoolableHitExplosion()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChild = skinnableExplosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, column.Index), _ => new DefaultHitExplosion())
+ {
+ RelativeSizeAxes = Axes.Both
+ };
+ }
+
+ public void Apply(JudgementResult result)
+ {
+ Result = result;
+ }
+
+ protected override void PrepareForUse()
+ {
+ base.PrepareForUse();
+
+ (skinnableExplosion?.Drawable as IHitExplosion)?.Animate(Result);
+
+ this.Delay(DURATION).Then().Expire();
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs
index faa04dea97..36780b0f80 100644
--- a/osu.Game.Rulesets.Mania/UI/Stage.cs
+++ b/osu.Game.Rulesets.Mania/UI/Stage.cs
@@ -6,6 +6,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
@@ -33,8 +34,8 @@ namespace osu.Game.Rulesets.Mania.UI
public IReadOnlyList Columns => columnFlow.Children;
private readonly FillFlowContainer columnFlow;
- public Container Judgements => judgements;
private readonly JudgementContainer judgements;
+ private readonly DrawablePool judgementPool;
private readonly Drawable barLineContainer;
private readonly Container topLevelContainer;
@@ -63,6 +64,7 @@ namespace osu.Game.Rulesets.Mania.UI
InternalChildren = new Drawable[]
{
+ judgementPool = new DrawablePool(2),
new Container
{
Anchor = Anchor.TopCentre,
@@ -208,12 +210,14 @@ namespace osu.Game.Rulesets.Mania.UI
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
- judgements.Clear();
- judgements.Add(new DrawableManiaJudgement(result, judgedObject)
+ judgements.Clear(false);
+ judgements.Add(judgementPool.Get(j =>
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- });
+ j.Apply(result, judgedObject);
+
+ j.Anchor = Anchor.Centre;
+ j.Origin = Anchor.Centre;
+ }));
}
protected override void Update()
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs b/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs
index a0a38fc47b..cad98185ce 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs
@@ -1,12 +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.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
public abstract class OsuSkinnableTestScene : SkinnableTestScene
{
+ private Container content;
+
+ protected override Container Content
+ {
+ get
+ {
+ if (content == null)
+ base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 }));
+
+ return content;
+ }
+ }
+
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-approachcircle.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-approachcircle.png
new file mode 100644
index 0000000000..3811e5050f
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-approachcircle.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-background.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-background.png
new file mode 100644
index 0000000000..d84eab2f15
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-background.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-circle.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-circle.png
new file mode 100644
index 0000000000..4dd4a6d319
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-circle.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-clear.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-clear.png
new file mode 100644
index 0000000000..c66f1c9309
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-clear.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-metre.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-metre.png
new file mode 100644
index 0000000000..33902186d9
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-metre.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-osu.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-osu.png
new file mode 100644
index 0000000000..6882a232e0
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-osu.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-spin.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-spin.png
new file mode 100644
index 0000000000..98a9991c2f
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-spin.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerbonus.wav b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerbonus.wav
new file mode 100644
index 0000000000..5e583e77aa
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerbonus.wav differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerspin.wav b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerspin.wav
new file mode 100644
index 0000000000..bba19381f1
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerspin.wav differ
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/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
index a9404f665a..6a689a1f80 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
@@ -3,7 +3,6 @@
using System.Collections.Generic;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -26,19 +25,6 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestFixture]
public class TestSceneSlider : OsuSkinnableTestScene
{
- private Container content;
-
- protected override Container Content
- {
- get
- {
- if (content == null)
- base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 }));
-
- return content;
- }
- }
-
private int depthIndex;
public TestSceneSlider()
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
index 67afc45e32..b57561f3e1 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs
@@ -4,37 +4,30 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
-using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
- public class TestSceneSpinner : OsuTestScene
+ public class TestSceneSpinner : OsuSkinnableTestScene
{
- private readonly Container content;
- protected override Container Content => content;
-
private int depthIndex;
public TestSceneSpinner()
{
- base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 }));
-
- AddStep("Miss Big", () => testSingle(2));
- AddStep("Miss Medium", () => testSingle(5));
- AddStep("Miss Small", () => testSingle(7));
- AddStep("Hit Big", () => testSingle(2, true));
- AddStep("Hit Medium", () => testSingle(5, true));
- AddStep("Hit Small", () => testSingle(7, true));
+ AddStep("Miss Big", () => SetContents(() => testSingle(2)));
+ AddStep("Miss Medium", () => SetContents(() => testSingle(5)));
+ AddStep("Miss Small", () => SetContents(() => testSingle(7)));
+ AddStep("Hit Big", () => SetContents(() => testSingle(2, true)));
+ AddStep("Hit Medium", () => SetContents(() => testSingle(5, true)));
+ AddStep("Hit Small", () => SetContents(() => testSingle(7, true)));
}
- private void testSingle(float circleSize, bool auto = false)
+ private Drawable testSingle(float circleSize, bool auto = false)
{
var spinner = new Spinner { StartTime = Time.Current + 2000, EndTime = Time.Current + 5000 };
@@ -49,12 +42,12 @@ namespace osu.Game.Rulesets.Osu.Tests
foreach (var mod in SelectedMods.Value.OfType())
mod.ApplyToDrawableHitObjects(new[] { drawable });
- Add(drawable);
+ return drawable;
}
private class TestDrawableSpinner : DrawableSpinner
{
- private bool auto;
+ private readonly bool auto;
public TestDrawableSpinner(Spinner s, bool auto)
: base(s)
@@ -62,16 +55,11 @@ namespace osu.Game.Rulesets.Osu.Tests
this.auto = auto;
}
- protected override void CheckForResult(bool userTriggered, double timeOffset)
+ protected override void Update()
{
- if (auto && !userTriggered && Time.Current > Spinner.StartTime + Spinner.Duration / 2 && Progress < 1)
- {
- // force completion only once to not break human interaction
- Disc.CumulativeRotation = Spinner.SpinsRequired * 360;
- auto = false;
- }
-
- base.CheckForResult(userTriggered, timeOffset);
+ base.Update();
+ if (auto)
+ RotationTracker.AddRotation((float)(Clock.ElapsedFrameTime * 3));
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index 6b1394d799..b46964e8b7 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -1,26 +1,29 @@
// 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 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 +37,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);
@@ -56,39 +61,60 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test]
public void TestSpinnerRewindingRotation()
{
+ double trackerRotationTolerance = 0;
+
addSeekStep(5000);
- AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Disc.Rotation, 0, 100));
- AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Disc.CumulativeRotation, 0, 100));
+ AddStep("calculate rotation tolerance", () =>
+ {
+ trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f);
+ });
+ AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, 100));
+ AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, 0, 100));
addSeekStep(0);
- AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.Disc.Rotation, 0, 100));
- AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.Disc.CumulativeRotation, 0, 100));
+ AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, trackerRotationTolerance));
+ AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, 0, 100));
}
[Test]
public void TestSpinnerMiddleRewindingRotation()
{
- double finalAbsoluteDiscRotation = 0, finalRelativeDiscRotation = 0, finalSpinnerSymbolRotation = 0;
+ double finalCumulativeTrackerRotation = 0;
+ double finalTrackerRotation = 0, trackerRotationTolerance = 0;
+ double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
addSeekStep(5000);
- AddStep("retrieve disc relative rotation", () => finalRelativeDiscRotation = drawableSpinner.Disc.Rotation);
- AddStep("retrieve disc absolute rotation", () => finalAbsoluteDiscRotation = drawableSpinner.Disc.CumulativeRotation);
- AddStep("retrieve spinner symbol rotation", () => finalSpinnerSymbolRotation = spinnerSymbol.Rotation);
+ AddStep("retrieve disc rotation", () =>
+ {
+ finalTrackerRotation = drawableSpinner.RotationTracker.Rotation;
+ trackerRotationTolerance = Math.Abs(finalTrackerRotation * 0.05f);
+ });
+ AddStep("retrieve spinner symbol rotation", () =>
+ {
+ finalSpinnerSymbolRotation = spinnerSymbol.Rotation;
+ spinnerSymbolRotationTolerance = Math.Abs(finalSpinnerSymbolRotation * 0.05f);
+ });
+ AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.RotationTracker.CumulativeRotation);
addSeekStep(2500);
- AddUntilStep("disc rotation rewound",
+ AddAssert("disc rotation rewound",
// we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in.
- () => Precision.AlmostEquals(drawableSpinner.Disc.Rotation, finalRelativeDiscRotation / 2, 100));
- AddUntilStep("symbol rotation rewound",
- () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, 100));
+ // due to the exponential damping applied we're allowing a larger margin of error of about 10%
+ // (5% relative to the final rotation value, but we're half-way through the spin).
+ () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation / 2, trackerRotationTolerance));
+ AddAssert("symbol rotation rewound",
+ () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, spinnerSymbolRotationTolerance));
+ AddAssert("is cumulative rotation rewound",
+ // cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
+ () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, finalCumulativeTrackerRotation / 2, 100));
addSeekStep(5000);
AddAssert("is disc rotation almost same",
- () => Precision.AlmostEquals(drawableSpinner.Disc.Rotation, finalRelativeDiscRotation, 100));
+ () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance));
AddAssert("is symbol rotation almost same",
- () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, 100));
- AddAssert("is disc rotation absolute almost same",
- () => Precision.AlmostEquals(drawableSpinner.Disc.CumulativeRotation, finalAbsoluteDiscRotation, 100));
+ () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, spinnerSymbolRotationTolerance));
+ AddAssert("is cumulative rotation almost same",
+ () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, finalCumulativeTrackerRotation, 100));
}
[Test]
@@ -111,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(5000);
- AddAssert("disc spin direction correct", () => clockwise ? drawableSpinner.Disc.Rotation > 0 : drawableSpinner.Disc.Rotation < 0);
+ AddAssert("disc spin direction correct", () => clockwise ? drawableSpinner.RotationTracker.Rotation > 0 : drawableSpinner.RotationTracker.Rotation < 0);
AddAssert("spinner symbol direction correct", () => clockwise ? spinnerSymbol.Rotation > 0 : spinnerSymbol.Rotation < 0);
}
@@ -129,18 +155,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.RotationTracker.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 +212,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/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/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
index fdba03f260..08fd13915d 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
@@ -82,9 +82,7 @@ namespace osu.Game.Rulesets.Osu.Mods
case DrawableSpinner spinner:
// hide elements we don't care about.
- spinner.Disc.Hide();
- spinner.Ticks.Hide();
- spinner.Background.Hide();
+ // todo: hide background
using (spinner.BeginAbsoluteSequence(fadeOutStartTime + longFadeDuration, true))
spinner.FadeOut(fadeOutDuration);
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/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
index 7b54baa99b..47d765fecd 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
@@ -40,8 +40,8 @@ namespace osu.Game.Rulesets.Osu.Mods
{
var spinner = (DrawableSpinner)drawable;
- spinner.Disc.Tracking = true;
- spinner.Disc.Rotate(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f));
+ spinner.RotationTracker.Tracking = true;
+ spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f));
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
index 774f9cf58b..d7582f3196 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs
@@ -43,6 +43,8 @@ namespace osu.Game.Rulesets.Osu.Mods
var h = drawableOsu.HitObject;
+ //todo: expose and hide spinner background somehow
+
switch (drawable)
{
case DrawableHitCircle circle:
@@ -56,11 +58,6 @@ namespace osu.Game.Rulesets.Osu.Mods
slider.Body.OnSkinChanged += () => applySliderState(slider);
applySliderState(slider);
break;
-
- case DrawableSpinner spinner:
- spinner.Disc.Hide();
- spinner.Background.Hide();
- 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 9c4608cbb1..68516bedf8 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -3,20 +3,18 @@
using System;
using System.Linq;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
-using osuTK;
-using osuTK.Graphics;
-using osu.Game.Graphics;
-using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Utils;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Ranking;
+using osu.Game.Skinning;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@@ -24,27 +22,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
protected readonly Spinner Spinner;
- public readonly SpinnerDisc Disc;
- public readonly SpinnerTicks Ticks;
+ private readonly Container ticks;
+
+ public readonly SpinnerRotationTracker RotationTracker;
public readonly SpinnerSpmCounter SpmCounter;
-
- private readonly Container mainContainer;
-
- public readonly SpinnerBackground Background;
- private readonly Container circleContainer;
- private readonly CirclePiece circle;
- private readonly GlowPiece glow;
-
- private readonly SpriteIcon symbol;
-
- private readonly Color4 baseColour = Color4Extensions.FromHex(@"002c3c");
- private readonly Color4 fillColour = Color4Extensions.FromHex(@"005b7c");
+ private readonly SpinnerBonusDisplay bonusDisplay;
private readonly IBindable positionBindable = new Bindable();
- private Color4 normalColour;
- private Color4 completeColour;
-
public DrawableSpinner(Spinner s)
: base(s)
{
@@ -53,65 +38,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
RelativeSizeAxes = Axes.Both;
- // we are slightly bigger than our parent, to clip the top and bottom of the circle
- Height = 1.3f;
-
Spinner = s;
InternalChildren = new Drawable[]
{
- circleContainer = new Container
- {
- AutoSizeAxes = Axes.Both,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Children = new Drawable[]
- {
- glow = new GlowPiece(),
- circle = new CirclePiece
- {
- Position = Vector2.Zero,
- Anchor = Anchor.Centre,
- },
- new RingPiece(),
- symbol = new SpriteIcon
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Size = new Vector2(48),
- Icon = FontAwesome.Solid.Asterisk,
- Shadow = false,
- },
- }
- },
- mainContainer = new AspectContainer
+ ticks = new Container(),
+ new AspectContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
- Children = new[]
+ Children = new Drawable[]
{
- Background = new SpinnerBackground
- {
- Disc =
- {
- Alpha = 0f,
- },
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
- Disc = new SpinnerDisc(Spinner)
- {
- Scale = Vector2.Zero,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
- circleContainer.CreateProxy(),
- Ticks = new SpinnerTicks
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
+ new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinnerDisc()),
+ RotationTracker = new SpinnerRotationTracker(Spinner)
}
},
SpmCounter = new SpinnerSpmCounter
@@ -120,42 +60,136 @@ 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,
}
};
}
+ private Bindable isSpinning;
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ isSpinning = RotationTracker.IsSpinning.GetBoundCopy();
+ isSpinning.BindValueChanged(updateSpinningSample);
+ }
+
+ private SkinnableSound spinningSample;
+
+ private const float minimum_volume = 0.0001f;
+
+ protected override void LoadSamples()
+ {
+ base.LoadSamples();
+
+ spinningSample?.Expire();
+ spinningSample = null;
+
+ var firstSample = HitObject.Samples.FirstOrDefault();
+
+ if (firstSample != null)
+ {
+ var clone = HitObject.SampleControlPoint.ApplyTo(firstSample);
+ clone.Name = "spinnerspin";
+
+ AddInternal(spinningSample = new SkinnableSound(clone)
+ {
+ Volume = { Value = minimum_volume },
+ Looping = true,
+ });
+ }
+ }
+
+ private void updateSpinningSample(ValueChangedEvent tracking)
+ {
+ // note that samples will not start playing if exiting a seek operation in the middle of a spinner.
+ // 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)
+ {
+ spinningSample?.Play();
+ spinningSample?.VolumeTo(1, 200);
+ }
+ else
+ {
+ spinningSample?.VolumeTo(minimum_volume, 200).Finally(_ => spinningSample.Stop());
+ }
+ }
+
+ protected override void AddNestedHitObject(DrawableHitObject hitObject)
+ {
+ base.AddNestedHitObject(hitObject);
+
+ switch (hitObject)
+ {
+ case DrawableSpinnerTick tick:
+ ticks.Add(tick);
+ break;
+ }
+ }
+
+ protected override void UpdateStateTransforms(ArmedState state)
+ {
+ base.UpdateStateTransforms(state);
+
+ using (BeginDelayedSequence(Spinner.Duration, true))
+ this.FadeOut(160);
+
+ // skin change does a rewind of transforms, which will stop the spinning sound from playing if it's currently in playback.
+ isSpinning?.TriggerChange();
+ }
+
+ 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;
- Ticks.AccentColour = normalColour;
-
- Disc.AccentColour = fillColour;
- circle.Colour = colours.BlueDark;
- glow.Colour = colours.BlueDark;
-
positionBindable.BindValueChanged(pos => Position = pos.NewValue);
positionBindable.BindTo(HitObject.PositionBindable);
}
- public float Progress => Math.Clamp(Disc.CumulativeRotation / 360 / Spinner.SpinsRequired, 0, 1);
+ ///
+ /// The completion progress of this spinner from 0..1 (clamped).
+ ///
+ public float Progress => Math.Clamp(RotationTracker.CumulativeRotation / 360 / Spinner.SpinsRequired, 0, 1);
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (Time.Current < HitObject.StartTime) return;
- if (Progress >= 1 && !Disc.Complete)
- {
- Disc.Complete = true;
- transformFillColour(completeColour, 200);
- }
+ RotationTracker.Complete.Value = Progress >= 1;
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)
@@ -172,85 +206,56 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override void Update()
{
base.Update();
+
if (HandleUserInput)
- Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false;
+ RotationTracker.Tracking = !Result.HasResult && (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false);
+
+ if (spinningSample != null)
+ // todo: implement SpinnerFrequencyModulate
+ spinningSample.Frequency.Value = 0.5f + Progress;
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
- if (!SpmCounter.IsPresent && Disc.Tracking)
+ if (!SpmCounter.IsPresent && RotationTracker.Tracking)
SpmCounter.FadeIn(HitObject.TimeFadeIn);
+ SpmCounter.SetRotation(RotationTracker.CumulativeRotation);
- circle.Rotation = Disc.Rotation;
- Ticks.Rotation = Disc.Rotation;
- SpmCounter.SetRotation(Disc.CumulativeRotation);
-
- 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)));
-
- symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, Disc.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1));
+ updateBonusScore();
}
- protected override void UpdateInitialTransforms()
+ private int wholeSpins;
+
+ private void updateBonusScore()
{
- base.UpdateInitialTransforms();
+ if (ticks.Count == 0)
+ return;
- circleContainer.ScaleTo(0);
- mainContainer.ScaleTo(0);
+ int spins = (int)(RotationTracker.CumulativeRotation / 360);
- using (BeginDelayedSequence(HitObject.TimePreempt / 2, true))
+ if (spins < wholeSpins)
{
- 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);
- }
+ // rewinding, silently handle
+ wholeSpins = spins;
+ return;
}
- }
- protected override void UpdateStateTransforms(ArmedState state)
- {
- base.UpdateStateTransforms(state);
-
- using (BeginDelayedSequence(Spinner.Duration, true))
+ while (wholeSpins != spins)
{
- this.FadeOut(160);
+ var tick = ticks.FirstOrDefault(t => !t.IsHit);
- switch (state)
+ // tick may be null if we've hit the spin limit.
+ if (tick != null)
{
- 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;
+ tick.TriggerResult(true);
+ if (tick is DrawableSpinnerBonusTick)
+ bonusDisplay.SetBonusCount(spins - Spinner.SpinsRequired);
}
+
+ wholeSpins++;
}
}
-
- 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/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs
new file mode 100644
index 0000000000..dfb692eba9
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs
@@ -0,0 +1,189 @@
+// 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.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Utils;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Objects.Drawables;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
+{
+ public class DefaultSpinnerDisc : CompositeDrawable
+ {
+ private DrawableSpinner drawableSpinner;
+
+ private Spinner spinner;
+
+ private const float idle_alpha = 0.2f;
+ private const float tracking_alpha = 0.4f;
+
+ private Color4 normalColour;
+ private Color4 completeColour;
+
+ private SpinnerTicks ticks;
+
+ private int wholeRotationCount;
+
+ private SpinnerFill fill;
+ private Container mainContainer;
+ private SpinnerCentreLayer centre;
+ private SpinnerBackgroundLayer background;
+
+ public DefaultSpinnerDisc()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ // we are slightly bigger than our parent, to clip the top and bottom of the circle
+ // this should probably be revisited when scaled spinners are a thing.
+ Scale = new Vector2(1.3f);
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours, DrawableHitObject drawableHitObject)
+ {
+ drawableSpinner = (DrawableSpinner)drawableHitObject;
+ spinner = (Spinner)drawableSpinner.HitObject;
+
+ normalColour = colours.BlueDark;
+ completeColour = colours.YellowLight;
+
+ InternalChildren = new Drawable[]
+ {
+ mainContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ background = new SpinnerBackgroundLayer(),
+ fill = new SpinnerFill
+ {
+ Alpha = idle_alpha,
+ AccentColour = normalColour
+ },
+ ticks = new SpinnerTicks
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AccentColour = normalColour
+ },
+ }
+ },
+ centre = new SpinnerCentreLayer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ drawableSpinner.RotationTracker.Complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200));
+ drawableSpinner.State.BindValueChanged(updateStateTransforms, true);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (drawableSpinner.RotationTracker.Complete.Value)
+ {
+ if (checkNewRotationCount)
+ {
+ fill.FinishTransforms(false, nameof(Alpha));
+ fill
+ .FadeTo(tracking_alpha + 0.2f, 60, Easing.OutExpo)
+ .Then()
+ .FadeTo(tracking_alpha, 250, Easing.OutQuint);
+ }
+ }
+ else
+ {
+ fill.Alpha = (float)Interpolation.Damp(fill.Alpha, drawableSpinner.RotationTracker.Tracking ? tracking_alpha : idle_alpha, 0.98f, (float)Math.Abs(Clock.ElapsedFrameTime));
+ }
+
+ const float initial_scale = 0.2f;
+ float targetScale = initial_scale + (1 - initial_scale) * drawableSpinner.Progress;
+
+ fill.Scale = new Vector2((float)Interpolation.Lerp(fill.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1)));
+ mainContainer.Rotation = drawableSpinner.RotationTracker.Rotation;
+ }
+
+ private void updateStateTransforms(ValueChangedEvent state)
+ {
+ centre.ScaleTo(0);
+ mainContainer.ScaleTo(0);
+
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true))
+ {
+ // constant ambient rotation to give the spinner "spinning" character.
+ this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration);
+
+ centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint);
+ mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint);
+
+ using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
+ {
+ centre.ScaleTo(0.5f, spinner.TimePreempt / 2, Easing.OutQuint);
+ mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint);
+ }
+ }
+
+ // transforms we have from completing the spinner will be rolled back, so reapply immediately.
+ updateComplete(state.NewValue == ArmedState.Hit, 0);
+
+ using (BeginDelayedSequence(spinner.Duration, true))
+ {
+ switch (state.NewValue)
+ {
+ case ArmedState.Hit:
+ this.ScaleTo(Scale * 1.2f, 320, Easing.Out);
+ this.RotateTo(mainContainer.Rotation + 180, 320);
+ break;
+
+ case ArmedState.Miss:
+ this.ScaleTo(Scale * 0.8f, 320, Easing.In);
+ break;
+ }
+ }
+ }
+
+ private void updateComplete(bool complete, double duration)
+ {
+ var colour = complete ? completeColour : normalColour;
+
+ ticks.FadeAccent(colour.Darken(1), duration);
+ fill.FadeAccent(colour.Darken(1), duration);
+
+ background.FadeAccent(colour, duration);
+ centre.FadeAccent(colour, duration);
+ }
+
+ private bool checkNewRotationCount
+ {
+ get
+ {
+ int rotations = (int)(drawableSpinner.RotationTracker.CumulativeRotation / 360);
+
+ if (wholeRotationCount == rotations) return false;
+
+ wholeRotationCount = rotations;
+ return true;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
index 395c76a233..07dc6021c9 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private readonly Slider slider;
private readonly Drawable followCircle;
private readonly DrawableSlider drawableSlider;
- private readonly CircularContainer ball;
+ private readonly Drawable ball;
public SliderBall(Slider slider, DrawableSlider drawableSlider = null)
{
@@ -54,19 +54,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Alpha = 0,
Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()),
},
- ball = new CircularContainer
+ ball = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall())
{
- Masking = true,
- RelativeSizeAxes = Axes.Both,
- Origin = Anchor.Centre,
Anchor = Anchor.Centre,
- Alpha = 1,
- Child = new Container
- {
- RelativeSizeAxes = Axes.Both,
- Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall()),
- }
- }
+ Origin = Anchor.Centre,
+ },
};
}
@@ -187,12 +179,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
return;
Position = newPos;
- Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI);
+ ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI);
lastPosition = newPos;
}
- private class FollowCircleContainer : Container
+ private class FollowCircleContainer : CircularContainer
{
public override bool HandlePositionalInput => true;
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs
new file mode 100644
index 0000000000..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/SpinnerBackground.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerFill.cs
similarity index 88%
rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs
rename to osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerFill.cs
index 944354abca..043bc5618c 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerFill.cs
@@ -10,7 +10,7 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
- public class SpinnerBackground : CircularContainer, IHasAccentColour
+ public class SpinnerFill : CircularContainer, IHasAccentColour
{
public readonly Box Disc;
@@ -31,11 +31,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}
}
- public SpinnerBackground()
+ public SpinnerFill()
{
RelativeSizeAxes = Axes.Both;
Masking = true;
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
Children = new Drawable[]
{
Disc = new Box
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs
similarity index 61%
rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs
rename to osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs
index 35819cd05e..0cc6c842f4 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs
@@ -2,76 +2,33 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
-using osu.Game.Graphics;
-using osuTK;
-using osuTK.Graphics;
using osu.Framework.Utils;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
- public class SpinnerDisc : CircularContainer, IHasAccentColour
+ public class SpinnerRotationTracker : CircularContainer
{
private readonly Spinner spinner;
- public Color4 AccentColour
- {
- get => background.AccentColour;
- set => background.AccentColour = value;
- }
-
- private readonly SpinnerBackground background;
-
- private const float idle_alpha = 0.2f;
- private const float tracking_alpha = 0.4f;
-
public override bool IsPresent => true; // handle input when hidden
- public SpinnerDisc(Spinner s)
+ public SpinnerRotationTracker(Spinner s)
{
spinner = s;
RelativeSizeAxes = Axes.Both;
-
- Children = new Drawable[]
- {
- background = new SpinnerBackground { Alpha = idle_alpha },
- };
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
- private bool tracking;
+ public bool Tracking { get; set; }
- public bool Tracking
- {
- get => tracking;
- set
- {
- if (value == tracking) return;
-
- tracking = value;
-
- background.FadeTo(tracking ? tracking_alpha : idle_alpha, 100);
- }
- }
-
- private bool complete;
-
- public bool Complete
- {
- get => complete;
- set
- {
- if (value == complete) return;
-
- complete = value;
-
- updateCompleteTick();
- }
- }
+ public readonly BindableBool Complete = new BindableBool();
///
/// The total rotation performed on the spinner disc, disregarding the spin direction.
@@ -84,7 +41,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
/// If the spinner is spun 360 degrees clockwise and then 360 degrees counter-clockwise,
/// this property will return the value of 720 (as opposed to 0 for ).
///
- public float CumulativeRotation;
+ public float CumulativeRotation { get; private set; }
+
+ ///
+ /// Whether the spinning is spinning at a reasonable speed to be considered visually spinning.
+ ///
+ public readonly BindableBool IsSpinning = new BindableBool();
///
/// Whether currently in the correct time range to allow spinning.
@@ -101,9 +63,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private float lastAngle;
private float currentRotation;
- private int completeTick;
-
- private bool updateCompleteTick() => completeTick != (completeTick = (int)(CumulativeRotation / 360));
private bool rotationTransferred;
@@ -114,21 +73,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
var delta = thisAngle - lastAngle;
- if (tracking)
- Rotate(delta);
+ if (Tracking)
+ AddRotation(delta);
lastAngle = thisAngle;
- if (Complete && updateCompleteTick())
- {
- background.FinishTransforms(false, nameof(Alpha));
- background
- .FadeTo(tracking_alpha + 0.2f, 60, Easing.OutExpo)
- .Then()
- .FadeTo(tracking_alpha, 250, Easing.OutQuint);
- }
+ IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation / 2 - Rotation) > 5f;
- Rotation = (float)Interpolation.Lerp(Rotation, currentRotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1));
+ Rotation = (float)Interpolation.Damp(Rotation, currentRotation / 2, 0.99, Math.Abs(Time.Elapsed));
}
///
@@ -138,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
/// Will be a no-op if not a valid time to spin.
///
/// The delta angle.
- public void Rotate(float angle)
+ public void AddRotation(float angle)
{
if (!isSpinnableTime)
return;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerBackgroundLayer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerBackgroundLayer.cs
new file mode 100644
index 0000000000..3cd2454706
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerBackgroundLayer.cs
@@ -0,0 +1,22 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
+
+namespace osu.Game.Rulesets.Osu.Objects.Drawables
+{
+ public class SpinnerBackgroundLayer : SpinnerFill
+ {
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours, DrawableHitObject drawableHitObject)
+ {
+ Disc.Alpha = 0;
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerCentreLayer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerCentreLayer.cs
new file mode 100644
index 0000000000..b62ce822f0
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerCentreLayer.cs
@@ -0,0 +1,71 @@
+// 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.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Utils;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Objects.Drawables
+{
+ public class SpinnerCentreLayer : CompositeDrawable, IHasAccentColour
+ {
+ private DrawableSpinner spinner;
+
+ private CirclePiece circle;
+ private GlowPiece glow;
+ private SpriteIcon symbol;
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject drawableHitObject)
+ {
+ spinner = (DrawableSpinner)drawableHitObject;
+
+ InternalChildren = new Drawable[]
+ {
+ glow = new GlowPiece(),
+ circle = new CirclePiece
+ {
+ Position = Vector2.Zero,
+ Anchor = Anchor.Centre,
+ },
+ new RingPiece(),
+ symbol = new SpriteIcon
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(48),
+ Icon = FontAwesome.Solid.Asterisk,
+ Shadow = false,
+ },
+ };
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+ symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, spinner.RotationTracker.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1));
+ }
+
+ private Color4 accentColour;
+
+ public Color4 AccentColour
+ {
+ get => accentColour;
+ set
+ {
+ accentColour = value;
+
+ circle.Colour = accentColour;
+ glow.Colour = accentColour;
+ }
+ }
+ }
+}
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/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
index b2cdc8ccbf..5468764692 100644
--- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
+++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs
@@ -17,5 +17,6 @@ namespace osu.Game.Rulesets.Osu
SliderFollowCircle,
SliderBall,
SliderBody,
+ SpinnerBody
}
}
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/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs
new file mode 100644
index 0000000000..72bc3ddc9a
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs
@@ -0,0 +1,99 @@
+// 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.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Skinning;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning
+{
+ ///
+ /// Legacy skinned spinner with two main spinning layers, one fixed overlay and one final spinning overlay.
+ /// No background layer.
+ ///
+ public class LegacyNewStyleSpinner : CompositeDrawable
+ {
+ private Sprite discBottom;
+ private Sprite discTop;
+ private Sprite spinningMiddle;
+ private Sprite fixedMiddle;
+
+ private DrawableSpinner drawableSpinner;
+
+ private const float final_scale = 0.625f;
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource source, DrawableHitObject drawableObject)
+ {
+ drawableSpinner = (DrawableSpinner)drawableObject;
+
+ Scale = new Vector2(final_scale);
+
+ InternalChildren = new Drawable[]
+ {
+ discBottom = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-bottom")
+ },
+ discTop = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-top")
+ },
+ fixedMiddle = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-middle")
+ },
+ spinningMiddle = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-middle2")
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ this.FadeOut();
+ drawableSpinner.State.BindValueChanged(updateStateTransforms, true);
+ }
+
+ private void updateStateTransforms(ValueChangedEvent state)
+ {
+ var spinner = (Spinner)drawableSpinner.HitObject;
+
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true))
+ this.FadeInFromZero(spinner.TimePreempt / 2);
+
+ fixedMiddle.FadeColour(Color4.White);
+ using (BeginAbsoluteSequence(spinner.StartTime, true))
+ fixedMiddle.FadeColour(Color4.Red, spinner.Duration);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+ spinningMiddle.Rotation = discTop.Rotation = drawableSpinner.RotationTracker.Rotation;
+ discBottom.Rotation = discTop.Rotation / 3;
+
+ Scale = new Vector2(final_scale * (0.8f + (float)Interpolation.ApplyEasing(Easing.Out, drawableSpinner.Progress) * 0.2f));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs
new file mode 100644
index 0000000000..0ae1d8f683
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs
@@ -0,0 +1,114 @@
+// 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.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Skinning
+{
+ ///
+ /// Legacy skinned spinner with one main spinning layer and a background layer.
+ ///
+ public class LegacyOldStyleSpinner : CompositeDrawable
+ {
+ private DrawableSpinner drawableSpinner;
+ private Sprite disc;
+ private Container metre;
+
+ private const float background_y_offset = 20;
+
+ private const float sprite_scale = 1 / 1.6f;
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource source, DrawableHitObject drawableObject)
+ {
+ drawableSpinner = (DrawableSpinner)drawableObject;
+
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ new Sprite
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Texture = source.GetTexture("spinner-background"),
+ Y = background_y_offset,
+ Scale = new Vector2(sprite_scale)
+ },
+ disc = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Texture = source.GetTexture("spinner-circle"),
+ Scale = new Vector2(sprite_scale)
+ },
+ metre = new Container
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Y = background_y_offset,
+ Masking = true,
+ Child = new Sprite
+ {
+ Texture = source.GetTexture("spinner-metre"),
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ },
+ Scale = new Vector2(0.625f)
+ }
+ };
+ }
+
+ private Vector2 metreFinalSize;
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ this.FadeOut();
+ drawableSpinner.State.BindValueChanged(updateStateTransforms, true);
+
+ metreFinalSize = metre.Size = metre.Child.Size;
+ }
+
+ private void updateStateTransforms(ValueChangedEvent state)
+ {
+ var spinner = drawableSpinner.HitObject;
+
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true))
+ this.FadeInFromZero(spinner.TimePreempt / 2);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+ disc.Rotation = drawableSpinner.RotationTracker.Rotation;
+ metre.Height = getMetreHeight(drawableSpinner.Progress);
+ }
+
+ private const int total_bars = 10;
+
+ private float getMetreHeight(float progress)
+ {
+ progress = Math.Min(99, progress * 100);
+
+ int barCount = (int)progress / 10;
+
+ // todo: add SpinnerNoBlink support
+ if (RNG.NextBool(((int)progress % 10) / 10f))
+ barCount++;
+
+ return (float)barCount / total_bars * metreFinalSize.Y;
+ }
+ }
+}
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..81d1d05b66 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
@@ -102,6 +102,16 @@ namespace osu.Game.Rulesets.Osu.Skinning
Scale = new Vector2(0.8f),
Spacing = new Vector2(-overlap, 0)
};
+
+ case OsuSkinComponents.SpinnerBody:
+ bool hasBackground = Source.GetTexture("spinner-background") != null;
+
+ if (Source.GetTexture("spinner-top") != null && !hasBackground)
+ return new LegacyNewStyleSpinner();
+ else if (hasBackground)
+ return new LegacyOldStyleSpinner();
+
+ return null;
}
return null;
diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/SampleLookups/taiko-hitobject-beatmap-custom-sample-bank.osu b/osu.Game.Rulesets.Taiko.Tests/Resources/SampleLookups/taiko-hitobject-beatmap-custom-sample-bank.osu
new file mode 100644
index 0000000000..f9755782c2
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Resources/SampleLookups/taiko-hitobject-beatmap-custom-sample-bank.osu
@@ -0,0 +1,10 @@
+osu file format v14
+
+[General]
+Mode: 1
+
+[TimingPoints]
+0,300,4,1,2,100,1,0
+
+[HitObjects]
+444,320,1000,5,0,0:0:0:0:
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/TestSceneTaikoHitObjectSamples.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectSamples.cs
new file mode 100644
index 0000000000..7089ea6619
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectSamples.cs
@@ -0,0 +1,52 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Reflection;
+using NUnit.Framework;
+using osu.Framework.IO.Stores;
+using osu.Game.Tests.Beatmaps;
+
+namespace osu.Game.Rulesets.Taiko.Tests
+{
+ public class TestSceneTaikoHitObjectSamples : HitObjectSampleTest
+ {
+ protected override Ruleset CreatePlayerRuleset() => new TaikoRuleset();
+
+ protected override IResourceStore Resources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneTaikoHitObjectSamples)));
+
+ [TestCase("taiko-normal-hitnormal")]
+ [TestCase("normal-hitnormal")]
+ [TestCase("hitnormal")]
+ public void TestDefaultCustomSampleFromBeatmap(string expectedSample)
+ {
+ SetupSkins(expectedSample, expectedSample);
+
+ CreateTestWithBeatmap("taiko-hitobject-beatmap-custom-sample-bank.osu");
+
+ AssertBeatmapLookup(expectedSample);
+ }
+
+ [TestCase("taiko-normal-hitnormal")]
+ [TestCase("normal-hitnormal")]
+ [TestCase("hitnormal")]
+ public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample)
+ {
+ SetupSkins(string.Empty, expectedSample);
+
+ CreateTestWithBeatmap("taiko-hitobject-beatmap-custom-sample-bank.osu");
+
+ AssertUserLookup(expectedSample);
+ }
+
+ [TestCase("taiko-normal-hitnormal2")]
+ [TestCase("normal-hitnormal2")]
+ public void TestUserSkinLookupIgnoresSampleBank(string unwantedSample)
+ {
+ SetupSkins(string.Empty, unwantedSample);
+
+ CreateTestWithBeatmap("taiko-hitobject-beatmap-custom-sample-bank.osu");
+
+ AssertNoLookup(unwantedSample);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs
index aaa634648a..0be005e1c4 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
};
[Test]
- public void TestSpinnerDoesNotFail()
+ public void TestSpinnerDoesFail()
{
bool judged = false;
AddStep("Setup judgements", () =>
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
Player.ScoreProcessor.NewJudgement += b => judged = true;
});
AddUntilStep("swell judged", () => judged);
- AddAssert("not failed", () => !Player.HasFailed);
+ AddAssert("failed", () => Player.HasFailed);
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs
new file mode 100644
index 0000000000..7c39c040b1
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs
@@ -0,0 +1,64 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Audio;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Taiko.Audio
+{
+ ///
+ /// Stores samples for the input drum.
+ /// The lifetime of the samples is adjusted so that they are only alive during the appropriate sample control point.
+ ///
+ public class DrumSampleContainer : LifetimeManagementContainer
+ {
+ private readonly ControlPointInfo controlPoints;
+ private readonly Dictionary mappings = new Dictionary();
+
+ public DrumSampleContainer(ControlPointInfo controlPoints)
+ {
+ this.controlPoints = controlPoints;
+
+ IReadOnlyList samplePoints = controlPoints.SamplePoints.Count == 0 ? new[] { controlPoints.SamplePointAt(double.MinValue) } : controlPoints.SamplePoints;
+
+ for (int i = 0; i < samplePoints.Count; i++)
+ {
+ var samplePoint = samplePoints[i];
+
+ var centre = samplePoint.GetSampleInfo();
+ var rim = samplePoint.GetSampleInfo(HitSampleInfo.HIT_CLAP);
+
+ var lifetimeStart = i > 0 ? samplePoint.Time : double.MinValue;
+ var lifetimeEnd = i + 1 < samplePoints.Count ? samplePoints[i + 1].Time : double.MaxValue;
+
+ mappings[samplePoint.Time] = new DrumSample
+ {
+ Centre = addSound(centre, lifetimeStart, lifetimeEnd),
+ Rim = addSound(rim, lifetimeStart, lifetimeEnd)
+ };
+ }
+ }
+
+ private SkinnableSound addSound(HitSampleInfo hitSampleInfo, double lifetimeStart, double lifetimeEnd)
+ {
+ var drawable = new SkinnableSound(hitSampleInfo)
+ {
+ LifetimeStart = lifetimeStart,
+ LifetimeEnd = lifetimeEnd
+ };
+ AddInternal(drawable);
+ return drawable;
+ }
+
+ public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time).Time];
+
+ public class DrumSample
+ {
+ public SkinnableSound Centre;
+ public SkinnableSound Rim;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs
deleted file mode 100644
index c31b07344d..0000000000
--- a/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System.Collections.Generic;
-using osu.Game.Audio;
-using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Skinning;
-
-namespace osu.Game.Rulesets.Taiko.Audio
-{
- public class DrumSampleMapping
- {
- private readonly ControlPointInfo controlPoints;
- private readonly Dictionary mappings = new Dictionary();
-
- public readonly List Sounds = new List();
-
- public DrumSampleMapping(ControlPointInfo controlPoints)
- {
- this.controlPoints = controlPoints;
-
- IEnumerable samplePoints = controlPoints.SamplePoints.Count == 0 ? new[] { controlPoints.SamplePointAt(double.MinValue) } : controlPoints.SamplePoints;
-
- foreach (var s in samplePoints)
- {
- var centre = s.GetSampleInfo();
- var rim = s.GetSampleInfo(HitSampleInfo.HIT_CLAP);
-
- mappings[s.Time] = new DrumSample
- {
- Centre = addSound(centre),
- Rim = addSound(rim)
- };
- }
- }
-
- private SkinnableSound addSound(HitSampleInfo hitSampleInfo)
- {
- var drawable = new SkinnableSound(hitSampleInfo);
- Sounds.Add(drawable);
- return drawable;
- }
-
- public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time).Time];
-
- public class DrumSample
- {
- public SkinnableSound Centre;
- public SkinnableSound Rim;
- }
- }
-}
diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs
index 604daa929f..0d91002f4b 100644
--- a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs
+++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs
@@ -7,8 +7,6 @@ namespace osu.Game.Rulesets.Taiko.Judgements
{
public class TaikoDrumRollJudgement : TaikoJudgement
{
- public override bool AffectsCombo => false;
-
protected override double HealthIncreaseFor(HitResult result)
{
// Drum rolls can be ignored with no health penalty
diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs
index 29be5e0eac..4d61efd3ee 100644
--- a/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs
+++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs
@@ -7,8 +7,6 @@ namespace osu.Game.Rulesets.Taiko.Judgements
{
public class TaikoSwellJudgement : TaikoJudgement
{
- public override bool AffectsCombo => false;
-
protected override double HealthIncreaseFor(HitResult result)
{
switch (result)
diff --git a/osu.Game.Rulesets.Taiko/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/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/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs
index 737946e1e0..c3acc2ebe7 100644
--- a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs
@@ -67,9 +67,11 @@ namespace osu.Game.Tests.Gameplay
/// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the beatmap skin:
/// normal-hitnormal2
/// normal-hitnormal
+ /// hitnormal
///
[TestCase("normal-hitnormal2")]
[TestCase("normal-hitnormal")]
+ [TestCase("hitnormal")]
public void TestDefaultCustomSampleFromBeatmap(string expectedSample)
{
SetupSkins(expectedSample, expectedSample);
@@ -80,12 +82,13 @@ namespace osu.Game.Tests.Gameplay
}
///
- /// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the user skin when the beatmap does not contain the sample:
- /// normal-hitnormal2
+ /// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the user skin
+ /// (ignoring the custom sample set index) when the beatmap skin does not contain the sample:
/// normal-hitnormal
+ /// hitnormal
///
- [TestCase("normal-hitnormal2")]
[TestCase("normal-hitnormal")]
+ [TestCase("hitnormal")]
public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample)
{
SetupSkins(string.Empty, expectedSample);
@@ -95,6 +98,23 @@ namespace osu.Game.Tests.Gameplay
AssertUserLookup(expectedSample);
}
+ ///
+ /// Tests that a hitobject which provides a custom sample set of 2 does not retrieve a normal-hitnormal2 sample from the user skin
+ /// if the beatmap skin does not contain the sample.
+ /// User skins in stable ignore the custom sample set index when performing lookups.
+ ///
+ [Test]
+ public void TestUserSkinLookupIgnoresSampleBank()
+ {
+ const string unwanted_sample = "normal-hitnormal2";
+
+ SetupSkins(string.Empty, unwanted_sample);
+
+ CreateTestWithBeatmap("hitobject-beatmap-custom-sample.osu");
+
+ AssertNoLookup(unwanted_sample);
+ }
+
///
/// Tests that a hitobject which provides a sample file retrieves the sample file from the beatmap skin.
///
@@ -145,6 +165,7 @@ namespace osu.Game.Tests.Gameplay
///
[TestCase("normal-hitnormal2")]
[TestCase("normal-hitnormal")]
+ [TestCase("hitnormal")]
public void TestControlPointCustomSampleFromBeatmap(string sampleName)
{
SetupSkins(sampleName, sampleName);
@@ -178,7 +199,7 @@ namespace osu.Game.Tests.Gameplay
string[] expectedSamples =
{
"normal-hitnormal2",
- "normal-hitwhistle2"
+ "normal-hitwhistle" // user skin lookups ignore custom sample set index
};
SetupSkins(expectedSamples[0], expectedSamples[1]);
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/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..03fd2b968c 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs
@@ -3,8 +3,16 @@
using System;
using System.Collections.Generic;
+using System.Linq;
+using System.Net;
using System.Threading.Tasks;
+using JetBrains.Annotations;
+using Newtonsoft.Json.Linq;
using NUnit.Framework;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Multiplayer;
@@ -12,6 +20,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Multi.Ranking;
+using osu.Game.Screens.Ranking;
using osu.Game.Tests.Beatmaps;
using osu.Game.Users;
@@ -19,43 +28,134 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneTimeshiftResultsScreen : ScreenTestScene
{
- private bool roomsReceived;
+ private const int scores_per_result = 10;
+
+ private TestResultsScreen resultsScreen;
+ private int currentScoreId;
+ private bool requestComplete;
[SetUp]
public void Setup() => Schedule(() =>
{
- roomsReceived = false;
+ currentScoreId = 0;
+ requestComplete = false;
bindHandler();
});
[Test]
- public void TestShowResultsWithScore()
+ public void TestShowWithUserScore()
{
- createResults(new TestScoreInfo(new OsuRuleset().RulesetInfo));
- AddWaitStep("wait for display", 5);
+ ScoreInfo userScore = null;
+
+ AddStep("bind user score info handler", () =>
+ {
+ userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ };
+ bindHandler(userScore: userScore);
+ });
+
+ createResults(() => userScore);
+ waitForDisplay();
+
+ AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded);
}
[Test]
- public void TestShowResultsNullScore()
+ public void TestShowNullUserScore()
{
- createResults(null);
- AddWaitStep("wait for display", 5);
+ createResults();
+ waitForDisplay();
+
+ AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
}
[Test]
- public void TestShowResultsNullScoreWithDelay()
+ public void TestShowUserScoreWithDelay()
+ {
+ ScoreInfo userScore = null;
+
+ AddStep("bind user score info handler", () =>
+ {
+ userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ };
+ bindHandler(3000, userScore);
+ });
+
+ createResults(() => userScore);
+ waitForDisplay();
+
+ AddAssert("more than 1 panel displayed", () => this.ChildrenOfType().Count() > 1);
+ AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded);
+ }
+
+ [Test]
+ public void TestShowNullUserScoreWithDelay()
{
AddStep("bind delayed handler", () => bindHandler(3000));
- createResults(null);
- AddUntilStep("wait for rooms to be received", () => roomsReceived);
- AddWaitStep("wait for display", 5);
+
+ createResults();
+ waitForDisplay();
+
+ AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
}
- private void createResults(ScoreInfo score)
+ [Test]
+ public void TestFetchWhenScrolledToTheRight()
+ {
+ createResults();
+ waitForDisplay();
+
+ AddStep("bind delayed handler", () => bindHandler(3000));
+
+ for (int i = 0; i < 2; i++)
+ {
+ int beforePanelCount = 0;
+
+ AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count());
+ AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType().Single().ScrollToEnd(false));
+
+ AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
+ waitForDisplay();
+
+ AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result);
+ AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden);
+ }
+ }
+
+ [Test]
+ public void TestFetchWhenScrolledToTheLeft()
+ {
+ ScoreInfo userScore = null;
+
+ AddStep("bind user score info handler", () =>
+ {
+ userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ };
+ bindHandler(userScore: userScore);
+ });
+
+ createResults(() => userScore);
+ waitForDisplay();
+
+ AddStep("bind delayed handler", () => bindHandler(3000));
+
+ for (int i = 0; i < 2; i++)
+ {
+ int beforePanelCount = 0;
+
+ AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count());
+ AddStep("scroll to left", () => resultsScreen.ScorePanelList.ChildrenOfType().Single().ScrollToStart(false));
+
+ AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible);
+ waitForDisplay();
+
+ AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result);
+ AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden);
+ }
+ }
+
+ private void createResults(Func getScore = null)
{
AddStep("load results", () =>
{
- LoadScreen(new TimeshiftResultsScreen(score, 1, new PlaylistItem
+ LoadScreen(resultsScreen = new TestResultsScreen(getScore?.Invoke(), 1, new PlaylistItem
{
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo }
@@ -63,62 +163,214 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
}
- private void bindHandler(double delay = 0)
+ private void waitForDisplay()
{
- var roomScores = new List();
+ AddUntilStep("wait for request to complete", () => requestComplete);
+ AddWaitStep("wait for display", 5);
+ }
- for (int i = 0; i < 10; i++)
+ private void bindHandler(double delay = 0, ScoreInfo userScore = null, bool failRequests = false) => ((DummyAPIAccess)API).HandleRequest = request =>
+ {
+ requestComplete = false;
+
+ if (failRequests)
{
- roomScores.Add(new RoomScore
+ triggerFail(request, delay);
+ return;
+ }
+
+ switch (request)
+ {
+ case ShowPlaylistUserScoreRequest s:
+ if (userScore == null)
+ triggerFail(s, delay);
+ else
+ triggerSuccess(s, createUserResponse(userScore), delay);
+ break;
+
+ case IndexPlaylistScoresRequest i:
+ triggerSuccess(i, createIndexResponse(i), delay);
+ break;
+ }
+ };
+
+ private void triggerSuccess(APIRequest req, T result, double delay)
+ where T : class
+ {
+ if (delay == 0)
+ success();
+ else
+ {
+ Task.Run(async () =>
{
- ID = i,
- Accuracy = 0.9 - 0.01 * i,
- EndedAt = DateTimeOffset.Now.Subtract(TimeSpan.FromHours(i)),
+ await Task.Delay(TimeSpan.FromMilliseconds(delay));
+ Schedule(success);
+ });
+ }
+
+ void success()
+ {
+ requestComplete = true;
+ req.TriggerSuccess(result);
+ }
+ }
+
+ private void triggerFail(APIRequest req, double delay)
+ {
+ if (delay == 0)
+ fail();
+ else
+ {
+ Task.Run(async () =>
+ {
+ await Task.Delay(TimeSpan.FromMilliseconds(delay));
+ Schedule(fail);
+ });
+ }
+
+ void fail()
+ {
+ requestComplete = true;
+ req.TriggerFailure(new WebException("Failed."));
+ }
+ }
+
+ private MultiplayerScore createUserResponse([NotNull] ScoreInfo userScore)
+ {
+ var multiplayerUserScore = new MultiplayerScore
+ {
+ ID = (int)(userScore.OnlineScoreID ?? currentScoreId++),
+ Accuracy = userScore.Accuracy,
+ EndedAt = userScore.Date,
+ Passed = userScore.Passed,
+ Rank = userScore.Rank,
+ Position = 200,
+ MaxCombo = userScore.MaxCombo,
+ TotalScore = userScore.TotalScore,
+ User = userScore.User,
+ Statistics = userScore.Statistics,
+ ScoresAround = new MultiplayerScoresAround
+ {
+ Higher = new MultiplayerScores(),
+ Lower = new MultiplayerScores()
+ }
+ };
+
+ for (int i = 1; i <= scores_per_result; i++)
+ {
+ multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore
+ {
+ ID = currentScoreId++,
+ Accuracy = userScore.Accuracy,
+ EndedAt = userScore.Date,
Passed = true,
- Rank = ScoreRank.B,
- MaxCombo = 999,
- TotalScore = 999999 - i * 1000,
+ Rank = userScore.Rank,
+ MaxCombo = userScore.MaxCombo,
+ TotalScore = userScore.TotalScore - i,
User = new User
{
Id = 2,
Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
},
- Statistics =
+ Statistics = userScore.Statistics
+ });
+
+ multiplayerUserScore.ScoresAround.Higher.Scores.Add(new MultiplayerScore
+ {
+ ID = currentScoreId++,
+ Accuracy = userScore.Accuracy,
+ EndedAt = userScore.Date,
+ Passed = true,
+ Rank = userScore.Rank,
+ MaxCombo = userScore.MaxCombo,
+ TotalScore = userScore.TotalScore + i,
+ User = new User
+ {
+ Id = 2,
+ Username = $"peppy{i}",
+ CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ },
+ Statistics = userScore.Statistics
+ });
+ }
+
+ addCursor(multiplayerUserScore.ScoresAround.Lower);
+ addCursor(multiplayerUserScore.ScoresAround.Higher);
+
+ return multiplayerUserScore;
+ }
+
+ private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req)
+ {
+ var result = new IndexedMultiplayerScores();
+
+ long startTotalScore = req.Cursor?.Properties["total_score"].ToObject() ?? 1000000;
+ string sort = req.IndexParams?.Properties["sort"].ToObject() ?? "score_desc";
+
+ for (int i = 1; i <= scores_per_result; i++)
+ {
+ result.Scores.Add(new MultiplayerScore
+ {
+ ID = currentScoreId++,
+ Accuracy = 1,
+ EndedAt = DateTimeOffset.Now,
+ Passed = true,
+ Rank = ScoreRank.X,
+ MaxCombo = 1000,
+ TotalScore = startTotalScore + (sort == "score_asc" ? i : -i),
+ User = new User
+ {
+ Id = 2,
+ Username = $"peppy{i}",
+ CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ },
+ Statistics = new Dictionary
{
{ HitResult.Miss, 1 },
{ HitResult.Meh, 50 },
{ HitResult.Good, 100 },
- { HitResult.Great, 300 },
+ { HitResult.Great, 300 }
}
});
}
- ((DummyAPIAccess)API).HandleRequest = request =>
+ addCursor(result);
+
+ return result;
+ }
+
+ private void addCursor(MultiplayerScores scores)
+ {
+ scores.Cursor = new Cursor
{
- switch (request)
+ Properties = new Dictionary
{
- case GetRoomPlaylistScoresRequest r:
- if (delay == 0)
- success();
- else
- {
- Task.Run(async () =>
- {
- await Task.Delay(TimeSpan.FromMilliseconds(delay));
- Schedule(success);
- });
- }
+ { "total_score", JToken.FromObject(scores.Scores[^1].TotalScore) },
+ { "score_id", JToken.FromObject(scores.Scores[^1].ID) },
+ }
+ };
- void success()
- {
- r.TriggerSuccess(new RoomPlaylistScores { Scores = roomScores });
- roomsReceived = true;
- }
-
- break;
+ scores.Params = new IndexScoresParams
+ {
+ Properties = new Dictionary
+ {
+ { "sort", JToken.FromObject(scores.Scores[^1].TotalScore > scores.Scores[^2].TotalScore ? "score_asc" : "score_desc") }
}
};
}
+
+ private class TestResultsScreen : TimeshiftResultsScreen
+ {
+ public new LoadingSpinner LeftSpinner => base.LeftSpinner;
+ public new LoadingSpinner CentreSpinner => base.CentreSpinner;
+ public new LoadingSpinner RightSpinner => base.RightSpinner;
+ public new ScorePanelList ScorePanelList => base.ScorePanelList;
+
+ public TestResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true)
+ : base(score, roomId, playlistItem, allowRetry)
+ {
+ }
+ }
}
}
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/TestSceneShowMoreButton.cs b/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs
index 273f593c32..18ac415126 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs
@@ -4,19 +4,22 @@
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Framework.Allocation;
-using osu.Game.Graphics;
+using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneShowMoreButton : OsuTestScene
{
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
+
public TestSceneShowMoreButton()
{
- TestButton button = null;
+ ShowMoreButton button = null;
int fireCount = 0;
- Add(button = new TestButton
+ Add(button = new ShowMoreButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -46,16 +49,5 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("action fired twice", () => fireCount == 2);
AddAssert("is in loading state", () => button.IsLoading);
}
-
- private class TestButton : ShowMoreButton
- {
- [BackgroundDependencyLoader]
- private void load(OsuColour colors)
- {
- IdleColour = colors.YellowDark;
- HoverColour = colors.Yellow;
- ChevronIconColour = colors.Red;
- }
- }
}
}
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/TestSceneHueAnimation.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoAnimation.cs
similarity index 85%
rename from osu.Game.Tests/Visual/UserInterface/TestSceneHueAnimation.cs
rename to osu.Game.Tests/Visual/UserInterface/TestSceneLogoAnimation.cs
index 9c5888d072..155d043bf9 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneHueAnimation.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoAnimation.cs
@@ -11,14 +11,14 @@ using osu.Game.Graphics.Sprites;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
- public class TestSceneHueAnimation : OsuTestScene
+ public class TestSceneLogoAnimation : OsuTestScene
{
[BackgroundDependencyLoader]
private void load(LargeTextureStore textures)
{
- HueAnimation anim2;
+ LogoAnimation anim2;
- Add(anim2 = new HueAnimation
+ Add(anim2 = new LogoAnimation
{
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
@@ -26,9 +26,9 @@ namespace osu.Game.Tests.Visual.UserInterface
Colour = Colour4.White,
});
- HueAnimation anim;
+ LogoAnimation anim;
- Add(anim = new HueAnimation
+ Add(anim = new LogoAnimation
{
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
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.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
index 4aea7ff4c0..fa530ea2c4 100644
--- a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
+++ b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs
@@ -81,7 +81,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
{
}
- private class SettingsRoundDropdown : LadderSettingsDropdown
+ private class SettingsRoundDropdown : SettingsDropdown
{
public SettingsRoundDropdown(BindableList rounds)
{
diff --git a/osu.Game.Tournament/Screens/Ladder/Components/LadderSettingsDropdown.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderSettingsDropdown.cs
deleted file mode 100644
index 347e4d91e0..0000000000
--- a/osu.Game.Tournament/Screens/Ladder/Components/LadderSettingsDropdown.cs
+++ /dev/null
@@ -1,26 +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.Graphics.UserInterface;
-using osu.Game.Overlays.Settings;
-
-namespace osu.Game.Tournament.Screens.Ladder.Components
-{
- public class LadderSettingsDropdown : SettingsDropdown
- {
- protected override OsuDropdown CreateDropdown() => new DropdownControl();
-
- private new class DropdownControl : SettingsDropdown.DropdownControl
- {
- protected override DropdownMenu CreateMenu() => new Menu();
-
- private new class Menu : OsuDropdownMenu
- {
- public Menu()
- {
- MaxHeight = 200;
- }
- }
- }
- }
-}
diff --git a/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs b/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs
index a630e51e44..6604e3a313 100644
--- a/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs
+++ b/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs
@@ -6,11 +6,12 @@ using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Game.Overlays.Settings;
using osu.Game.Tournament.Models;
namespace osu.Game.Tournament.Screens.Ladder.Components
{
- public class SettingsTeamDropdown : LadderSettingsDropdown
+ public class SettingsTeamDropdown : SettingsDropdown
{
public SettingsTeamDropdown(BindableList teams)
{
diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs
new file mode 100644
index 0000000000..b80b4e45ed
--- /dev/null
+++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs
@@ -0,0 +1,303 @@
+// 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;
+ private readonly List linkedCancellationSources = new List();
+
+ ///
+ /// Updates all tracked using the current ruleset and mods.
+ ///
+ private void updateTrackedBindables()
+ {
+ cancelTrackedBindableUpdate();
+ trackedUpdateCancellationSource = new CancellationTokenSource();
+
+ foreach (var b in trackedBindables)
+ {
+ var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(trackedUpdateCancellationSource.Token, b.CancellationToken);
+ linkedCancellationSources.Add(linkedSource);
+
+ updateBindable(b, currentRuleset.Value, currentMods.Value, linkedSource.Token);
+ }
+ }
+
+ ///
+ /// Cancels the existing update of all tracked via .
+ ///
+ private void cancelTrackedBindableUpdate()
+ {
+ trackedUpdateCancellationSource?.Cancel();
+ trackedUpdateCancellationSource = null;
+
+ if (linkedCancellationSources != null)
+ {
+ foreach (var c in linkedCancellationSources)
+ c.Dispose();
+
+ linkedCancellationSources.Clear();
+ }
+ }
+
+ ///
+ /// 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;
+ }
+
+ ///
+ /// 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);
+ }
+
+ ///
+ /// 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);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ cancelTrackedBindableUpdate();
+ updateScheduler?.Dispose();
+ }
+
+ private readonly struct DifficultyCacheLookup : IEquatable
+ {
+ public readonly int BeatmapId;
+ public readonly int RulesetId;
+ public readonly Mod[] Mods;
+
+ public DifficultyCacheLookup(int beatmapId, int rulesetId, IEnumerable mods)
+ {
+ BeatmapId = beatmapId;
+ RulesetId = rulesetId;
+ Mods = mods?.OrderBy(m => m.Acronym).ToArray() ?? Array.Empty();
+ }
+
+ public bool Equals(DifficultyCacheLookup other)
+ => BeatmapId == other.BeatmapId
+ && RulesetId == other.RulesetId
+ && Mods.SequenceEqual(other.Mods);
+
+ public override int GetHashCode()
+ {
+ var hashCode = new HashCode();
+
+ hashCode.Add(BeatmapId);
+ hashCode.Add(RulesetId);
+ foreach (var mod in Mods)
+ hashCode.Add(mod.Acronym);
+
+ return hashCode.ToHashCode();
+ }
+ }
+
+ private class BindableStarDifficulty : Bindable
+ {
+ public readonly BeatmapInfo Beatmap;
+ public readonly CancellationToken CancellationToken;
+
+ public BindableStarDifficulty(BeatmapInfo beatmap, CancellationToken cancellationToken)
+ {
+ Beatmap = beatmap;
+ CancellationToken = cancellationToken;
+ }
+ }
+ }
+
+ public readonly struct StarDifficulty
+ {
+ public readonly double Stars;
+
+ public StarDifficulty(double stars)
+ {
+ Stars = stars;
+
+ // Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...)
+ }
+ }
+}
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index 268328272c..a8a8794320 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -99,6 +99,7 @@ namespace osu.Game.Configuration
Set(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised);
Set(OsuSetting.IncreaseFirstObjectVisibility, true);
+ Set(OsuSetting.GameplayDisableWinKey, true);
// Update
Set(OsuSetting.ReleaseStream, ReleaseStream.Lazer);
@@ -229,6 +230,7 @@ namespace osu.Game.Configuration
IntroSequence,
UIHoldActivationDelay,
HitLighting,
- MenuBackgroundSource
+ MenuBackgroundSource,
+ GameplayDisableWinKey
}
}
diff --git a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs
index b7ea1ba56a..3015c44613 100644
--- a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs
+++ b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@@ -55,7 +54,16 @@ namespace osu.Game.Graphics.Cursor
return;
}
- var newTarget = inputManager.HoveredDrawables.OfType().FirstOrDefault(t => t.ProvidingUserCursor) ?? this;
+ IProvideCursor newTarget = this;
+
+ foreach (var d in inputManager.HoveredDrawables)
+ {
+ if (d is IProvideCursor p && p.ProvidingUserCursor)
+ {
+ newTarget = p;
+ break;
+ }
+ }
if (currentTarget == newTarget)
return;
diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs
index 9804aefce8..d1f6fd445e 100644
--- a/osu.Game/Graphics/ScreenshotManager.cs
+++ b/osu.Game/Graphics/ScreenshotManager.cs
@@ -19,6 +19,7 @@ using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats.Jpeg;
namespace osu.Game.Graphics
{
@@ -119,7 +120,9 @@ namespace osu.Game.Graphics
break;
case ScreenshotFormat.Jpg:
- image.SaveAsJpeg(stream);
+ const int jpeg_quality = 92;
+
+ image.SaveAsJpeg(stream, new JpegEncoder { Quality = jpeg_quality });
break;
default:
diff --git a/osu.Game/Graphics/Sprites/HueAnimation.cs b/osu.Game/Graphics/Sprites/LogoAnimation.cs
similarity index 79%
rename from osu.Game/Graphics/Sprites/HueAnimation.cs
rename to osu.Game/Graphics/Sprites/LogoAnimation.cs
index 8ad68ace05..b1383065fe 100644
--- a/osu.Game/Graphics/Sprites/HueAnimation.cs
+++ b/osu.Game/Graphics/Sprites/LogoAnimation.cs
@@ -11,13 +11,13 @@ using osu.Framework.Graphics.Textures;
namespace osu.Game.Graphics.Sprites
{
- public class HueAnimation : Sprite
+ public class LogoAnimation : Sprite
{
[BackgroundDependencyLoader]
private void load(ShaderManager shaders, TextureStore textures)
{
- TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"HueAnimation");
- RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"HueAnimation"); // Masking isn't supported for now
+ TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"LogoAnimation");
+ RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"LogoAnimation"); // Masking isn't supported for now
}
private float animationProgress;
@@ -36,15 +36,15 @@ namespace osu.Game.Graphics.Sprites
public override bool IsPresent => true;
- protected override DrawNode CreateDrawNode() => new HueAnimationDrawNode(this);
+ protected override DrawNode CreateDrawNode() => new LogoAnimationDrawNode(this);
- private class HueAnimationDrawNode : SpriteDrawNode
+ private class LogoAnimationDrawNode : SpriteDrawNode
{
- private HueAnimation source => (HueAnimation)Source;
+ private LogoAnimation source => (LogoAnimation)Source;
private float progress;
- public HueAnimationDrawNode(HueAnimation source)
+ public LogoAnimationDrawNode(LogoAnimation source)
: base(source)
{
}
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/Graphics/UserInterface/ShowMoreButton.cs b/osu.Game/Graphics/UserInterface/ShowMoreButton.cs
index c9cd9f1158..924c7913f3 100644
--- a/osu.Game/Graphics/UserInterface/ShowMoreButton.cs
+++ b/osu.Game/Graphics/UserInterface/ShowMoreButton.cs
@@ -1,13 +1,15 @@
// 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.Sprites;
+using osu.Game.Overlays;
using osuTK;
-using osuTK.Graphics;
using System.Collections.Generic;
namespace osu.Game.Graphics.UserInterface
@@ -16,14 +18,6 @@ namespace osu.Game.Graphics.UserInterface
{
private const int duration = 200;
- private Color4 chevronIconColour;
-
- protected Color4 ChevronIconColour
- {
- get => chevronIconColour;
- set => chevronIconColour = leftChevron.Colour = rightChevron.Colour = value;
- }
-
public string Text
{
get => text.Text;
@@ -32,22 +26,28 @@ namespace osu.Game.Graphics.UserInterface
protected override IEnumerable EffectTargets => new[] { background };
- private ChevronIcon leftChevron;
- private ChevronIcon rightChevron;
+ private ChevronIcon leftIcon;
+ private ChevronIcon rightIcon;
private SpriteText text;
private Box background;
private FillFlowContainer textContainer;
public ShowMoreButton()
{
- Height = 30;
- Width = 140;
+ AutoSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
+ {
+ IdleColour = colourProvider.Background2;
+ HoverColour = colourProvider.Background1;
}
protected override Drawable CreateContent() => new CircularContainer
{
Masking = true,
- RelativeSizeAxes = Axes.Both,
+ AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
background = new Box
@@ -56,22 +56,36 @@ namespace osu.Game.Graphics.UserInterface
},
textContainer = new FillFlowContainer
{
+ AlwaysPresent = true,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
- Spacing = new Vector2(7),
+ Spacing = new Vector2(10),
+ Margin = new MarginPadding
+ {
+ Horizontal = 20,
+ Vertical = 5
+ },
Children = new Drawable[]
{
- leftChevron = new ChevronIcon(),
+ leftIcon = new ChevronIcon
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
+ Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
Text = "show more".ToUpper(),
},
- rightChevron = new ChevronIcon(),
+ rightIcon = new ChevronIcon
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
}
}
}
@@ -81,17 +95,40 @@ namespace osu.Game.Graphics.UserInterface
protected override void OnLoadFinished() => textContainer.FadeIn(duration, Easing.OutQuint);
- private class ChevronIcon : SpriteIcon
+ protected override bool OnHover(HoverEvent e)
{
- private const int icon_size = 8;
+ base.OnHover(e);
+ leftIcon.SetHoveredState(true);
+ rightIcon.SetHoveredState(true);
+ return true;
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ base.OnHoverLost(e);
+ leftIcon.SetHoveredState(false);
+ rightIcon.SetHoveredState(false);
+ }
+
+ public class ChevronIcon : SpriteIcon
+ {
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; }
public ChevronIcon()
{
- Anchor = Anchor.Centre;
- Origin = Anchor.Centre;
- Size = new Vector2(icon_size);
+ Size = new Vector2(7.5f);
Icon = FontAwesome.Solid.ChevronDown;
}
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Colour = colourProvider.Foreground1;
+ }
+
+ public void SetHoveredState(bool hovered) =>
+ this.FadeColour(hovered ? colourProvider.Light1 : colourProvider.Foreground1, 200, Easing.OutQuint);
}
}
}
diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs
index 2115326cc2..6912d9b629 100644
--- a/osu.Game/Online/API/APIRequest.cs
+++ b/osu.Game/Online/API/APIRequest.cs
@@ -136,6 +136,11 @@ namespace osu.Game.Online.API
Success?.Invoke();
}
+ internal void TriggerFailure(Exception e)
+ {
+ Failure?.Invoke(e);
+ }
+
public void Cancel() => Fail(new OperationCanceledException(@"Request cancelled"));
public void Fail(Exception e)
@@ -166,7 +171,7 @@ namespace osu.Game.Online.API
}
Logger.Log($@"Failing request {this} ({e})", LoggingTarget.Network);
- pendingFailure = () => Failure?.Invoke(e);
+ pendingFailure = () => TriggerFailure(e);
checkAndScheduleFailure();
}
diff --git a/osu.Game/Online/API/Requests/Cursor.cs b/osu.Game/Online/API/Requests/Cursor.cs
index f21445ca32..3de8db770c 100644
--- a/osu.Game/Online/API/Requests/Cursor.cs
+++ b/osu.Game/Online/API/Requests/Cursor.cs
@@ -15,6 +15,6 @@ namespace osu.Game.Online.API.Requests
{
[UsedImplicitly]
[JsonExtensionData]
- public IDictionary Properties;
+ public IDictionary Properties { get; set; } = new Dictionary();
}
}
diff --git a/osu.Game/Online/API/Requests/GetRoomPlaylistScoresRequest.cs b/osu.Game/Online/API/Requests/GetRoomPlaylistScoresRequest.cs
deleted file mode 100644
index 38f852870b..0000000000
--- a/osu.Game/Online/API/Requests/GetRoomPlaylistScoresRequest.cs
+++ /dev/null
@@ -1,28 +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 Newtonsoft.Json;
-
-namespace osu.Game.Online.API.Requests
-{
- public class GetRoomPlaylistScoresRequest : APIRequest
- {
- private readonly int roomId;
- private readonly int playlistItemId;
-
- public GetRoomPlaylistScoresRequest(int roomId, int playlistItemId)
- {
- this.roomId = roomId;
- this.playlistItemId = playlistItemId;
- }
-
- protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores";
- }
-
- public class RoomPlaylistScores
- {
- [JsonProperty("scores")]
- public List Scores { get; set; }
- }
-}
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 77%
rename from osu.Game/Online/API/Requests/CreateRoomScoreRequest.cs
rename to osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs
index e6246b4f1f..2d99b12519 100644
--- a/osu.Game/Online/API/Requests/CreateRoomScoreRequest.cs
+++ b/osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs
@@ -3,25 +3,28 @@
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
{
private readonly int roomId;
private readonly int playlistItemId;
+ private readonly string versionHash;
- public CreateRoomScoreRequest(int roomId, int playlistItemId)
+ public CreateRoomScoreRequest(int roomId, int playlistItemId, string versionHash)
{
this.roomId = roomId;
this.playlistItemId = playlistItemId;
+ this.versionHash = versionHash;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.Method = HttpMethod.Post;
+ req.AddParameter("version_hash", versionHash);
return req;
}
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/Multiplayer/IndexPlaylistScoresRequest.cs b/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs
new file mode 100644
index 0000000000..684d0aecd8
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs
@@ -0,0 +1,59 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Diagnostics;
+using JetBrains.Annotations;
+using osu.Framework.IO.Network;
+using osu.Game.Extensions;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+
+namespace osu.Game.Online.Multiplayer
+{
+ ///
+ /// Returns a list of scores for the specified playlist item.
+ ///
+ public class IndexPlaylistScoresRequest : APIRequest
+ {
+ public readonly int RoomId;
+ public readonly int PlaylistItemId;
+
+ [CanBeNull]
+ public readonly Cursor Cursor;
+
+ [CanBeNull]
+ public readonly IndexScoresParams IndexParams;
+
+ public IndexPlaylistScoresRequest(int roomId, int playlistItemId)
+ {
+ RoomId = roomId;
+ PlaylistItemId = playlistItemId;
+ }
+
+ public IndexPlaylistScoresRequest(int roomId, int playlistItemId, [NotNull] Cursor cursor, [NotNull] IndexScoresParams indexParams)
+ : this(roomId, playlistItemId)
+ {
+ Cursor = cursor;
+ IndexParams = indexParams;
+ }
+
+ protected override WebRequest CreateWebRequest()
+ {
+ var req = base.CreateWebRequest();
+
+ if (Cursor != null)
+ {
+ Debug.Assert(IndexParams != null);
+
+ req.AddCursor(Cursor);
+
+ foreach (var (key, value) in IndexParams.Properties)
+ req.AddParameter(key, value.ToString());
+ }
+
+ return req;
+ }
+
+ protected override string Target => $@"rooms/{RoomId}/playlist/{PlaylistItemId}/scores";
+ }
+}
diff --git a/osu.Game/Online/Multiplayer/IndexScoresParams.cs b/osu.Game/Online/Multiplayer/IndexScoresParams.cs
new file mode 100644
index 0000000000..a511e9a780
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/IndexScoresParams.cs
@@ -0,0 +1,20 @@
+// 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 JetBrains.Annotations;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace osu.Game.Online.Multiplayer
+{
+ ///
+ /// A collection of parameters which should be passed to the index endpoint to fetch the next page.
+ ///
+ public class IndexScoresParams
+ {
+ [UsedImplicitly]
+ [JsonExtensionData]
+ public IDictionary Properties { get; set; } = new Dictionary();
+ }
+}
diff --git a/osu.Game/Online/Multiplayer/IndexedMultiplayerScores.cs b/osu.Game/Online/Multiplayer/IndexedMultiplayerScores.cs
new file mode 100644
index 0000000000..e237b7e3fb
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/IndexedMultiplayerScores.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 JetBrains.Annotations;
+using Newtonsoft.Json;
+
+namespace osu.Game.Online.Multiplayer
+{
+ ///
+ /// A object returned via a .
+ ///
+ public class IndexedMultiplayerScores : MultiplayerScores
+ {
+ ///
+ /// The total scores in the playlist item.
+ ///
+ [JsonProperty("total")]
+ public int? TotalScores { get; set; }
+
+ ///
+ /// The user's score, if any.
+ ///
+ [JsonProperty("user_score")]
+ [CanBeNull]
+ public MultiplayerScore UserScore { get; set; }
+ }
+}
diff --git a/osu.Game/Online/API/Requests/JoinRoomRequest.cs b/osu.Game/Online/Multiplayer/JoinRoomRequest.cs
similarity index 90%
rename from osu.Game/Online/API/Requests/JoinRoomRequest.cs
rename to osu.Game/Online/Multiplayer/JoinRoomRequest.cs
index b0808afa45..74375af856 100644
--- a/osu.Game/Online/API/Requests/JoinRoomRequest.cs
+++ b/osu.Game/Online/Multiplayer/JoinRoomRequest.cs
@@ -3,9 +3,9 @@
using System.Net.Http;
using osu.Framework.IO.Network;
-using osu.Game.Online.Multiplayer;
+using osu.Game.Online.API;
-namespace osu.Game.Online.API.Requests
+namespace osu.Game.Online.Multiplayer
{
public class JoinRoomRequest : APIRequest
{
diff --git a/osu.Game/Online/API/RoomScore.cs b/osu.Game/Online/Multiplayer/MultiplayerScore.cs
similarity index 78%
rename from osu.Game/Online/API/RoomScore.cs
rename to osu.Game/Online/Multiplayer/MultiplayerScore.cs
index 3c7f8c9833..8191003aad 100644
--- a/osu.Game/Online/API/RoomScore.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerScore.cs
@@ -4,17 +4,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using JetBrains.Annotations;
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; }
@@ -47,6 +48,19 @@ namespace osu.Game.Online.API
[JsonProperty("ended_at")]
public DateTimeOffset EndedAt { get; set; }
+ ///
+ /// The position of this score, starting at 1.
+ ///
+ [JsonProperty("position")]
+ public int? Position { get; set; }
+
+ ///
+ /// Any scores in the room around this score.
+ ///
+ [JsonProperty("scores_around")]
+ [CanBeNull]
+ public MultiplayerScoresAround ScoresAround { get; set; }
+
public ScoreInfo CreateScoreInfo(PlaylistItem playlistItem)
{
var rulesetInstance = playlistItem.Ruleset.Value.CreateInstance();
@@ -66,7 +80,8 @@ namespace osu.Game.Online.API
Date = EndedAt,
Hash = string.Empty, // todo: temporary?
Rank = Rank,
- Mods = Mods?.Select(m => m.ToMod(rulesetInstance)).ToArray() ?? Array.Empty()
+ Mods = Mods?.Select(m => m.ToMod(rulesetInstance)).ToArray() ?? Array.Empty(),
+ Position = Position,
};
return scoreInfo;
diff --git a/osu.Game/Online/Multiplayer/MultiplayerScores.cs b/osu.Game/Online/Multiplayer/MultiplayerScores.cs
new file mode 100644
index 0000000000..7b9dcff828
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/MultiplayerScores.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 System.Collections.Generic;
+using Newtonsoft.Json;
+using osu.Game.Online.API.Requests;
+
+namespace osu.Game.Online.Multiplayer
+{
+ ///
+ /// An object which contains scores and related data for fetching next pages.
+ ///
+ public class MultiplayerScores : ResponseWithCursor
+ {
+ ///
+ /// The scores.
+ ///
+ [JsonProperty("scores")]
+ public List Scores { get; set; } = new List();
+
+ ///
+ /// The parameters to be used to fetch the next page.
+ ///
+ [JsonProperty("params")]
+ public IndexScoresParams Params { get; set; } = new IndexScoresParams();
+ }
+}
diff --git a/osu.Game/Online/Multiplayer/MultiplayerScoresAround.cs b/osu.Game/Online/Multiplayer/MultiplayerScoresAround.cs
new file mode 100644
index 0000000000..2ac62d0300
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/MultiplayerScoresAround.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 JetBrains.Annotations;
+using Newtonsoft.Json;
+
+namespace osu.Game.Online.Multiplayer
+{
+ ///
+ /// An object which stores scores higher and lower than the user's score.
+ ///
+ public class MultiplayerScoresAround
+ {
+ ///
+ /// Scores sorted "higher" than the user's score, depending on the sorting order.
+ ///
+ [JsonProperty("higher")]
+ [CanBeNull]
+ public MultiplayerScores Higher { get; set; }
+
+ ///
+ /// Scores sorted "lower" than the user's score, depending on the sorting order.
+ ///
+ [JsonProperty("lower")]
+ [CanBeNull]
+ public MultiplayerScores Lower { get; set; }
+ }
+}
diff --git a/osu.Game/Online/API/Requests/PartRoomRequest.cs b/osu.Game/Online/Multiplayer/PartRoomRequest.cs
similarity index 90%
rename from osu.Game/Online/API/Requests/PartRoomRequest.cs
rename to osu.Game/Online/Multiplayer/PartRoomRequest.cs
index c988cd5c9e..54bb005d96 100644
--- a/osu.Game/Online/API/Requests/PartRoomRequest.cs
+++ b/osu.Game/Online/Multiplayer/PartRoomRequest.cs
@@ -3,9 +3,9 @@
using System.Net.Http;
using osu.Framework.IO.Network;
-using osu.Game.Online.Multiplayer;
+using osu.Game.Online.API;
-namespace osu.Game.Online.API.Requests
+namespace osu.Game.Online.Multiplayer
{
public class PartRoomRequest : APIRequest
{
diff --git a/osu.Game/Online/Multiplayer/ShowPlaylistUserScoreRequest.cs b/osu.Game/Online/Multiplayer/ShowPlaylistUserScoreRequest.cs
new file mode 100644
index 0000000000..936b8bbe89
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/ShowPlaylistUserScoreRequest.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.Online.API;
+
+namespace osu.Game.Online.Multiplayer
+{
+ public class ShowPlaylistUserScoreRequest : APIRequest
+ {
+ private readonly int roomId;
+ private readonly int playlistItemId;
+ private readonly long userId;
+
+ public ShowPlaylistUserScoreRequest(int roomId, int playlistItemId, long userId)
+ {
+ this.roomId = roomId;
+ this.playlistItemId = playlistItemId;
+ this.userId = userId;
+ }
+
+ protected override string Target => $"rooms/{roomId}/playlist/{playlistItemId}/scores/users/{userId}";
+ }
+}
diff --git a/osu.Game/Online/API/Requests/SubmitRoomScoreRequest.cs b/osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs
similarity index 90%
rename from osu.Game/Online/API/Requests/SubmitRoomScoreRequest.cs
rename to osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs
index 8eb2952159..d31aef2ea5 100644
--- a/osu.Game/Online/API/Requests/SubmitRoomScoreRequest.cs
+++ b/osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs
@@ -4,11 +4,12 @@
using System.Net.Http;
using Newtonsoft.Json;
using osu.Framework.IO.Network;
+using osu.Game.Online.API;
using osu.Game.Scoring;
-namespace osu.Game.Online.API.Requests
+namespace osu.Game.Online.Multiplayer
{
- public class SubmitRoomScoreRequest : APIRequest
+ public class SubmitRoomScoreRequest : APIRequest
{
private readonly int scoreId;
private readonly int roomId;
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index f4bb10340e..d6a07651e2 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -18,6 +18,7 @@ using osu.Game.Screens.Menu;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Humanizer;
using JetBrains.Annotations;
using osu.Framework.Audio;
using osu.Framework.Bindables;
@@ -759,7 +760,7 @@ namespace osu.Game
Schedule(() => notifications.Post(new SimpleNotification
{
Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb,
- Text = entry.Message + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty),
+ Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty),
}));
}
else if (recentLogCount == short_term_display_limit)
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index dd120937af..278f2d849f 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -11,6 +11,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Development;
+using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.IO.Stores;
@@ -97,6 +98,11 @@ namespace osu.Game
public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version();
+ ///
+ /// MD5 representation of the game executable.
+ ///
+ public string VersionHash { get; private set; }
+
public bool IsDeployedBuild => AssemblyVersion.Major > 0;
public virtual string Version
@@ -128,6 +134,9 @@ namespace osu.Game
[BackgroundDependencyLoader]
private void load()
{
+ using (var str = File.OpenRead(typeof(OsuGameBase).Assembly.Location))
+ VersionHash = str.ComputeMD5Hash();
+
Resources.AddStore(new DllResourceStore(OsuResources.ResourceAssembly));
dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage));
@@ -199,6 +208,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/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
index f7e0cb0a6c..57bf2af4d2 100644
--- a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs
+++ b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs
@@ -5,12 +5,12 @@ 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;
+using static osu.Game.Graphics.UserInterface.ShowMoreButton;
namespace osu.Game.Overlays.Comments.Buttons
{
@@ -25,17 +25,13 @@ namespace osu.Game.Overlays.Comments.Buttons
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
- private readonly SpriteIcon icon;
+ private readonly ChevronIcon icon;
private readonly Box background;
private readonly OsuSpriteText text;
protected CommentRepliesButton()
{
AutoSizeAxes = Axes.Both;
- Margin = new MarginPadding
- {
- Vertical = 2
- };
InternalChildren = new Drawable[]
{
new CircularContainer
@@ -72,12 +68,10 @@ namespace osu.Game.Overlays.Comments.Buttons
AlwaysPresent = true,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold)
},
- icon = new SpriteIcon
+ icon = new ChevronIcon
{
Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Size = new Vector2(7.5f),
- Icon = FontAwesome.Solid.ChevronDown
+ Origin = Anchor.CentreLeft
}
}
}
@@ -92,7 +86,6 @@ namespace osu.Game.Overlays.Comments.Buttons
private void load()
{
background.Colour = colourProvider.Background2;
- icon.Colour = colourProvider.Foreground1;
}
protected void SetIconDirection(bool upwards) => icon.ScaleTo(new Vector2(1, upwards ? -1 : 1));
@@ -103,7 +96,7 @@ namespace osu.Game.Overlays.Comments.Buttons
{
base.OnHover(e);
background.FadeColour(colourProvider.Background1, 200, Easing.OutQuint);
- icon.FadeColour(colourProvider.Light1, 200, Easing.OutQuint);
+ icon.SetHoveredState(true);
return true;
}
@@ -111,7 +104,7 @@ namespace osu.Game.Overlays.Comments.Buttons
{
base.OnHoverLost(e);
background.FadeColour(colourProvider.Background2, 200, Easing.OutQuint);
- icon.FadeColour(colourProvider.Foreground1, 200, Easing.OutQuint);
+ icon.SetHoveredState(false);
}
}
}
diff --git a/osu.Game/Overlays/Comments/GetCommentRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/ShowMoreRepliesButton.cs
similarity index 68%
rename from osu.Game/Overlays/Comments/GetCommentRepliesButton.cs
rename to osu.Game/Overlays/Comments/Buttons/ShowMoreRepliesButton.cs
index a3817ba416..c115a8bb8f 100644
--- a/osu.Game/Overlays/Comments/GetCommentRepliesButton.cs
+++ b/osu.Game/Overlays/Comments/Buttons/ShowMoreRepliesButton.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 ShowMoreRepliesButton : LoadingButton
{
- private const int duration = 200;
-
protected override IEnumerable EffectTargets => new[] { text };
private OsuSpriteText text;
- protected GetCommentRepliesButton()
+ public ShowMoreRepliesButton()
{
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/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs
index f71808ba89..2a78748be6 100644
--- a/osu.Game/Overlays/Comments/CommentsContainer.cs
+++ b/osu.Game/Overlays/Comments/CommentsContainer.cs
@@ -78,21 +78,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
{
@@ -102,7 +103,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/CommentsShowMoreButton.cs b/osu.Game/Overlays/Comments/CommentsShowMoreButton.cs
index d2ff7ecb1f..adf64eabb1 100644
--- a/osu.Game/Overlays/Comments/CommentsShowMoreButton.cs
+++ b/osu.Game/Overlays/Comments/CommentsShowMoreButton.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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Graphics.UserInterface;
@@ -11,16 +10,6 @@ namespace osu.Game.Overlays.Comments
{
public readonly BindableInt Current = new BindableInt();
- [BackgroundDependencyLoader]
- private void load(OverlayColourProvider colourProvider)
- {
- Height = 20;
-
- IdleColour = colourProvider.Background2;
- HoverColour = colourProvider.Background1;
- ChevronIconColour = colourProvider.Foreground1;
- }
-
protected override void LoadComplete()
{
Current.BindValueChanged(onCurrentChanged, 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 3cdc0a0cbd..31aa41e967 100644
--- a/osu.Game/Overlays/Comments/DrawableComment.cs
+++ b/osu.Game/Overlays/Comments/DrawableComment.cs
@@ -28,7 +28,6 @@ namespace osu.Game.Overlays.Comments
public class DrawableComment : CompositeDrawable
{
private const int avatar_size = 40;
- private const int margin = 10;
public Action RepliesRequested;
@@ -47,7 +46,7 @@ namespace osu.Game.Overlays.Comments
private FillFlowContainer childCommentsVisibilityContainer;
private FillFlowContainer childCommentsContainer;
private LoadRepliesButton loadRepliesButton;
- private ShowMoreButton showMoreButton;
+ private ShowMoreRepliesButton showMoreButton;
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, Top = Comment.IsTopLevel ? 10 : 0 },
- 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,93 +98,84 @@ 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
},
- new FillFlowContainer
+ info = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Vertical,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
- info = new FillFlowContainer
+ new DrawableDate(Comment.CreatedAt, 12, false)
{
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Horizontal,
- Spacing = new Vector2(10, 0),
- Children = new Drawable[]
- {
- new OsuSpriteText
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft,
- Font = OsuFont.GetFont(size: 12),
- Colour = OsuColour.Gray(0.7f),
- Text = HumanizerUtils.Humanize(Comment.CreatedAt)
- },
- }
- },
+ Colour = colourProvider.Foreground1
+ }
+ }
+ },
+ new Container
+ {
+ AutoSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
showRepliesButton = new ShowRepliesButton(Comment.RepliesCount)
{
Expanded = { BindTarget = childrenExpanded }
@@ -200,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 ShowMoreRepliesButton
+ {
+ 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
+ }
}
};
@@ -247,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
});
}
@@ -357,35 +356,21 @@ namespace osu.Game.Overlays.Comments
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 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/MusicController.cs b/osu.Game/Overlays/MusicController.cs
index 546f7a1ec4..a990f9a6ab 100644
--- a/osu.Game/Overlays/MusicController.cs
+++ b/osu.Game/Overlays/MusicController.cs
@@ -236,8 +236,8 @@ namespace osu.Game.Overlays
{
if (beatmap is Bindable working)
working.Value = beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value);
- beatmap.Value.Track.Restart();
+ restartTrack();
return PreviousTrackResult.Previous;
}
@@ -262,13 +262,21 @@ namespace osu.Game.Overlays
{
if (beatmap is Bindable working)
working.Value = beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value);
- beatmap.Value.Track.Restart();
+
+ restartTrack();
return true;
}
return false;
}
+ private void restartTrack()
+ {
+ // if not scheduled, the previously track will be stopped one frame later (see ScheduleAfterChildren logic in GameBase).
+ // we probably want to move this to a central method for switching to a new working beatmap in the future.
+ Schedule(() => beatmap.Value.Track.Restart());
+ }
+
private WorkingBeatmap current;
private TrackChangeDirection? queuedDirection;
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..55474c9d3e 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");
@@ -39,7 +41,7 @@ namespace osu.Game.Overlays.Profile
Masking = true,
Children = new Drawable[]
{
- coverContainer = new UserCoverBackground
+ coverContainer = new ProfileCoverBackground
{
RelativeSizeAxes = Axes.Both,
},
@@ -98,5 +100,10 @@ namespace osu.Game.Overlays.Profile
IconTexture = "Icons/profile";
}
}
+
+ private class ProfileCoverBackground : UserCoverBackground
+ {
+ protected override double LoadDelay => 0;
+ }
}
}
diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
index a30ff786fb..9720469548 100644
--- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
@@ -14,12 +14,13 @@ using osu.Game.Users;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
+using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Profile.Sections
{
public abstract class PaginatedContainer : FillFlowContainer
{
- private readonly ProfileShowMoreButton moreButton;
+ private readonly ShowMoreButton moreButton;
private readonly OsuSpriteText missingText;
private APIRequest> retrievalRequest;
private CancellationTokenSource loadCancellation;
@@ -74,7 +75,7 @@ namespace osu.Game.Overlays.Profile.Sections
RelativeSizeAxes = Axes.X,
Spacing = new Vector2(0, 2),
},
- moreButton = new ProfileShowMoreButton
+ moreButton = new ShowMoreButton
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
diff --git a/osu.Game/Overlays/Profile/Sections/ProfileShowMoreButton.cs b/osu.Game/Overlays/Profile/Sections/ProfileShowMoreButton.cs
deleted file mode 100644
index 426ebeebe6..0000000000
--- a/osu.Game/Overlays/Profile/Sections/ProfileShowMoreButton.cs
+++ /dev/null
@@ -1,19 +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.Allocation;
-using osu.Game.Graphics.UserInterface;
-
-namespace osu.Game.Overlays.Profile.Sections
-{
- public class ProfileShowMoreButton : ShowMoreButton
- {
- [BackgroundDependencyLoader]
- private void load(OverlayColourProvider colourProvider)
- {
- IdleColour = colourProvider.Background2;
- HoverColour = colourProvider.Background1;
- ChevronIconColour = colourProvider.Foreground1;
- }
- }
-}
diff --git a/osu.Game/Overlays/Rankings/SpotlightSelector.cs b/osu.Game/Overlays/Rankings/SpotlightSelector.cs
index f112c1ec43..422373d099 100644
--- a/osu.Game/Overlays/Rankings/SpotlightSelector.cs
+++ b/osu.Game/Overlays/Rankings/SpotlightSelector.cs
@@ -18,10 +18,8 @@ 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 BindableWithCurrent current = new BindableWithCurrent();
public readonly Bindable Sort = new Bindable();
@@ -37,10 +35,7 @@ namespace osu.Game.Overlays.Rankings
set => dropdown.Items = value;
}
- protected override bool StartHidden => true;
-
private readonly Box background;
- private readonly Container content;
private readonly SpotlightsDropdown dropdown;
private readonly InfoColumn startDateColumn;
private readonly InfoColumn endDateColumn;
@@ -51,73 +46,68 @@ namespace osu.Game.Overlays.Rankings
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
- Add(content = new Container
+ InternalChildren = new Drawable[]
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Children = new Drawable[]
+ background = new Box
{
- background = new Box
- {
- RelativeSizeAxes = Axes.Both,
- },
- new Container
+ RelativeSizeAxes = Axes.Both,
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN },
+ Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN },
- Child = new FillFlowContainer
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
+ new Container
{
- new Container
- {
- Margin = new MarginPadding { Vertical = 20 },
- RelativeSizeAxes = Axes.X,
- Height = 40,
- Depth = -float.MaxValue,
- Child = dropdown = new SpotlightsDropdown
- {
- RelativeSizeAxes = Axes.X,
- Current = Current
- }
- },
- new Container
+ Margin = new MarginPadding { Vertical = 20 },
+ RelativeSizeAxes = Axes.X,
+ Height = 40,
+ Depth = -float.MaxValue,
+ Child = dropdown = new SpotlightsDropdown
{
RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Children = new Drawable[]
+ Current = Current
+ }
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Children = new Drawable[]
+ {
+ new FillFlowContainer
{
- new FillFlowContainer
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(10, 0),
+ Margin = new MarginPadding { Bottom = 5 },
+ Children = new Drawable[]
{
- 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
+ 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]
@@ -134,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
diff --git a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs
index 0f9b07bf89..61339df76f 100644
--- a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs
+++ b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs
@@ -81,8 +81,6 @@ namespace osu.Game.Overlays.Rankings
{
base.LoadComplete();
- selector.Show();
-
selectedSpotlight.BindValueChanged(_ => onSpotlightChanged());
sort.BindValueChanged(_ => onSpotlightChanged());
Ruleset.BindValueChanged(onRulesetChanged);
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/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
index 04390a1193..596d3a9801 100644
--- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
@@ -116,8 +116,6 @@ namespace osu.Game.Overlays.Settings.Sections
private class SkinDropdownControl : DropdownControl
{
protected override string GenerateItemText(SkinInfo item) => item.ToString();
-
- protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = 200);
}
}
diff --git a/osu.Game/Overlays/Settings/SettingsDropdown.cs b/osu.Game/Overlays/Settings/SettingsDropdown.cs
index 167061f485..1175ddaab8 100644
--- a/osu.Game/Overlays/Settings/SettingsDropdown.cs
+++ b/osu.Game/Overlays/Settings/SettingsDropdown.cs
@@ -38,6 +38,8 @@ namespace osu.Game.Overlays.Settings
Margin = new MarginPadding { Top = 5 };
RelativeSizeAxes = Axes.X;
}
+
+ protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = 200);
}
}
}
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/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs
index 7fe606d584..65f1a972ed 100644
--- a/osu.Game/Rulesets/Mods/ModPerfect.cs
+++ b/osu.Game/Rulesets/Mods/ModPerfect.cs
@@ -17,6 +17,7 @@ namespace osu.Game.Rulesets.Mods
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
=> !(result.Judgement is IgnoreJudgement)
+ && result.Judgement.AffectsCombo
&& result.Type != result.Judgement.MaxResult;
}
}
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index b633cb0860..581617b567 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -126,12 +126,12 @@ namespace osu.Game.Rulesets.Objects.Drawables
if (Result == null)
throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}.");
- loadSamples();
+ LoadSamples();
}
- protected override void LoadComplete()
+ protected override void LoadAsyncComplete()
{
- base.LoadComplete();
+ base.LoadAsyncComplete();
HitObject.DefaultsApplied += onDefaultsApplied;
@@ -145,14 +145,19 @@ namespace osu.Game.Rulesets.Objects.Drawables
}
samplesBindable = HitObject.SamplesBindable.GetBoundCopy();
- samplesBindable.CollectionChanged += (_, __) => loadSamples();
+ samplesBindable.CollectionChanged += (_, __) => LoadSamples();
apply(HitObject);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
updateState(ArmedState.Idle, true);
}
- private void loadSamples()
+ protected virtual void LoadSamples()
{
if (Samples != null)
{
@@ -353,17 +358,32 @@ namespace osu.Game.Rulesets.Objects.Drawables
[Resolved(canBeNull: true)]
private GameplayClock gameplayClock { get; set; }
+ ///
+ /// Calculate the position to be used for sample playback at a specified X position (0..1).
+ ///
+ /// The lookup X position. Generally should be .
+ ///
+ protected double CalculateSamplePlaybackBalance(double position)
+ {
+ const float balance_adjust_amount = 0.4f;
+
+ return balance_adjust_amount * (userPositionalHitSounds.Value ? position - 0.5f : 0);
+ }
+
+ ///
+ /// Whether samples should currently be playing. Will be false during seek operations.
+ ///
+ protected bool ShouldPlaySamples => gameplayClock?.IsSeeking != true;
+
///
/// Plays all the hit sounds for this .
/// This is invoked automatically when this is hit.
///
public virtual void PlaySamples()
{
- const float balance_adjust_amount = 0.4f;
-
- if (Samples != null && gameplayClock?.IsSeeking != true)
+ if (Samples != null && ShouldPlaySamples)
{
- Samples.Balance.Value = balance_adjust_amount * (userPositionalHitSounds.Value ? SamplePlaybackPosition - 0.5f : 0);
+ Samples.Balance.Value = CalculateSamplePlaybackBalance(SamplePlaybackPosition);
Samples.Play();
}
}
diff --git a/osu.Game/Rulesets/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/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs
index 58a2ba056e..dd43092c0d 100644
--- a/osu.Game/Rulesets/RulesetStore.cs
+++ b/osu.Game/Rulesets/RulesetStore.cs
@@ -65,11 +65,15 @@ namespace osu.Game.Rulesets
// the requesting assembly may be located out of the executable's base directory, thus requiring manual resolving of its dependencies.
// this attempts resolving the ruleset dependencies on game core and framework assemblies by returning assemblies with the same assembly name
// already loaded in the AppDomain.
- foreach (var curAsm in AppDomain.CurrentDomain.GetAssemblies())
- {
- if (asm.Name.Equals(curAsm.GetName().Name, StringComparison.Ordinal))
- return curAsm;
- }
+ var domainAssembly = AppDomain.CurrentDomain.GetAssemblies()
+ // Given name is always going to be equally-or-more qualified than the assembly name.
+ .Where(a => args.Name.Contains(a.GetName().Name, StringComparison.Ordinal))
+ // Pick the greatest assembly version.
+ .OrderByDescending(a => a.GetName().Version)
+ .FirstOrDefault();
+
+ if (domainAssembly != null)
+ return domainAssembly;
return loadedAssemblies.Keys.FirstOrDefault(a => a.FullName == asm.FullName);
}
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/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs
index 84c0d5b54e..efcf1737c9 100644
--- a/osu.Game/Scoring/ScoreInfo.cs
+++ b/osu.Game/Scoring/ScoreInfo.cs
@@ -179,6 +179,13 @@ namespace osu.Game.Scoring
[JsonIgnore]
public bool DeletePending { get; set; }
+ ///
+ /// The position of this score, starting at 1.
+ ///
+ [NotMapped]
+ [JsonProperty("position")]
+ public int? Position { get; set; }
+
[Serializable]
protected class DeserializedMod : IMod
{
diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs
index b56ba6c8a4..a9ef20436f 100644
--- a/osu.Game/Screens/Menu/IntroTriangles.cs
+++ b/osu.Game/Screens/Menu/IntroTriangles.cs
@@ -260,7 +260,7 @@ namespace osu.Game.Screens.Menu
private class LazerLogo : CompositeDrawable
{
- private HueAnimation highlight, background;
+ private LogoAnimation highlight, background;
public float Progress
{
@@ -282,13 +282,13 @@ namespace osu.Game.Screens.Menu
{
InternalChildren = new Drawable[]
{
- highlight = new HueAnimation
+ highlight = new LogoAnimation
{
RelativeSizeAxes = Axes.Both,
Texture = textures.Get(@"Intro/Triangles/logo-highlight"),
Colour = Color4.White,
},
- background = new HueAnimation
+ background = new LogoAnimation
{
RelativeSizeAxes = Axes.Both,
Texture = textures.Get(@"Intro/Triangles/logo-background"),
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/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs
index cf0197d26b..da082692d7 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;
@@ -59,7 +58,7 @@ namespace osu.Game.Screens.Multi.Play
if (!playlistItem.RequiredMods.All(m => Mods.Value.Any(m.Equals)))
throw new InvalidOperationException("Current Mods do not match PlaylistItem's RequiredMods");
- var req = new CreateRoomScoreRequest(roomId.Value ?? 0, playlistItem.ID);
+ var req = new CreateRoomScoreRequest(roomId.Value ?? 0, playlistItem.ID, Game.VersionHash);
req.Success += r => token = r.ID;
req.Failure += e =>
{
diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs
index 5cafc974f1..8da6a530a8 100644
--- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs
+++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs
@@ -3,13 +3,14 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
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;
@@ -21,7 +22,15 @@ namespace osu.Game.Screens.Multi.Ranking
private readonly int roomId;
private readonly PlaylistItem playlistItem;
- private LoadingSpinner loadingLayer;
+ protected LoadingSpinner LeftSpinner { get; private set; }
+ protected LoadingSpinner CentreSpinner { get; private set; }
+ protected LoadingSpinner RightSpinner { get; private set; }
+
+ private MultiplayerScores higherScores;
+ private MultiplayerScores lowerScores;
+
+ [Resolved]
+ private IAPIProvider api { get; set; }
public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true)
: base(score, allowRetry)
@@ -33,29 +42,209 @@ namespace osu.Game.Screens.Multi.Ranking
[BackgroundDependencyLoader]
private void load()
{
- AddInternal(loadingLayer = new LoadingLayer
+ AddInternal(new Container
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- X = -10,
- State = { Value = Score == null ? Visibility.Visible : Visibility.Hidden },
- Padding = new MarginPadding { Bottom = TwoLayerButton.SIZE_EXTENDED.Y }
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Bottom = TwoLayerButton.SIZE_EXTENDED.Y },
+ Children = new Drawable[]
+ {
+ LeftSpinner = new PanelListLoadingSpinner(ScorePanelList)
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.Centre,
+ },
+ CentreSpinner = new PanelListLoadingSpinner(ScorePanelList)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ State = { Value = Score == null ? Visibility.Visible : Visibility.Hidden },
+ },
+ RightSpinner = new PanelListLoadingSpinner(ScorePanelList)
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.Centre,
+ },
+ }
});
}
protected override APIRequest FetchScores(Action> scoresCallback)
{
- var req = new GetRoomPlaylistScoresRequest(roomId, playlistItem.ID);
+ // This performs two requests:
+ // 1. A request to show the user's score (and scores around).
+ // 2. If that fails, a request to index the room starting from the highest score.
- req.Success += r =>
+ var userScoreReq = new ShowPlaylistUserScoreRequest(roomId, playlistItem.ID, api.LocalUser.Value.Id);
+
+ userScoreReq.Success += userScore =>
{
- scoresCallback?.Invoke(r.Scores.Where(s => s.ID != Score?.OnlineScoreID).Select(s => s.CreateScoreInfo(playlistItem)));
- loadingLayer.Hide();
+ var allScores = new List { userScore };
+
+ if (userScore.ScoresAround?.Higher != null)
+ {
+ allScores.AddRange(userScore.ScoresAround.Higher.Scores);
+ higherScores = userScore.ScoresAround.Higher;
+
+ Debug.Assert(userScore.Position != null);
+ setPositions(higherScores, userScore.Position.Value, -1);
+ }
+
+ if (userScore.ScoresAround?.Lower != null)
+ {
+ allScores.AddRange(userScore.ScoresAround.Lower.Scores);
+ lowerScores = userScore.ScoresAround.Lower;
+
+ Debug.Assert(userScore.Position != null);
+ setPositions(lowerScores, userScore.Position.Value, 1);
+ }
+
+ performSuccessCallback(scoresCallback, allScores);
};
- req.Failure += _ => loadingLayer.Hide();
+ // On failure, fallback to a normal index.
+ userScoreReq.Failure += _ => api.Queue(createIndexRequest(scoresCallback));
- return req;
+ return userScoreReq;
+ }
+
+ protected override APIRequest FetchNextPage(int direction, Action> scoresCallback)
+ {
+ Debug.Assert(direction == 1 || direction == -1);
+
+ MultiplayerScores pivot = direction == -1 ? higherScores : lowerScores;
+
+ if (pivot?.Cursor == null)
+ return null;
+
+ if (pivot == higherScores)
+ LeftSpinner.Show();
+ else
+ RightSpinner.Show();
+
+ return createIndexRequest(scoresCallback, pivot);
+ }
+
+ ///
+ /// Creates a with an optional score pivot.
+ ///
+ /// Does not queue the request.
+ /// The callback to perform with the resulting scores.
+ /// An optional score pivot to retrieve scores around. Can be null to retrieve scores from the highest score.
+ /// The indexing .
+ private APIRequest createIndexRequest(Action> scoresCallback, [CanBeNull] MultiplayerScores pivot = null)
+ {
+ var indexReq = pivot != null
+ ? new IndexPlaylistScoresRequest(roomId, playlistItem.ID, pivot.Cursor, pivot.Params)
+ : new IndexPlaylistScoresRequest(roomId, playlistItem.ID);
+
+ indexReq.Success += r =>
+ {
+ if (pivot == lowerScores)
+ {
+ lowerScores = r;
+ setPositions(r, pivot, 1);
+ }
+ else
+ {
+ higherScores = r;
+ setPositions(r, pivot, -1);
+ }
+
+ performSuccessCallback(scoresCallback, r.Scores, r);
+ };
+
+ indexReq.Failure += _ => hideLoadingSpinners(pivot);
+
+ return indexReq;
+ }
+
+ ///
+ /// Transforms returned into s, ensure the is put into a sane state, and invokes a given success callback.
+ ///
+ /// The callback to invoke with the final s.
+ /// The s that were retrieved from s.
+ /// An optional pivot around which the scores were retrieved.
+ private void performSuccessCallback([NotNull] Action> callback, [NotNull] List scores, [CanBeNull] MultiplayerScores pivot = null)
+ {
+ var scoreInfos = new List(scores.Select(s => s.CreateScoreInfo(playlistItem)));
+
+ // Select a score if we don't already have one selected.
+ // Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll).
+ if (SelectedScore.Value == null)
+ {
+ Schedule(() =>
+ {
+ // Prefer selecting the local user's score, or otherwise default to the first visible score.
+ SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.Id == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault();
+ });
+ }
+
+ // Invoke callback to add the scores. Exclude the user's current score which was added previously.
+ callback.Invoke(scoreInfos.Where(s => s.OnlineScoreID != Score?.OnlineScoreID));
+
+ hideLoadingSpinners(pivot);
+ }
+
+ private void hideLoadingSpinners([CanBeNull] MultiplayerScores pivot = null)
+ {
+ CentreSpinner.Hide();
+
+ if (pivot == lowerScores)
+ RightSpinner.Hide();
+ else if (pivot == higherScores)
+ LeftSpinner.Hide();
+ }
+
+ ///
+ /// Applies positions to all s referenced to a given pivot.
+ ///
+ /// The to set positions on.
+ /// The pivot.
+ /// The amount to increment the pivot position by for each in .
+ private void setPositions([NotNull] MultiplayerScores scores, [CanBeNull] MultiplayerScores pivot, int increment)
+ => setPositions(scores, pivot?.Scores[^1].Position ?? 0, increment);
+
+ ///
+ /// Applies positions to all s referenced to a given pivot.
+ ///
+ /// The to set positions on.
+ /// The pivot position.
+ /// The amount to increment the pivot position by for each in .
+ private void setPositions([NotNull] MultiplayerScores scores, int pivotPosition, int increment)
+ {
+ foreach (var s in scores.Scores)
+ {
+ pivotPosition += increment;
+ s.Position = pivotPosition;
+ }
+ }
+
+ private class PanelListLoadingSpinner : LoadingSpinner
+ {
+ private readonly ScorePanelList list;
+
+ ///
+ /// Creates a new .
+ ///
+ /// The list to track.
+ /// Whether the spinner should have a surrounding black box for visibility.
+ public PanelListLoadingSpinner(ScorePanelList list, bool withBox = true)
+ : base(withBox)
+ {
+ this.list = list;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ float panelOffset = list.DrawWidth / 2 - ScorePanel.EXPANDED_WIDTH;
+
+ if ((Anchor & Anchor.x0) > 0)
+ X = (float)(panelOffset - list.Current);
+ else if ((Anchor & Anchor.x2) > 0)
+ X = (float)(list.ScrollableExtent - list.Current - panelOffset);
+ }
}
}
}
diff --git a/osu.Game/Screens/Multi/RoomManager.cs b/osu.Game/Screens/Multi/RoomManager.cs
index 491be2e946..2a96fa536d 100644
--- a/osu.Game/Screens/Multi/RoomManager.cs
+++ b/osu.Game/Screens/Multi/RoomManager.cs
@@ -14,7 +14,6 @@ using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Online;
using osu.Game.Online.API;
-using osu.Game.Online.API.Requests;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Screens.Multi.Lounge.Components;
diff --git a/osu.Game/Screens/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/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/Ranking/Contracted/ContractedPanelTopContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelTopContent.cs
new file mode 100644
index 0000000000..0935ee7fb2
--- /dev/null
+++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelTopContent.cs
@@ -0,0 +1,37 @@
+// 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.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Scoring;
+
+namespace osu.Game.Screens.Ranking.Contracted
+{
+ public class ContractedPanelTopContent : CompositeDrawable
+ {
+ private readonly ScoreInfo score;
+
+ public ContractedPanelTopContent(ScoreInfo score)
+ {
+ this.score = score;
+
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChild = new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Y = 6,
+ Text = score.Position != null ? $"#{score.Position}" : string.Empty,
+ Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold)
+ };
+ }
+ }
+}
diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs
index 44458d8c8e..c95cf1066e 100644
--- a/osu.Game/Screens/Ranking/ResultsScreen.cs
+++ b/osu.Game/Screens/Ranking/ResultsScreen.cs
@@ -37,7 +37,8 @@ namespace osu.Game.Screens.Ranking
public readonly Bindable SelectedScore = new Bindable();
public readonly ScoreInfo Score;
- private readonly bool allowRetry;
+
+ protected ScorePanelList ScorePanelList { get; private set; }
[Resolved(CanBeNull = true)]
private Player player { get; set; }
@@ -47,9 +48,13 @@ namespace osu.Game.Screens.Ranking
private StatisticsPanel statisticsPanel;
private Drawable bottomPanel;
- private ScorePanelList scorePanelList;
private Container detachedPanelContainer;
+ private bool fetchedInitialScores;
+ private APIRequest nextPageRequest;
+
+ private readonly bool allowRetry;
+
protected ResultsScreen(ScoreInfo score, bool allowRetry = true)
{
Score = score;
@@ -84,7 +89,7 @@ namespace osu.Game.Screens.Ranking
RelativeSizeAxes = Axes.Both,
Score = { BindTarget = SelectedScore }
},
- scorePanelList = new ScorePanelList
+ ScorePanelList = new ScorePanelList
{
RelativeSizeAxes = Axes.Both,
SelectedScore = { BindTarget = SelectedScore },
@@ -142,7 +147,7 @@ namespace osu.Game.Screens.Ranking
};
if (Score != null)
- scorePanelList.AddScore(Score);
+ ScorePanelList.AddScore(Score);
if (player != null && allowRetry)
{
@@ -164,11 +169,7 @@ namespace osu.Game.Screens.Ranking
{
base.LoadComplete();
- var req = FetchScores(scores => Schedule(() =>
- {
- foreach (var s in scores)
- addScore(s);
- }));
+ var req = FetchScores(fetchScoresCallback);
if (req != null)
api.Queue(req);
@@ -176,6 +177,28 @@ namespace osu.Game.Screens.Ranking
statisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true);
}
+ protected override void Update()
+ {
+ base.Update();
+
+ if (fetchedInitialScores && nextPageRequest == null)
+ {
+ if (ScorePanelList.IsScrolledToStart)
+ nextPageRequest = FetchNextPage(-1, fetchScoresCallback);
+ else if (ScorePanelList.IsScrolledToEnd)
+ nextPageRequest = FetchNextPage(1, fetchScoresCallback);
+
+ if (nextPageRequest != null)
+ {
+ // Scheduled after children to give the list a chance to update its scroll position and not potentially trigger a second request too early.
+ nextPageRequest.Success += () => ScheduleAfterChildren(() => nextPageRequest = null);
+ nextPageRequest.Failure += _ => ScheduleAfterChildren(() => nextPageRequest = null);
+
+ api.Queue(nextPageRequest);
+ }
+ }
+ }
+
///
/// Performs a fetch/refresh of scores to be displayed.
///
@@ -183,6 +206,22 @@ namespace osu.Game.Screens.Ranking
/// An