diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 082e0d247c..33ec3d6602 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,6 +4,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+permissions:
+ contents: read # to fetch code (actions/checkout)
+
jobs:
inspect-code:
name: Code Quality
diff --git a/.github/workflows/report-nunit.yml b/.github/workflows/report-nunit.yml
index 358cbda17a..bfc9620174 100644
--- a/.github/workflows/report-nunit.yml
+++ b/.github/workflows/report-nunit.yml
@@ -8,8 +8,12 @@ on:
workflows: ["Continuous Integration"]
types:
- completed
+permissions: {}
jobs:
annotate:
+ permissions:
+ checks: write # to create checks (dorny/test-reporter)
+
name: Annotate CI run with test results
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion != 'cancelled' }}
diff --git a/.github/workflows/sentry-release.yml b/.github/workflows/sentry-release.yml
index 442b97c473..cce3f23e5f 100644
--- a/.github/workflows/sentry-release.yml
+++ b/.github/workflows/sentry-release.yml
@@ -5,6 +5,9 @@ on:
tags:
- '*'
+permissions:
+ contents: read # to fetch code (actions/checkout)
+
jobs:
sentry_release:
runs-on: ubuntu-latest
diff --git a/osu.Android.props b/osu.Android.props
index 77c29a5d6e..6ad810483b 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,7 +52,7 @@
-
+
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index d9ad95f96a..3ee1b3da30 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -137,12 +137,13 @@ namespace osu.Desktop
{
base.SetHost(host);
- var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
-
var desktopWindow = (SDL2DesktopWindow)host.Window;
+ var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
+ if (iconStream != null)
+ desktopWindow.SetIconFromStream(iconStream);
+
desktopWindow.CursorState |= CursorState.Hidden;
- desktopWindow.SetIconFromStream(iconStream);
desktopWindow.Title = Name;
desktopWindow.DragDrop += f => fileDrop(new[] { f });
}
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs
index 6ecbf58a52..a4b2b26624 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchSkinConfiguration.cs
@@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Catch.Tests
});
}
- private class TestSkin : DefaultSkin
+ private class TestSkin : TrianglesSkin
{
public bool FlipCatcherPlate { get; set; }
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
index 956d0e0c14..2dc99077d3 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs
@@ -21,7 +21,6 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Skinning;
using osu.Game.Tests.Visual;
using osuTK;
@@ -106,20 +105,37 @@ namespace osu.Game.Rulesets.Catch.Tests
public void TestCatcherCatchWidth()
{
float halfWidth = Catcher.CalculateCatchWidth(new BeatmapDifficulty { CircleSize = 0 }) / 2;
+
+ AddStep("move catcher to center", () => catcher.X = CatchPlayfield.CENTER_X);
+
+ float leftPlateBounds = CatchPlayfield.CENTER_X - halfWidth;
+ float rightPlateBounds = CatchPlayfield.CENTER_X + halfWidth;
+
AddStep("catch fruit", () =>
{
- attemptCatch(new Fruit { X = -halfWidth + 1 });
- attemptCatch(new Fruit { X = halfWidth - 1 });
+ attemptCatch(new Fruit { X = leftPlateBounds + 1 });
+ attemptCatch(new Fruit { X = rightPlateBounds - 1 });
});
checkPlate(2);
+
AddStep("miss fruit", () =>
{
- attemptCatch(new Fruit { X = -halfWidth - 1 });
- attemptCatch(new Fruit { X = halfWidth + 1 });
+ attemptCatch(new Fruit { X = leftPlateBounds - 1 });
+ attemptCatch(new Fruit { X = rightPlateBounds + 1 });
});
checkPlate(2);
}
+ [Test]
+ public void TestFruitClampedToCatchableRegion()
+ {
+ AddStep("catch fruit left", () => attemptCatch(new Fruit { X = -CatchPlayfield.WIDTH }));
+ checkPlate(1);
+ AddStep("move catcher to right", () => catcher.X = CatchPlayfield.WIDTH);
+ AddStep("catch fruit right", () => attemptCatch(new Fruit { X = CatchPlayfield.WIDTH * 2 }));
+ checkPlate(2);
+ }
+
[Test]
public void TestFruitChangesCatcherState()
{
@@ -233,11 +249,9 @@ namespace osu.Game.Rulesets.Catch.Tests
[Test]
public void TestHitLightingColour()
{
- var fruitColour = SkinConfiguration.DefaultComboColours[1];
AddStep("enable hit lighting", () => config.SetValue(OsuSetting.HitLighting, true));
AddStep("catch fruit", () => attemptCatch(new Fruit()));
- AddAssert("correct hit lighting colour", () =>
- catcher.ChildrenOfType().First()?.Entry?.ObjectColour == fruitColour);
+ AddAssert("correct hit lighting colour", () => catcher.ChildrenOfType().First()?.Entry?.ObjectColour == this.ChildrenOfType().First().AccentColour.Value);
}
[Test]
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs
index abe391ba4e..1adc969f8f 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs
@@ -3,7 +3,6 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
@@ -16,22 +15,14 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
- [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
- public override BindableFloat SizeMultiplier { get; } = new BindableFloat
+ public override BindableFloat SizeMultiplier { get; } = new BindableFloat(1)
{
MinValue = 0.5f,
MaxValue = 1.5f,
- Default = 1f,
- Value = 1f,
Precision = 0.1f
};
- [SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")]
- public override BindableBool ComboBasedSize { get; } = new BindableBool
- {
- Default = true,
- Value = true
- };
+ public override BindableBool ComboBasedSize { get; } = new BindableBool(true);
public override float DefaultFlashlightSize => 350;
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs b/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs
index 9038153e20..19b4a39f97 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModNoScope.cs
@@ -6,8 +6,6 @@ using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Framework.Utils;
-using osu.Game.Configuration;
-using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.UI;
@@ -17,15 +15,8 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public override LocalisableString Description => "Where's the catcher?";
- [SettingSource(
- "Hidden at combo",
- "The combo count at which the catcher becomes completely hidden",
- SettingControlType = typeof(SettingsSlider)
- )]
- public override BindableInt HiddenComboCount { get; } = new BindableInt
+ public override BindableInt HiddenComboCount { get; } = new BindableInt(10)
{
- Default = 10,
- Value = 10,
MinValue = 0,
MaxValue = 50,
};
diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
index 6e01c44e1f..cd2b8348e2 100644
--- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
@@ -69,7 +70,7 @@ namespace osu.Game.Rulesets.Catch.Objects
/// This value is the original value plus the offset applied by the beatmap processing.
/// Use if a value not affected by the offset is desired.
///
- public float EffectiveX => OriginalX + XOffset;
+ public float EffectiveX => Math.Clamp(OriginalX + XOffset, 0, CatchPlayfield.WIDTH);
public double TimePreempt { get; set; } = 1000;
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 311e15116e..015457e84f 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -11,6 +11,7 @@ using Newtonsoft.Json;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@@ -84,8 +85,8 @@ namespace osu.Game.Rulesets.Catch.Objects
AddNested(new TinyDroplet
{
StartTime = t + lastEvent.Value.Time,
- X = OriginalX + Path.PositionAt(
- lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X,
+ X = ClampToPlayfield(EffectiveX + Path.PositionAt(
+ lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X),
});
}
}
@@ -102,7 +103,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{
Samples = dropletSamples,
StartTime = e.Time,
- X = OriginalX + Path.PositionAt(e.PathProgress).X,
+ X = ClampToPlayfield(EffectiveX + Path.PositionAt(e.PathProgress).X),
});
break;
@@ -113,14 +114,16 @@ namespace osu.Game.Rulesets.Catch.Objects
{
Samples = this.GetNodeSamples(nodeIndex++),
StartTime = e.Time,
- X = OriginalX + Path.PositionAt(e.PathProgress).X,
+ X = ClampToPlayfield(EffectiveX + Path.PositionAt(e.PathProgress).X),
});
break;
}
}
}
- public float EndX => OriginalX + this.CurvePositionAt(1).X;
+ public float EndX => ClampToPlayfield(EffectiveX + this.CurvePositionAt(1).X);
+
+ public float ClampToPlayfield(float value) => Math.Clamp(value, 0, CatchPlayfield.WIDTH);
[JsonIgnore]
public double Duration
diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyHitExplosion.cs
index 0630de9156..8f46bdbe6e 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyHitExplosion.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyHitExplosion.cs
@@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
[BackgroundDependencyLoader]
private void load(SkinManager skins)
{
- var defaultLegacySkin = skins.DefaultLegacySkin;
+ var defaultLegacySkin = skins.DefaultClassicSkin;
// sprite names intentionally swapped to match stable member naming / ease of cross-referencing
explosion1.Texture = defaultLegacySkin.GetTexture("scoreboard-explosion-2");
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
index 0354228cca..e96a186ae4 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaComposeScreen.cs
@@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
[Test]
public void TestDefaultSkin()
{
- AddStep("set default skin", () => skins.CurrentSkinInfo.Value = DefaultSkin.CreateInfo().ToLiveUnmanaged());
+ AddStep("set default skin", () => skins.CurrentSkinInfo.Value = TrianglesSkin.CreateInfo().ToLiveUnmanaged());
}
[Test]
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs
index 8ef5bfd94c..6eaede2112 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs
@@ -5,7 +5,6 @@ using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Layout;
-using osu.Game.Configuration;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
using osuTK;
@@ -17,22 +16,14 @@ namespace osu.Game.Rulesets.Mania.Mods
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(ModHidden) };
- [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
- public override BindableFloat SizeMultiplier { get; } = new BindableFloat
+ public override BindableFloat SizeMultiplier { get; } = new BindableFloat(1)
{
MinValue = 0.5f,
MaxValue = 3f,
- Default = 1f,
- Value = 1f,
Precision = 0.1f
};
- [SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")]
- public override BindableBool ComboBasedSize { get; } = new BindableBool
- {
- Default = false,
- Value = false
- };
+ public override BindableBool ComboBasedSize { get; } = new BindableBool();
public override float DefaultFlashlightSize => 50;
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
index 6020348938..a607ed572d 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
@@ -54,10 +54,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
}
}
- protected override void UpdateInitialTransforms()
- {
- }
-
protected override void UpdateStartTimeStateTransforms() => this.FadeOut(150);
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
index d374e935ec..66cc93b033 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
@@ -30,14 +30,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public bool UpdateResult() => base.UpdateResult(true);
- protected override void UpdateInitialTransforms()
- {
- base.UpdateInitialTransforms();
-
- // This hitobject should never expire, so this is just a safe maximum.
- LifetimeEnd = LifetimeStart + 30000;
- }
-
protected override void UpdateHitStateTransforms(ArmedState state)
{
// suppress the base call explicitly.
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
index a7bdcd047e..3084f71be2 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public void UpdateResult() => base.UpdateResult(true);
- protected override double MaximumJudgementOffset => base.MaximumJudgementOffset * release_window_lenience;
+ public override double MaximumJudgementOffset => base.MaximumJudgementOffset * release_window_lenience;
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
index bcc10ab7bc..6cd39d835d 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
@@ -23,10 +23,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected readonly IBindable Direction = new Bindable();
- // Leaving the default (10s) makes hitobjects not appear, as this offset is used for the initial state transforms.
- // Calculated as DrawableManiaRuleset.MAX_TIME_RANGE + some additional allowance for velocity < 1.
- protected override double InitialLifetimeOffset => 30000;
-
[Resolved(canBeNull: true)]
private ManiaPlayfield playfield { get; set; }
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs
index 9d06ff5801..88b6b9dd56 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs
@@ -8,7 +8,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
-using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
@@ -88,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
if (!objects.Any())
return false;
- return objects.All(o => Precision.AlmostEquals(o.ChildrenOfType().First().Children.OfType().Single().Scale.X, target));
+ return objects.All(o => Precision.AlmostEquals(o.ChildrenOfType().First().Scale.X, target));
}
private bool checkSomeHit()
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRandom.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRandom.cs
new file mode 100644
index 0000000000..c24ba6d530
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRandom.cs
@@ -0,0 +1,102 @@
+// 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 NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Osu.Beatmaps;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public class TestSceneOsuModRandom : OsuModTestScene
+ {
+ [TestCase(1)]
+ [TestCase(7)]
+ [TestCase(10)]
+ public void TestDefaultBeatmap(float angleSharpness) => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModRandom
+ {
+ AngleSharpness = { Value = angleSharpness }
+ },
+ Autoplay = true,
+ PassCondition = () => true
+ });
+
+ [TestCase(1)]
+ [TestCase(7)]
+ [TestCase(10)]
+ public void TestJumpBeatmap(float angleSharpness) => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModRandom
+ {
+ AngleSharpness = { Value = angleSharpness }
+ },
+ Beatmap = jumpBeatmap,
+ Autoplay = true,
+ PassCondition = () => true
+ });
+
+ [TestCase(1)]
+ [TestCase(7)]
+ [TestCase(10)]
+ public void TestStreamBeatmap(float angleSharpness) => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModRandom
+ {
+ AngleSharpness = { Value = angleSharpness }
+ },
+ Beatmap = streamBeatmap,
+ Autoplay = true,
+ PassCondition = () => true
+ });
+
+ private OsuBeatmap jumpBeatmap =>
+ createHitCircleBeatmap(new[] { 100, 200, 300, 400 }, 8, 300, 2 * 300);
+
+ private OsuBeatmap streamBeatmap =>
+ createHitCircleBeatmap(new[] { 10, 20, 30, 40, 50, 60, 70, 80 }, 16, 150, 4 * 150);
+
+ private OsuBeatmap createHitCircleBeatmap(IEnumerable spacings, int objectsPerSpacing, int interval, int beatLength)
+ {
+ var controlPointInfo = new ControlPointInfo();
+ controlPointInfo.Add(0, new TimingControlPoint
+ {
+ Time = 0,
+ BeatLength = beatLength
+ });
+
+ var beatmap = new OsuBeatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ StackLeniency = 0,
+ Difficulty = new BeatmapDifficulty
+ {
+ ApproachRate = 8.5f
+ }
+ },
+ ControlPointInfo = controlPointInfo
+ };
+
+ foreach (int spacing in spacings)
+ {
+ for (int i = 0; i < objectsPerSpacing; i++)
+ {
+ beatmap.HitObjects.Add(new HitCircle
+ {
+ StartTime = interval * beatmap.HitObjects.Count,
+ Position = beatmap.HitObjects.Count % 2 == 0 ? Vector2.Zero : new Vector2(spacing, 0),
+ NewCombo = i == 0
+ });
+ }
+ }
+
+ return beatmap;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorParticles.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorParticles.cs
index b5d1c4854c..7f0ecaca2b 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorParticles.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorParticles.cs
@@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Tests
});
AddStep("setup default legacy skin", () =>
{
- skinManager.CurrentSkinInfo.Value = skinManager.DefaultLegacySkin.SkinInfo;
+ skinManager.CurrentSkinInfo.Value = skinManager.DefaultClassicSkin.SkinInfo;
});
});
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
index cc69054e23..be224b88ce 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs
@@ -58,10 +58,11 @@ namespace osu.Game.Rulesets.Osu.Tests
private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null)
{
- var drawable = createSingle(circleSize, auto, timeOffset, positionOffset);
-
var playfield = new TestOsuPlayfield();
- playfield.Add(drawable);
+
+ for (double t = timeOffset; t < timeOffset + 60000; t += 2000)
+ playfield.Add(createSingle(circleSize, auto, t, positionOffset));
+
return playfield;
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs
index 0cf2ec6b7e..57734236da 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleArea.cs
@@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Tests
hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
- Child = new SkinProvidingContainer(new DefaultSkin(null))
+ Child = new SkinProvidingContainer(new TrianglesSkin(null))
{
RelativeSizeAxes = Axes.Both,
Child = drawableHitCircle = new DrawableHitCircle(hitCircle)
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index c01b2576e8..5fa4e24f5e 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -7,7 +7,6 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
-using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
@@ -43,7 +42,6 @@ namespace osu.Game.Rulesets.Osu.Tests
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
private DrawableSpinner drawableSpinner = null!;
- private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType().Single();
[SetUpSteps]
public override void SetUpSteps()
@@ -77,7 +75,6 @@ namespace osu.Game.Rulesets.Osu.Tests
{
double finalCumulativeTrackerRotation = 0;
double finalTrackerRotation = 0, trackerRotationTolerance = 0;
- double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
addSeekStep(spinner_start_time + 5000);
AddStep("retrieve disc rotation", () =>
@@ -85,11 +82,6 @@ namespace osu.Game.Rulesets.Osu.Tests
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.Result.RateAdjustedRotation);
addSeekStep(spinner_start_time + 2500);
@@ -98,8 +90,6 @@ namespace osu.Game.Rulesets.Osu.Tests
// 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).
() => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation / 2).Within(trackerRotationTolerance));
- AddAssert("symbol rotation rewound",
- () => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation / 2).Within(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.
() => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation / 2).Within(100));
@@ -107,8 +97,6 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(spinner_start_time + 5000);
AddAssert("is disc rotation almost same",
() => drawableSpinner.RotationTracker.Rotation, () => Is.EqualTo(finalTrackerRotation).Within(trackerRotationTolerance));
- AddAssert("is symbol rotation almost same",
- () => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation).Within(spinnerSymbolRotationTolerance));
AddAssert("is cumulative rotation almost same",
() => drawableSpinner.Result.RateAdjustedRotation, () => Is.EqualTo(finalCumulativeTrackerRotation).Within(100));
}
@@ -122,7 +110,6 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(5000);
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);
}
private Replay flip(Replay scoreReplay) => new Replay
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneTrianglesSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneTrianglesSpinnerRotation.cs
new file mode 100644
index 0000000000..80e3af6cc0
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneTrianglesSpinnerRotation.cs
@@ -0,0 +1,149 @@
+// 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.Graphics.Sprites;
+using osu.Framework.Testing;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Database;
+using osu.Game.Replays;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+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.Skinning;
+using osu.Game.Storyboards;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestSceneTrianglesSpinnerRotation : TestSceneOsuPlayer
+ {
+ private const double spinner_start_time = 100;
+ private const double spinner_duration = 6000;
+
+ [Resolved]
+ private SkinManager skinManager { get; set; } = null!;
+
+ [Resolved]
+ private AudioManager audioManager { get; set; } = null!;
+
+ protected override bool Autoplay => true;
+
+ protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer();
+
+ protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
+ => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
+
+ private DrawableSpinner drawableSpinner = null!;
+ private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType().Single();
+
+ [SetUpSteps]
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ AddStep("set triangles skin", () => skinManager.CurrentSkinInfo.Value = TrianglesSkin.CreateInfo().ToLiveUnmanaged());
+
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
+ AddStep("retrieve spinner", () => drawableSpinner = (DrawableSpinner)Player.DrawableRuleset.Playfield.AllHitObjects.First());
+ }
+
+ [Test]
+ public void TestSymbolMiddleRewindingRotation()
+ {
+ double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
+
+ addSeekStep(spinner_start_time + 5000);
+ AddStep("retrieve spinner symbol rotation", () =>
+ {
+ finalSpinnerSymbolRotation = spinnerSymbol.Rotation;
+ spinnerSymbolRotationTolerance = Math.Abs(finalSpinnerSymbolRotation * 0.05f);
+ });
+
+ addSeekStep(spinner_start_time + 2500);
+ AddAssert("symbol rotation rewound",
+ () => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation / 2).Within(spinnerSymbolRotationTolerance));
+
+ addSeekStep(spinner_start_time + 5000);
+ AddAssert("is symbol rotation almost same",
+ () => spinnerSymbol.Rotation, () => Is.EqualTo(finalSpinnerSymbolRotation).Within(spinnerSymbolRotationTolerance));
+ }
+
+ [Test]
+ public void TestSymbolRotationDirection([Values(true, false)] bool clockwise)
+ {
+ if (clockwise)
+ transformReplay(flip);
+
+ addSeekStep(5000);
+ AddAssert("spinner symbol direction correct", () => clockwise ? spinnerSymbol.Rotation > 0 : spinnerSymbol.Rotation < 0);
+ }
+
+ private Replay flip(Replay scoreReplay) => new Replay
+ {
+ Frames = scoreReplay
+ .Frames
+ .Cast()
+ .Select(replayFrame =>
+ {
+ var flippedPosition = new Vector2(OsuPlayfield.BASE_SIZE.X - replayFrame.Position.X, replayFrame.Position.Y);
+ return new OsuReplayFrame(replayFrame.Time, flippedPosition, replayFrame.Actions.ToArray());
+ })
+ .Cast()
+ .ToList()
+ };
+
+ private void addSeekStep(double time)
+ {
+ AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
+ AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(100));
+ }
+
+ private void transformReplay(Func replayTransformation) => AddStep("set replay", () =>
+ {
+ var drawableRuleset = this.ChildrenOfType().Single();
+ var score = drawableRuleset.ReplayScore;
+ var transformedScore = new Score
+ {
+ ScoreInfo = score.ScoreInfo,
+ Replay = replayTransformation.Invoke(score.Replay)
+ };
+ drawableRuleset.SetReplayScore(transformedScore);
+ });
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Spinner
+ {
+ Position = new Vector2(256, 192),
+ StartTime = spinner_start_time,
+ Duration = spinner_duration
+ },
+ }
+ };
+
+ private class ScoreExposedPlayer : TestPlayer
+ {
+ public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
+
+ public ScoreExposedPlayer()
+ : base(false, false)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs
index 2ba856d014..dabbfcd2fb 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Evaluators/FlashlightEvaluator.cs
@@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
if (!(currentObj.BaseObject is Spinner))
{
- double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.EndPosition).Length;
+ double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.StackedEndPosition).Length;
cumulativeStrainTime += lastObj.StrainTime;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index 33787da8f6..1e83d6d820 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -44,6 +44,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
+ if (mods.Any(m => m is OsuModTouchDevice))
+ {
+ aimRating = Math.Pow(aimRating, 0.8);
+ flashlightRating = Math.Pow(flashlightRating, 0.8);
+ }
+
if (mods.Any(h => h is OsuModRelax))
{
aimRating *= 0.9;
@@ -127,6 +133,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
{
+ new OsuModTouchDevice(),
new OsuModDoubleTime(),
new OsuModHalfTime(),
new OsuModEasy(),
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index fb0eff5cb2..30b56ff769 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -88,12 +88,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
- double rawAim = attributes.AimDifficulty;
-
- if (score.Mods.Any(m => m is OsuModTouchDevice))
- rawAim = Math.Pow(rawAim, 0.8);
-
- double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0;
+ double aimValue = Math.Pow(5.0 * Math.Max(1.0, attributes.AimDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
@@ -233,12 +228,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (!score.Mods.Any(h => h is OsuModFlashlight))
return 0.0;
- double rawFlashlight = attributes.FlashlightDifficulty;
-
- if (score.Mods.Any(m => m is OsuModTouchDevice))
- rawFlashlight = Math.Pow(rawFlashlight, 0.8);
-
- double flashlightValue = Math.Pow(rawFlashlight, 2.0) * 25.0;
+ double flashlightValue = Math.Pow(attributes.FlashlightDifficulty, 2.0) * 25.0;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0)
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
index c24f78e430..94655f3cf7 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
@@ -303,11 +303,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
else
{
+ var result = snapProvider?.FindSnappedPositionAndTime(Parent.ToScreenSpace(e.MousePosition));
+
+ Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? Parent.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - slider.Position;
+
for (int i = 0; i < controlPoints.Count; ++i)
{
var controlPoint = controlPoints[i];
if (selectedControlPoints.Contains(controlPoint))
- controlPoint.Position = dragStartPositions[i] + (e.MousePosition - e.MouseDownPosition);
+ controlPoint.Position = dragStartPositions[i] + movementDelta;
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
index e2f98c273e..dd5335a743 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
@@ -198,7 +198,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
// Update the cursor position.
- cursor.Position = ToLocalSpace(inputManager.CurrentState.Mouse.Position) - HitObject.Position;
+ var result = snapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position);
+ cursor.Position = ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position;
}
else if (cursor != null)
{
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index eb69efd636..7c289b5b05 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -163,7 +163,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected override void OnDrag(DragEvent e)
{
if (placementControlPoint != null)
- placementControlPoint.Position = e.MousePosition - HitObject.Position;
+ {
+ var result = snapProvider?.FindSnappedPositionAndTime(ToScreenSpace(e.MousePosition));
+ placementControlPoint.Position = ToLocalSpace(result?.ScreenSpacePosition ?? ToScreenSpace(e.MousePosition)) - HitObject.Position;
+ }
}
protected override void OnMouseUp(MouseUpEvent e)
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs
index e624660410..f6622c268d 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs
@@ -4,7 +4,6 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
-using osu.Game.Configuration;
namespace osu.Game.Rulesets.Osu.Mods
{
@@ -18,13 +17,10 @@ namespace osu.Game.Rulesets.Osu.Mods
public override LocalisableString Description => "Hit them at the right size!";
- [SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
- public override BindableNumber StartScale { get; } = new BindableFloat
+ public override BindableNumber StartScale { get; } = new BindableFloat(2)
{
MinValue = 1f,
MaxValue = 25f,
- Default = 2f,
- Value = 2f,
Precision = 0.1f,
};
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
index e5a458488e..79f5eed139 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
@@ -32,22 +32,14 @@ namespace osu.Game.Rulesets.Osu.Mods
Precision = default_follow_delay,
};
- [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
- public override BindableFloat SizeMultiplier { get; } = new BindableFloat
+ public override BindableFloat SizeMultiplier { get; } = new BindableFloat(1)
{
MinValue = 0.5f,
MaxValue = 2f,
- Default = 1f,
- Value = 1f,
Precision = 0.1f
};
- [SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")]
- public override BindableBool ComboBasedSize { get; } = new BindableBool
- {
- Default = true,
- Value = true
- };
+ public override BindableBool ComboBasedSize { get; } = new BindableBool(true);
public override float DefaultFlashlightSize => 180;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs
index b77c887cd3..3d066d3ada 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs
@@ -4,7 +4,6 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
-using osu.Game.Configuration;
namespace osu.Game.Rulesets.Osu.Mods
{
@@ -18,13 +17,10 @@ namespace osu.Game.Rulesets.Osu.Mods
public override LocalisableString Description => "Hit them at the right size!";
- [SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
- public override BindableNumber StartScale { get; } = new BindableFloat
+ public override BindableNumber StartScale { get; } = new BindableFloat(0.5f)
{
MinValue = 0f,
MaxValue = 0.99f,
- Default = 0.5f,
- Value = 0.5f,
Precision = 0.01f,
};
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
index 817f7b599c..2f84c30581 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
@@ -7,8 +7,6 @@ using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
-using osu.Game.Configuration;
-using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
@@ -22,15 +20,8 @@ namespace osu.Game.Rulesets.Osu.Mods
private PeriodTracker spinnerPeriods = null!;
- [SettingSource(
- "Hidden at combo",
- "The combo count at which the cursor becomes completely hidden",
- SettingControlType = typeof(SettingsSlider)
- )]
- public override BindableInt HiddenComboCount { get; } = new BindableInt
+ public override BindableInt HiddenComboCount { get; } = new BindableInt(10)
{
- Default = 10,
- Value = 10,
MinValue = 0,
MaxValue = 50,
};
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
index 59984f9a7b..6f1206382a 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs
@@ -4,6 +4,7 @@
using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
@@ -20,6 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 1;
+ [SettingSource("Starting Size", "The initial size multiplier applied to all objects.")]
public abstract BindableNumber StartScale { get; }
protected virtual float EndScale => 1;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
index 056a325dce..618fcfe05d 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
@@ -4,9 +4,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
@@ -25,6 +28,14 @@ namespace osu.Game.Rulesets.Osu.Mods
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModTarget)).ToArray();
+ [SettingSource("Angle sharpness", "How sharp angles should be", SettingControlType = typeof(SettingsSlider))]
+ public BindableFloat AngleSharpness { get; } = new BindableFloat(7)
+ {
+ MinValue = 1,
+ MaxValue = 10,
+ Precision = 0.1f
+ };
+
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
private Random random = null!;
@@ -50,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
if (shouldStartNewSection(osuBeatmap, positionInfos, i))
{
- sectionOffset = OsuHitObjectGenerationUtils.RandomGaussian(random, 0, 0.0008f);
+ sectionOffset = getRandomOffset(0.0008f);
flowDirection = !flowDirection;
}
@@ -65,11 +76,11 @@ namespace osu.Game.Rulesets.Osu.Mods
float flowChangeOffset = 0;
// Offsets only the angle of the current hit object.
- float oneTimeOffset = OsuHitObjectGenerationUtils.RandomGaussian(random, 0, 0.002f);
+ float oneTimeOffset = getRandomOffset(0.002f);
if (shouldApplyFlowChange(positionInfos, i))
{
- flowChangeOffset = OsuHitObjectGenerationUtils.RandomGaussian(random, 0, 0.002f);
+ flowChangeOffset = getRandomOffset(0.002f);
flowDirection = !flowDirection;
}
@@ -86,13 +97,36 @@ namespace osu.Game.Rulesets.Osu.Mods
osuBeatmap.HitObjects = OsuHitObjectGenerationUtils.RepositionHitObjects(positionInfos);
}
+ private float getRandomOffset(float stdDev)
+ {
+ // Range: [0.5, 2]
+ // Higher angle sharpness -> lower multiplier
+ float customMultiplier = (1.5f * AngleSharpness.MaxValue - AngleSharpness.Value) / (1.5f * AngleSharpness.MaxValue - AngleSharpness.Default);
+
+ return OsuHitObjectGenerationUtils.RandomGaussian(random, 0, stdDev * customMultiplier);
+ }
+
/// The target distance between the previous and the current .
/// The angle (in rad) by which the target angle should be offset.
/// Whether the relative angle should be positive or negative.
- private static float getRelativeTargetAngle(float targetDistance, float offset, bool flowDirection)
+ private float getRelativeTargetAngle(float targetDistance, float offset, bool flowDirection)
{
- float angle = (float)(2.16 / (1 + 200 * Math.Exp(0.036 * (targetDistance - 310))) + 0.5 + offset);
+ // Range: [0.1, 1]
+ float angleSharpness = AngleSharpness.Value / AngleSharpness.MaxValue;
+ // Range: [0, 0.9]
+ float angleWideness = 1 - angleSharpness;
+
+ // Range: [-60, 30]
+ float customOffsetX = angleSharpness * 100 - 70;
+ // Range: [-0.075, 0.15]
+ float customOffsetY = angleWideness * 0.25f - 0.075f;
+
+ targetDistance += customOffsetX;
+ float angle = (float)(2.16 / (1 + 200 * Math.Exp(0.036 * (targetDistance - 310 + customOffsetX))) + 0.5);
+ angle += offset + customOffsetY;
+
float relativeAngle = (float)Math.PI - angle;
+
return flowDirection ? -relativeAngle : relativeAngle;
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
index 861ad80b7f..406968ba08 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
@@ -53,11 +53,7 @@ namespace osu.Game.Rulesets.Osu.Mods
}).ToArray();
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))]
- public Bindable Seed { get; } = new Bindable
- {
- Default = null,
- Value = null
- };
+ public Bindable Seed { get; } = new Bindable();
[SettingSource("Metronome ticks", "Whether a metronome beat should play in the background")]
public Bindable Metronome { get; } = new BindableBool(true);
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index 23e5cb0ad7..23db29b9a6 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
+using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Judgements;
@@ -47,12 +48,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
}
+ private ShakeContainer shakeContainer;
+
[BackgroundDependencyLoader]
private void load()
{
Origin = Anchor.Centre;
- InternalChildren = new Drawable[]
+ AddRangeInternal(new Drawable[]
{
scaleContainer = new Container
{
@@ -72,22 +75,30 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return true;
},
},
- CirclePiece = new SkinnableDrawable(new OsuSkinComponent(CirclePieceComponent), _ => new MainCirclePiece())
+ shakeContainer = new ShakeContainer
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- },
- ApproachCircle = new ProxyableSkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.ApproachCircle), _ => new DefaultApproachCircle())
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
+ ShakeDuration = 30,
RelativeSizeAxes = Axes.Both,
- Alpha = 0,
- Scale = new Vector2(4),
+ Children = new Drawable[]
+ {
+ CirclePiece = new SkinnableDrawable(new OsuSkinComponent(CirclePieceComponent), _ => new MainCirclePiece())
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ ApproachCircle = new ProxyableSkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.ApproachCircle), _ => new DefaultApproachCircle())
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ Scale = new Vector2(4),
+ }
+ }
}
}
},
- };
+ });
Size = HitArea.DrawSize;
@@ -123,6 +134,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
}
+ public override void Shake() => shakeContainer.Shake();
+
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
Debug.Assert(HitObject.HitWindows != null);
@@ -139,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false)
{
- Shake(Math.Abs(timeOffset) - HitObject.HitWindows.WindowFor(HitResult.Miss));
+ Shake();
return;
}
@@ -191,12 +204,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// todo: temporary / arbitrary, used for lifetime optimisation.
this.Delay(800).FadeOut();
- // in the case of an early state change, the fade should be expedited to the current point in time.
- if (HitStateUpdateTime < HitObject.StartTime)
- ApproachCircle.FadeOut(50);
-
switch (state)
{
+ default:
+ ApproachCircle.FadeOut();
+ break;
+
case ArmedState.Idle:
HitArea.HitAction = null;
break;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
index 6e525071ca..d9d0d28477 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
@@ -6,17 +6,18 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Judgements;
-using osu.Game.Graphics.Containers;
+using osu.Game.Rulesets.Osu.Scoring;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
- public class DrawableOsuHitObject : DrawableHitObject
+ public abstract class DrawableOsuHitObject : DrawableHitObject
{
public readonly IBindable PositionBindable = new Bindable();
public readonly IBindable StackHeightBindable = new Bindable();
@@ -34,8 +35,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
///
public Func CheckHittable;
- private ShakeContainer shakeContainer;
-
protected DrawableOsuHitObject(OsuHitObject hitObject)
: base(hitObject)
{
@@ -45,12 +44,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private void load()
{
Alpha = 0;
-
- base.AddInternal(shakeContainer = new ShakeContainer
- {
- ShakeDuration = 30,
- RelativeSizeAxes = Axes.Both
- });
}
protected override void OnApply()
@@ -73,18 +66,32 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ScaleBindable.UnbindFrom(HitObject.ScaleBindable);
}
- // Forward all internal management to shakeContainer.
- // This is a bit ugly but we don't have the concept of InternalContent so it'll have to do for now. (https://github.com/ppy/osu-framework/issues/1690)
- protected override void AddInternal(Drawable drawable) => shakeContainer.Add(drawable);
- protected override void ClearInternal(bool disposeChildren = true) => shakeContainer.Clear(disposeChildren);
- protected override bool RemoveInternal(Drawable drawable, bool disposeImmediately) => shakeContainer.Remove(drawable, disposeImmediately);
+ protected override void UpdateInitialTransforms()
+ {
+ base.UpdateInitialTransforms();
+
+ // Dim should only be applied at a top level, as it will be implicitly applied to nested objects.
+ if (ParentHitObject == null)
+ {
+ // Of note, no one noticed this was missing for years, but it definitely feels like it should still exist.
+ // For now this is applied across all skins, and matches stable.
+ // For simplicity, dim colour is applied to the DrawableHitObject itself.
+ // We may need to make a nested container setup if this even causes a usage conflict (ie. with a mod).
+ this.FadeColour(new Color4(195, 195, 195, 255));
+ using (BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW))
+ this.FadeColour(Color4.White, 100);
+ }
+ }
protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt;
private OsuInputManager osuActionInputManager;
internal OsuInputManager OsuActionInputManager => osuActionInputManager ??= GetContainingInputManager() as OsuInputManager;
- public virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength);
+ ///
+ /// Shake the hit object in case it was clicked far too early or late (aka "note lock").
+ ///
+ public virtual void Shake() { }
///
/// Causes this to get missed, disregarding all conditions in implementations of .
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index d83f5df7a3..d58a435728 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -11,6 +11,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
+using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning;
@@ -34,6 +35,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public SkinnableDrawable Body { get; private set; }
+ private ShakeContainer shakeContainer;
+
///
/// A target container which can be used to add top level elements to the slider's display.
/// Intended to be used for proxy purposes only.
@@ -74,17 +77,26 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
[BackgroundDependencyLoader]
private void load()
{
- InternalChildren = new Drawable[]
+ AddRangeInternal(new Drawable[]
{
- Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling),
- tailContainer = new Container { RelativeSizeAxes = Axes.Both },
- tickContainer = new Container { RelativeSizeAxes = Axes.Both },
- repeatContainer = new Container { RelativeSizeAxes = Axes.Both },
+ shakeContainer = new ShakeContainer
+ {
+ ShakeDuration = 30,
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling),
+ tailContainer = new Container { RelativeSizeAxes = Axes.Both },
+ tickContainer = new Container { RelativeSizeAxes = Axes.Both },
+ repeatContainer = new Container { RelativeSizeAxes = Axes.Both },
+ }
+ },
+ // slider head is not included in shake as it handles hit detection, and handles its own shaking.
headContainer = new Container { RelativeSizeAxes = Axes.Both },
OverlayElementContainer = new Container { RelativeSizeAxes = Axes.Both, },
Ball,
slidingSample = new PausableSkinnableSound { Looping = true }
- };
+ });
PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
StackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
@@ -109,6 +121,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
PathVersion.BindTo(HitObject.Path.Version);
}
+ public override void Shake() => shakeContainer.Shake();
+
protected override void OnFree()
{
base.OnFree();
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
index 70b1bd225f..80b9544e5b 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
@@ -63,7 +63,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
pathVersion.BindTo(DrawableSlider.PathVersion);
- OnShake = DrawableSlider.Shake;
CheckHittable = (d, t) => DrawableSlider.CheckHittable?.Invoke(d, t) ?? true;
}
@@ -96,9 +95,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return result.IsHit() ? HitResult.LargeTickHit : HitResult.LargeTickMiss;
}
- public Action OnShake;
-
- public override void Shake(double maximumLength) => OnShake?.Invoke(maximumLength);
+ public override void Shake()
+ {
+ base.Shake();
+ DrawableSlider.Shake();
+ }
private void updatePosition()
{
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
index 1bddc603ac..7b9c0c7e40 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs
@@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
- InternalChild = scaleContainer = new Container
+ AddInternal(scaleContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
@@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
},
Arrow = new ReverseArrowPiece(),
}
- };
+ });
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
index df3a12fe33..063d297f5a 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
- InternalChildren = new Drawable[]
+ AddRangeInternal(new Drawable[]
{
scaleContainer = new Container
{
@@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty())
}
},
- };
+ });
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs
index 3ffbe68b98..4bd98fc8b2 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs
@@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
Origin = Anchor.Centre;
- InternalChild = scaleContainer = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderScorePoint), _ => new CircularContainer
+ AddInternal(scaleContainer = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderScorePoint), _ => new CircularContainer
{
Masking = true,
Origin = Anchor.Centre,
@@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- };
+ });
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs
index 6a15463a32..4975ca1248 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre;
}
- protected override double MaximumJudgementOffset => DrawableSpinner.HitObject.Duration;
+ public override double MaximumJudgementOffset => DrawableSpinner.HitObject.Duration;
///
/// Apply a judgement result.
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
index 569e9b7c1c..676ff62455 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Objects
// This is so on repeats ticks don't appear too late to be visually processed by the player.
offset = 200;
else
- offset = TimeFadeIn * 0.66f;
+ offset = TimePreempt * 0.66f;
TimePreempt = (StartTime - SpanStartTime) / 2 + offset;
}
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 0a5b3139fc..3f5e728651 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -27,6 +27,7 @@ using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.Scoring;
+using osu.Game.Rulesets.Osu.Skinning.Argon;
using osu.Game.Rulesets.Osu.Skinning.Legacy;
using osu.Game.Rulesets.Osu.Statistics;
using osu.Game.Rulesets.Osu.UI;
@@ -237,6 +238,9 @@ namespace osu.Game.Rulesets.Osu
{
case LegacySkin:
return new OsuLegacySkinTransformer(skin);
+
+ case ArgonSkin:
+ return new OsuArgonSkinTransformer(skin);
}
return null;
diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs
index 05fbac625e..6f55e1790f 100644
--- a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs
+++ b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs
@@ -1,20 +1,23 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring
{
public class OsuHitWindows : HitWindows
{
+ ///
+ /// osu! ruleset has a fixed miss window regardless of difficulty settings.
+ ///
+ public const double MISS_WINDOW = 400;
+
private static readonly DifficultyRange[] osu_ranges =
{
new DifficultyRange(HitResult.Great, 80, 50, 20),
new DifficultyRange(HitResult.Ok, 140, 100, 60),
new DifficultyRange(HitResult.Meh, 200, 150, 100),
- new DifficultyRange(HitResult.Miss, 400, 400, 400),
+ new DifficultyRange(HitResult.Miss, MISS_WINDOW, MISS_WINDOW, MISS_WINDOW),
};
public override bool IsHitResultAllowed(HitResult result)
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursor.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursor.cs
new file mode 100644
index 0000000000..446f3c83ae
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursor.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 osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Osu.UI.Cursor;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonCursor : OsuCursorSprite
+ {
+ public ArgonCursor()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ InternalChildren = new[]
+ {
+ ExpandTarget = new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ BorderThickness = 6,
+ BorderColour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0.4f,
+ Colour = Colour4.FromHex("FC618F").Darken(0.6f),
+ },
+ new CircularContainer
+ {
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ BorderThickness = 2,
+ BorderColour = Color4.White.Opacity(0.8f),
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true,
+ },
+ },
+ },
+ },
+ },
+ new Circle
+ {
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Scale = new Vector2(0.2f),
+ Colour = new Color4(255, 255, 255, 255),
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Radius = 20,
+ Colour = new Color4(171, 255, 255, 100),
+ },
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursorTrail.cs
new file mode 100644
index 0000000000..9bb3122a3b
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonCursorTrail.cs
@@ -0,0 +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 osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Rulesets.Osu.UI.Cursor;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonCursorTrail : CursorTrail
+ {
+ protected override float IntervalMultiplier => 0.4f;
+
+ protected override float FadeExponent => 4;
+
+ [BackgroundDependencyLoader]
+ private void load(TextureStore textures)
+ {
+ Texture = textures.Get(@"Cursor/cursortrail");
+ Scale = new Vector2(0.8f / Texture.ScaleAdjust);
+
+ Blending = BlendingParameters.Additive;
+
+ Alpha = 0.8f;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.cs
new file mode 100644
index 0000000000..83c5f6295a
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowCircle.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 osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonFollowCircle : FollowCircle
+ {
+ public ArgonFollowCircle()
+ {
+ InternalChild = new CircularContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ BorderThickness = 4,
+ BorderColour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
+ Blending = BlendingParameters.Additive,
+ Child = new Box
+ {
+ Colour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0.3f,
+ }
+ };
+ }
+
+ protected override void OnSliderPress()
+ {
+ const float duration = 300f;
+
+ if (Precision.AlmostEquals(0, Alpha))
+ this.ScaleTo(1);
+
+ this.ScaleTo(DrawableSliderBall.FOLLOW_AREA, duration, Easing.OutQuint)
+ .FadeIn(duration, Easing.OutQuint);
+ }
+
+ protected override void OnSliderRelease()
+ {
+ const float duration = 150;
+
+ this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.2f, duration, Easing.OutQuint)
+ .FadeTo(0, duration, Easing.OutQuint);
+ }
+
+ protected override void OnSliderEnd()
+ {
+ const float duration = 300;
+
+ this.ScaleTo(1, duration, Easing.OutQuint)
+ .FadeOut(duration / 2, Easing.OutQuint);
+ }
+
+ protected override void OnSliderTick()
+ {
+ this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.08f, 40, Easing.OutQuint)
+ .Then()
+ .ScaleTo(DrawableSliderBall.FOLLOW_AREA, 200f, Easing.OutQuint);
+ }
+
+ protected override void OnSliderBreak()
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowPoint.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowPoint.cs
new file mode 100644
index 0000000000..47dae3c30a
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonFollowPoint.cs
@@ -0,0 +1,39 @@
+// 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.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonFollowPoint : CompositeDrawable
+ {
+ public ArgonFollowPoint()
+ {
+ Blending = BlendingParameters.Additive;
+
+ Colour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41"));
+ AutoSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.ChevronRight,
+ Size = new Vector2(8),
+ Colour = OsuColour.Gray(0.2f),
+ },
+ new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.ChevronRight,
+ Size = new Vector2(8),
+ X = 4,
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs
new file mode 100644
index 0000000000..b08b7b4e85
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs
@@ -0,0 +1,171 @@
+// 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.Extensions;
+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.Graphics.Sprites;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Osu.Skinning.Default;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
+ {
+ protected readonly HitResult Result;
+
+ protected SpriteText JudgementText { get; private set; } = null!;
+
+ private RingExplosion? ringExplosion;
+
+ [Resolved]
+ private OsuColour colours { get; set; } = null!;
+
+ public ArgonJudgementPiece(HitResult result)
+ {
+ Result = result;
+ Origin = Anchor.Centre;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AutoSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ JudgementText = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Text = Result.GetDescription().ToUpperInvariant(),
+ Colour = colours.ForHitResult(Result),
+ Blending = BlendingParameters.Additive,
+ Spacing = new Vector2(5, 0),
+ Font = OsuFont.Default.With(size: 20, weight: FontWeight.Bold),
+ },
+ };
+
+ if (Result.IsHit())
+ {
+ AddInternal(ringExplosion = new RingExplosion(Result)
+ {
+ Colour = colours.ForHitResult(Result),
+ });
+ }
+ }
+
+ ///
+ /// Plays the default animation for this judgement piece.
+ ///
+ ///
+ /// The base implementation only handles fade (for all result types) and misses.
+ /// Individual rulesets are recommended to implement their appropriate hit animations.
+ ///
+ public virtual void PlayAnimation()
+ {
+ switch (Result)
+ {
+ default:
+ JudgementText
+ .ScaleTo(Vector2.One)
+ .ScaleTo(new Vector2(1.2f), 1800, Easing.OutQuint);
+ break;
+
+ case HitResult.Miss:
+ this.ScaleTo(1.6f);
+ this.ScaleTo(1, 100, Easing.In);
+
+ this.MoveTo(Vector2.Zero);
+ this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
+
+ this.RotateTo(0);
+ this.RotateTo(40, 800, Easing.InQuint);
+ break;
+ }
+
+ this.FadeOutFromOne(800);
+
+ ringExplosion?.PlayAnimation();
+ }
+
+ public Drawable? GetAboveHitObjectsProxiedContent() => null;
+
+ private class RingExplosion : CompositeDrawable
+ {
+ private readonly float travel = 52;
+
+ public RingExplosion(HitResult result)
+ {
+ const float thickness = 4;
+
+ const float small_size = 9;
+ const float large_size = 14;
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ Blending = BlendingParameters.Additive;
+
+ int countSmall = 0;
+ int countLarge = 0;
+
+ switch (result)
+ {
+ case HitResult.Meh:
+ countSmall = 3;
+ travel *= 0.3f;
+ break;
+
+ case HitResult.Ok:
+ case HitResult.Good:
+ countSmall = 4;
+ travel *= 0.6f;
+ break;
+
+ case HitResult.Great:
+ case HitResult.Perfect:
+ countSmall = 4;
+ countLarge = 4;
+ break;
+ }
+
+ for (int i = 0; i < countSmall; i++)
+ AddInternal(new RingPiece(thickness) { Size = new Vector2(small_size) });
+
+ for (int i = 0; i < countLarge; i++)
+ AddInternal(new RingPiece(thickness) { Size = new Vector2(large_size) });
+ }
+
+ public void PlayAnimation()
+ {
+ foreach (var c in InternalChildren)
+ {
+ const float start_position_ratio = 0.3f;
+
+ float direction = RNG.NextSingle(0, 360);
+ float distance = RNG.NextSingle(travel / 2, travel);
+
+ c.MoveTo(new Vector2(
+ MathF.Cos(direction) * distance * start_position_ratio,
+ MathF.Sin(direction) * distance * start_position_ratio
+ ));
+
+ c.MoveTo(new Vector2(
+ MathF.Cos(direction) * distance,
+ MathF.Sin(direction) * distance
+ ), 600, Easing.OutQuint);
+ }
+
+ this.FadeOutFromOne(1000, Easing.OutQuint);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
new file mode 100644
index 0000000000..ffdcba3cdb
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
@@ -0,0 +1,226 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Skinning.Default;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonMainCirclePiece : CompositeDrawable
+ {
+ public const float BORDER_THICKNESS = (OsuHitObject.OBJECT_RADIUS * 2) * (2f / 58);
+
+ public const float GRADIENT_THICKNESS = BORDER_THICKNESS * 2.5f;
+
+ public const float OUTER_GRADIENT_SIZE = (OsuHitObject.OBJECT_RADIUS * 2) - BORDER_THICKNESS * 4;
+
+ public const float INNER_GRADIENT_SIZE = OUTER_GRADIENT_SIZE - GRADIENT_THICKNESS * 2;
+ public const float INNER_FILL_SIZE = INNER_GRADIENT_SIZE - GRADIENT_THICKNESS * 2;
+
+ private readonly Circle outerFill;
+ private readonly Circle outerGradient;
+ private readonly Circle innerGradient;
+ private readonly Circle innerFill;
+
+ private readonly RingPiece border;
+ private readonly OsuSpriteText number;
+
+ private readonly IBindable accentColour = new Bindable();
+ private readonly IBindable indexInCurrentCombo = new Bindable();
+ private readonly FlashPiece flash;
+
+ [Resolved]
+ private DrawableHitObject drawableObject { get; set; } = null!;
+
+ public ArgonMainCirclePiece(bool withOuterFill)
+ {
+ Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ InternalChildren = new Drawable[]
+ {
+ outerFill = new Circle // renders white outer border and dark fill
+ {
+ Size = Size,
+ Alpha = withOuterFill ? 1 : 0,
+ },
+ outerGradient = new Circle // renders the outer bright gradient
+ {
+ Size = new Vector2(OUTER_GRADIENT_SIZE),
+ Alpha = 1,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ innerGradient = new Circle // renders the inner bright gradient
+ {
+ Size = new Vector2(INNER_GRADIENT_SIZE),
+ Alpha = 1,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ innerFill = new Circle // renders the inner dark fill
+ {
+ Size = new Vector2(INNER_FILL_SIZE),
+ Alpha = 1,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ number = new OsuSpriteText
+ {
+ Font = OsuFont.Default.With(size: 52, weight: FontWeight.Bold),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Y = -2,
+ Text = @"1",
+ },
+ flash = new FlashPiece(),
+ border = new RingPiece(BORDER_THICKNESS),
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ var drawableOsuObject = (DrawableOsuHitObject)drawableObject;
+
+ accentColour.BindTo(drawableObject.AccentColour);
+ indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ accentColour.BindValueChanged(colour =>
+ {
+ outerFill.Colour = innerFill.Colour = colour.NewValue.Darken(4);
+ outerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.1f));
+ innerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue.Darken(0.5f), colour.NewValue.Darken(0.6f));
+ flash.Colour = colour.NewValue;
+ }, true);
+
+ indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true);
+
+ drawableObject.ApplyCustomUpdateState += updateStateTransforms;
+ updateStateTransforms(drawableObject, drawableObject.State.Value);
+ }
+
+ private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
+ {
+ using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
+ {
+ switch (state)
+ {
+ case ArmedState.Hit:
+ // Fade out time is at a maximum of 800. Must match `DrawableHitCircle`'s arbitrary lifetime spec.
+ const double fade_out_time = 800;
+
+ const double flash_in_duration = 150;
+ const double resize_duration = 400;
+
+ const float shrink_size = 0.8f;
+
+ // Animating with the number present is distracting.
+ // The number disappearing is hidden by the bright flash.
+ number.FadeOut(flash_in_duration / 2);
+
+ // The fill layers add too much noise during the explosion animation.
+ // They will be hidden by the additive effects anyway.
+ outerFill.FadeOut(flash_in_duration, Easing.OutQuint);
+ innerFill.FadeOut(flash_in_duration, Easing.OutQuint);
+
+ // The inner-most gradient should actually be resizing, but is only visible for
+ // a few milliseconds before it's hidden by the flash, so it's pointless overhead to bother with it.
+ innerGradient.FadeOut(flash_in_duration, Easing.OutQuint);
+
+ // The border is always white, but after hit it gets coloured by the skin/beatmap's colouring.
+ // A gradient is applied to make the border less prominent over the course of the animation.
+ // Without this, the border dominates the visual presence of the explosion animation in a bad way.
+ border.TransformTo(nameof
+ (BorderColour), ColourInfo.GradientVertical(
+ accentColour.Value.Opacity(0.5f),
+ accentColour.Value.Opacity(0)), fade_out_time);
+
+ // The outer ring shrinks immediately, but accounts for its thickness so it doesn't overlap the inner
+ // gradient layers.
+ border.ResizeTo(Size * shrink_size + new Vector2(border.BorderThickness), resize_duration, Easing.OutElasticHalf);
+
+ // The outer gradient is resize with a slight delay from the border.
+ // This is to give it a bomb-like effect, with the border "triggering" its animation when getting close.
+ using (BeginDelayedSequence(flash_in_duration / 12))
+ {
+ outerGradient.ResizeTo(outerGradient.Size * shrink_size, resize_duration, Easing.OutElasticHalf);
+ outerGradient
+ .FadeColour(Color4.White, 80)
+ .Then()
+ .FadeOut(flash_in_duration);
+ }
+
+ // The flash layer starts white to give the wanted brightness, but is almost immediately
+ // recoloured to the accent colour. This would more correctly be done with two layers (one for the initial flash)
+ // but works well enough with the colour fade.
+ flash.FadeTo(1, flash_in_duration, Easing.OutQuint);
+ flash.FlashColour(accentColour.Value, fade_out_time, Easing.OutQuint);
+
+ this.FadeOut(fade_out_time, Easing.OutQuad);
+ break;
+ }
+ }
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (drawableObject.IsNotNull())
+ drawableObject.ApplyCustomUpdateState -= updateStateTransforms;
+ }
+
+ private class FlashPiece : Circle
+ {
+ public FlashPiece()
+ {
+ Size = new Vector2(OsuHitObject.OBJECT_RADIUS);
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ Alpha = 0;
+ Blending = BlendingParameters.Additive;
+
+ // The edge effect provides the fill due to not being rendered hollow.
+ Child.Alpha = 0;
+ Child.AlwaysPresent = true;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = Colour,
+ Radius = OsuHitObject.OBJECT_RADIUS * 1.2f,
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs
new file mode 100644
index 0000000000..9d44db3614
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonReverseArrow.cs
@@ -0,0 +1,54 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonReverseArrow : CompositeDrawable
+ {
+ private Bindable accentColour = null!;
+
+ private SpriteIcon icon = null!;
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject hitObject)
+ {
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
+
+ InternalChildren = new Drawable[]
+ {
+ new Circle
+ {
+ Size = new Vector2(40, 20),
+ Colour = Color4.White,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ icon = new SpriteIcon
+ {
+ Icon = FontAwesome.Solid.AngleDoubleRight,
+ Size = new Vector2(16),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ };
+
+ accentColour = hitObject.AccentColour.GetBoundCopy();
+ accentColour.BindValueChanged(accent => icon.Colour = accent.NewValue.Darken(4), true);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs
new file mode 100644
index 0000000000..3df9edd225
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBall.cs
@@ -0,0 +1,109 @@
+// 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.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonSliderBall : CircularContainer
+ {
+ private readonly Box fill;
+ private readonly SpriteIcon icon;
+
+ private readonly Vector2 defaultIconScale = new Vector2(0.6f, 0.8f);
+
+ [Resolved(canBeNull: true)]
+ private DrawableHitObject? parentObject { get; set; }
+
+ public ArgonSliderBall()
+ {
+ Size = new Vector2(ArgonMainCirclePiece.OUTER_GRADIENT_SIZE);
+
+ Masking = true;
+
+ BorderThickness = ArgonMainCirclePiece.GRADIENT_THICKNESS;
+ BorderColour = Color4.White;
+
+ InternalChildren = new Drawable[]
+ {
+ fill = new Box
+ {
+ Colour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ icon = new SpriteIcon
+ {
+ Size = new Vector2(48),
+ Scale = defaultIconScale,
+ Icon = FontAwesome.Solid.AngleRight,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ if (parentObject != null)
+ {
+ parentObject.ApplyCustomUpdateState += updateStateTransforms;
+ updateStateTransforms(parentObject, parentObject.State.Value);
+ }
+ }
+
+ private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState _)
+ {
+ // Gets called by slider ticks, tails, etc., leading to duplicated
+ // animations which in this case have no visual impact (due to
+ // instant fade) but may negatively affect performance
+ if (drawableObject is not DrawableSlider)
+ return;
+
+ const float duration = 200;
+ const float icon_scale = 0.9f;
+
+ using (BeginAbsoluteSequence(drawableObject.StateUpdateTime))
+ {
+ this.FadeInFromZero(duration, Easing.OutQuint);
+ icon.ScaleTo(0).Then().ScaleTo(defaultIconScale, duration, Easing.OutElasticHalf);
+ }
+
+ using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
+ {
+ this.FadeOut(duration, Easing.OutQuint);
+ icon.ScaleTo(defaultIconScale * icon_scale, duration, Easing.OutQuint);
+ }
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ //undo rotation on layers which should not be rotated.
+ float appliedRotation = Parent.Rotation;
+
+ fill.Rotation = -appliedRotation;
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (parentObject != null)
+ parentObject.ApplyCustomUpdateState -= updateStateTransforms;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBody.cs
new file mode 100644
index 0000000000..e1642d126d
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderBody.cs
@@ -0,0 +1,40 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Game.Rulesets.Osu.Skinning.Default;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonSliderBody : PlaySliderBody
+ {
+ protected override void LoadComplete()
+ {
+ const float path_radius = ArgonMainCirclePiece.OUTER_GRADIENT_SIZE / 2;
+
+ base.LoadComplete();
+
+ AccentColourBindable.BindValueChanged(accent => BorderColour = accent.NewValue, true);
+ ScaleBindable.BindValueChanged(scale => PathRadius = path_radius * scale.NewValue, true);
+
+ // This border size thing is kind of weird, hey.
+ const float intended_thickness = ArgonMainCirclePiece.GRADIENT_THICKNESS / path_radius;
+
+ BorderSize = intended_thickness / Default.DrawableSliderPath.BORDER_PORTION;
+ }
+
+ protected override Default.DrawableSliderPath CreateSliderPath() => new DrawableSliderPath();
+
+ private class DrawableSliderPath : Default.DrawableSliderPath
+ {
+ protected override Color4 ColourAt(float position)
+ {
+ if (CalculatedBorderPortion != 0f && position <= CalculatedBorderPortion)
+ return BorderColour;
+
+ return AccentColour.Darken(4);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderScorePoint.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderScorePoint.cs
new file mode 100644
index 0000000000..4c6b9a2f17
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSliderScorePoint.cs
@@ -0,0 +1,40 @@
+// 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.Shapes;
+using osu.Game.Rulesets.Objects.Drawables;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonSliderScorePoint : CircularContainer
+ {
+ private Bindable accentColour = null!;
+
+ private const float size = 12;
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject hitObject)
+ {
+ Masking = true;
+ Origin = Anchor.Centre;
+ Size = new Vector2(size);
+ BorderThickness = 3;
+ BorderColour = Color4.White;
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ AlwaysPresent = true,
+ Alpha = 0,
+ };
+
+ accentColour = hitObject.AccentColour.GetBoundCopy();
+ accentColour.BindValueChanged(accent => BorderColour = accent.NewValue, true);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinner.cs
new file mode 100644
index 0000000000..95438e9588
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinner.cs
@@ -0,0 +1,146 @@
+// 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.Globalization;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonSpinner : CompositeDrawable
+ {
+ private DrawableSpinner drawableSpinner = null!;
+
+ private OsuSpriteText bonusCounter = null!;
+
+ private Container spmContainer = null!;
+ private OsuSpriteText spmCounter = null!;
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject drawableHitObject)
+ {
+ RelativeSizeAxes = Axes.Both;
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ drawableSpinner = (DrawableSpinner)drawableHitObject;
+
+ InternalChildren = new Drawable[]
+ {
+ bonusCounter = new OsuSpriteText
+ {
+ Alpha = 0,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Font = OsuFont.Default.With(size: 24),
+ Y = -120,
+ },
+ new ArgonSpinnerDisc
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ bonusCounter = new OsuSpriteText
+ {
+ Alpha = 0,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Font = OsuFont.Default.With(size: 28, weight: FontWeight.Bold),
+ Y = -100,
+ },
+ spmContainer = new Container
+ {
+ Alpha = 0f,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Y = 60,
+ Children = new[]
+ {
+ spmCounter = new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = @"0",
+ Font = OsuFont.Default.With(size: 28, weight: FontWeight.SemiBold)
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = @"SPINS PER MINUTE",
+ Font = OsuFont.Default.With(size: 16, weight: FontWeight.SemiBold),
+ Y = 30
+ }
+ }
+ }
+ };
+ }
+
+ private IBindable gainedBonus = null!;
+ private IBindable spinsPerMinute = null!;
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ gainedBonus = drawableSpinner.GainedBonus.GetBoundCopy();
+ gainedBonus.BindValueChanged(bonus =>
+ {
+ bonusCounter.Text = bonus.NewValue.ToString(NumberFormatInfo.InvariantInfo);
+ bonusCounter.FadeOutFromOne(1500);
+ bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
+ });
+
+ spinsPerMinute = drawableSpinner.SpinsPerMinute.GetBoundCopy();
+ spinsPerMinute.BindValueChanged(spm =>
+ {
+ spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
+ }, true);
+
+ drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
+ updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!spmContainer.IsPresent && drawableSpinner.Result?.TimeStarted != null)
+ fadeCounterOnTimeStart();
+ }
+
+ private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
+ {
+ if (!(drawableHitObject is DrawableSpinner))
+ return;
+
+ fadeCounterOnTimeStart();
+ }
+
+ private void fadeCounterOnTimeStart()
+ {
+ if (drawableSpinner.Result?.TimeStarted is double startTime)
+ {
+ using (BeginAbsoluteSequence(startTime))
+ spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn);
+ }
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (drawableSpinner.IsNotNull())
+ drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs
new file mode 100644
index 0000000000..4669b5b913
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs
@@ -0,0 +1,247 @@
+// 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.Diagnostics;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+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.Rulesets.Osu.Skinning.Default;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonSpinnerDisc : CompositeDrawable
+ {
+ private const float initial_scale = 1f;
+ private const float idle_alpha = 0.2f;
+ private const float tracking_alpha = 0.4f;
+
+ private const float idle_centre_size = 80f;
+ private const float tracking_centre_size = 40f;
+
+ private DrawableSpinner drawableSpinner = null!;
+
+ private readonly BindableBool complete = new BindableBool();
+
+ private int wholeRotationCount;
+
+ private bool checkNewRotationCount
+ {
+ get
+ {
+ int rotations = (int)(drawableSpinner.Result.RateAdjustedRotation / 360);
+
+ if (wholeRotationCount == rotations) return false;
+
+ wholeRotationCount = rotations;
+ return true;
+ }
+ }
+
+ private Container disc = null!;
+ private Container centre = null!;
+ private CircularContainer fill = null!;
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject drawableHitObject)
+ {
+ drawableSpinner = (DrawableSpinner)drawableHitObject;
+
+ // 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(initial_scale);
+
+ InternalChildren = new Drawable[]
+ {
+ disc = new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ fill = new CircularContainer
+ {
+ Name = @"Fill",
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Shadow,
+ Colour = Colour4.FromHex("FC618F").Opacity(1f),
+ Radius = 40,
+ },
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0f,
+ AlwaysPresent = true,
+ }
+ },
+ new CircularContainer
+ {
+ Name = @"Ring",
+ Masking = true,
+ BorderColour = Color4.White,
+ BorderThickness = 5,
+ RelativeSizeAxes = Axes.Both,
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true,
+ }
+ },
+ new ArgonSpinnerTicks(),
+ }
+ },
+ centre = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(idle_centre_size),
+ Children = new[]
+ {
+ new RingPiece(10)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(0.8f),
+ },
+ new RingPiece(3)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(1f),
+ }
+ },
+ },
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
+
+ updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ complete.Value = Time.Current >= drawableSpinner.Result.TimeCompleted;
+
+ if (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));
+ }
+
+ if (centre.Width == idle_centre_size && drawableSpinner.Result?.TimeStarted != null)
+ updateCentrePieceSize();
+
+ const float initial_fill_scale = 0.1f;
+ float targetScale = initial_fill_scale + (0.98f - initial_fill_scale) * drawableSpinner.Progress;
+
+ fill.Scale = new Vector2((float)Interpolation.Lerp(fill.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1)));
+ disc.Rotation = drawableSpinner.RotationTracker.Rotation;
+ }
+
+ private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
+ {
+ if (!(drawableHitObject is DrawableSpinner))
+ return;
+
+ Spinner spinner = drawableSpinner.HitObject;
+
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
+ {
+ this.ScaleTo(initial_scale);
+ this.RotateTo(0);
+
+ using (BeginDelayedSequence(spinner.TimePreempt / 2))
+ {
+ // constant ambient rotation to give the spinner "spinning" character.
+ this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration);
+ }
+
+ using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset))
+ {
+ switch (state)
+ {
+ case ArmedState.Hit:
+ this.ScaleTo(initial_scale * 1.2f, 320, Easing.Out);
+ this.RotateTo(Rotation + 180, 320);
+ break;
+
+ case ArmedState.Miss:
+ this.ScaleTo(initial_scale * 0.8f, 320, Easing.In);
+ break;
+ }
+ }
+ }
+
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
+ {
+ centre.ScaleTo(0);
+ disc.ScaleTo(0);
+
+ using (BeginDelayedSequence(spinner.TimePreempt / 2))
+ {
+ centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint);
+ disc.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint);
+
+ using (BeginDelayedSequence(spinner.TimePreempt / 2))
+ {
+ centre.ScaleTo(0.8f, spinner.TimePreempt / 2, Easing.OutQuint);
+ disc.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint);
+ }
+ }
+ }
+
+ if (drawableSpinner.Result?.TimeStarted != null)
+ updateCentrePieceSize();
+ }
+
+ private void updateCentrePieceSize()
+ {
+ Debug.Assert(drawableSpinner.Result?.TimeStarted != null);
+
+ Spinner spinner = drawableSpinner.HitObject;
+
+ using (BeginAbsoluteSequence(drawableSpinner.Result.TimeStarted.Value))
+ centre.ResizeTo(new Vector2(tracking_centre_size), spinner.TimePreempt / 2, Easing.OutQuint);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (drawableSpinner.IsNotNull())
+ drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerTicks.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerTicks.cs
new file mode 100644
index 0000000000..0203432088
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerTicks.cs
@@ -0,0 +1,61 @@
+// 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.Effects;
+using osu.Framework.Graphics.Shapes;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonSpinnerTicks : CompositeDrawable
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Origin = Anchor.Centre;
+ Anchor = Anchor.Centre;
+ RelativeSizeAxes = Axes.Both;
+
+ const float count = 25;
+
+ for (float i = 0; i < count; i++)
+ {
+ AddInternal(new CircularContainer
+ {
+ RelativePositionAxes = Axes.Both,
+ Masking = true,
+ CornerRadius = 5,
+ BorderColour = Color4.White,
+ BorderThickness = 2f,
+ Size = new Vector2(30, 5),
+ Origin = Anchor.Centre,
+ Position = new Vector2(
+ 0.5f + MathF.Sin(i / count * 2 * MathF.PI) / 2 * 0.75f,
+ 0.5f + MathF.Cos(i / count * 2 * MathF.PI) / 2 * 0.75f
+ ),
+ Rotation = -i / count * 360 - 120,
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Shadow,
+ Colour = Colour4.White.Opacity(0.2f),
+ Radius = 30,
+ },
+ Children = new[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true,
+ }
+ }
+ });
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs
new file mode 100644
index 0000000000..7bc6723afb
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/OsuArgonSkinTransformer.cs
@@ -0,0 +1,67 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class OsuArgonSkinTransformer : SkinTransformer
+ {
+ public OsuArgonSkinTransformer(ISkin skin)
+ : base(skin)
+ {
+ }
+
+ public override Drawable? GetDrawableComponent(ISkinComponent component)
+ {
+ switch (component)
+ {
+ case GameplaySkinComponent resultComponent:
+ return new ArgonJudgementPiece(resultComponent.Component);
+
+ case OsuSkinComponent osuComponent:
+ switch (osuComponent.Component)
+ {
+ case OsuSkinComponents.HitCircle:
+ return new ArgonMainCirclePiece(true);
+
+ case OsuSkinComponents.SliderHeadHitCircle:
+ return new ArgonMainCirclePiece(false);
+
+ case OsuSkinComponents.SliderBody:
+ return new ArgonSliderBody();
+
+ case OsuSkinComponents.SliderBall:
+ return new ArgonSliderBall();
+
+ case OsuSkinComponents.SliderFollowCircle:
+ return new ArgonFollowCircle();
+
+ case OsuSkinComponents.SliderScorePoint:
+ return new ArgonSliderScorePoint();
+
+ case OsuSkinComponents.SpinnerBody:
+ return new ArgonSpinner();
+
+ case OsuSkinComponents.ReverseArrow:
+ return new ArgonReverseArrow();
+
+ case OsuSkinComponents.FollowPoint:
+ return new ArgonFollowPoint();
+
+ case OsuSkinComponents.Cursor:
+ return new ArgonCursor();
+
+ case OsuSkinComponents.CursorTrail:
+ return new ArgonCursorTrail();
+ }
+
+ break;
+ }
+
+ return base.GetDrawableComponent(component);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DrawableSliderPath.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DrawableSliderPath.cs
index 94f93807d4..e3a83a9280 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/DrawableSliderPath.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/DrawableSliderPath.cs
@@ -10,8 +10,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public abstract class DrawableSliderPath : SmoothPath
{
- protected const float BORDER_PORTION = 0.128f;
- protected const float GRADIENT_PORTION = 1 - BORDER_PORTION;
+ public const float BORDER_PORTION = 0.128f;
+ public const float GRADIENT_PORTION = 1 - BORDER_PORTION;
private const float border_max_size = 8f;
private const float border_min_size = 0f;
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs
index 83f7bb8904..6c422cf127 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs
@@ -16,9 +16,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public abstract class PlaySliderBody : SnakingSliderBody
{
- private IBindable scaleBindable;
+ protected IBindable ScaleBindable { get; private set; } = null!;
+
+ protected IBindable AccentColourBindable { get; private set; } = null!;
+
private IBindable pathVersion;
- private IBindable accentColour;
[Resolved(CanBeNull = true)]
private OsuRulesetConfigManager config { get; set; }
@@ -30,14 +32,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
var drawableSlider = (DrawableSlider)drawableObject;
- scaleBindable = drawableSlider.ScaleBindable.GetBoundCopy();
- scaleBindable.BindValueChanged(scale => PathRadius = OsuHitObject.OBJECT_RADIUS * scale.NewValue, true);
+ ScaleBindable = drawableSlider.ScaleBindable.GetBoundCopy();
+ ScaleBindable.BindValueChanged(scale => PathRadius = OsuHitObject.OBJECT_RADIUS * scale.NewValue, true);
pathVersion = drawableSlider.PathVersion.GetBoundCopy();
pathVersion.BindValueChanged(_ => Refresh());
- accentColour = drawableObject.AccentColour.GetBoundCopy();
- accentColour.BindValueChanged(accent => AccentColour = GetBodyAccentColour(skin, accent.NewValue), true);
+ AccentColourBindable = drawableObject.AccentColour.GetBoundCopy();
+ AccentColourBindable.BindValueChanged(accent => AccentColour = GetBodyAccentColour(skin, accent.NewValue), true);
config?.BindWith(OsuRulesetSetting.SnakingInSliders, SnakingIn);
config?.BindWith(OsuRulesetSetting.SnakingOutSliders, configSnakingOut);
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs
index b941a86171..e813a7e274 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -14,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public class RingPiece : CircularContainer
{
- public RingPiece()
+ public RingPiece(float thickness = 9)
{
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
@@ -22,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
Origin = Anchor.Centre;
Masking = true;
- BorderThickness = 9; // roughly matches slider borders and makes stacked circles distinctly visible from each other.
+ BorderThickness = thickness;
BorderColour = Color4.White;
Child = new Box
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
index d5cc469ca9..22944becf3 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
@@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
var topProvider = source.FindProvider(s => s.GetTexture("spinner-top") != null);
- if (topProvider is LegacySkinTransformer transformer && !(transformer.Skin is DefaultLegacySkin))
+ if (topProvider is ISkinTransformer transformer && !(transformer.Skin is DefaultLegacySkin))
{
AddInternal(ApproachCircle = new Sprite
{
diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs
index a405f0e8ba..d2d5cdb6ac 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneHitJudgements.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using NUnit.Framework;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
@@ -129,5 +130,32 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
AssertResult(0, HitResult.Miss);
AssertResult(0, HitResult.IgnoreMiss);
}
+
+ [Test]
+ public void TestHighVelocityHit()
+ {
+ const double hit_time = 1000;
+
+ var beatmap = CreateBeatmap(new Hit
+ {
+ Type = HitType.Centre,
+ StartTime = hit_time,
+ });
+
+ beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 6 });
+ beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 10 });
+
+ var hitWindows = new HitWindows();
+ hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
+
+ PerformTest(new List
+ {
+ new TaikoReplayFrame(0),
+ new TaikoReplayFrame(hit_time - hitWindows.WindowFor(HitResult.Great), TaikoAction.LeftCentre),
+ }, beatmap);
+
+ AssertJudgementCount(1);
+ AssertResult(0, HitResult.Ok);
+ }
}
}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
index 95a1e8bc66..dc7bad2f75 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
@@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private int countOk;
private int countMeh;
private int countMiss;
+ private double accuracy;
private double effectiveMissCount;
@@ -36,6 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
+ accuracy = customAccuracy;
// The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000.
if (totalSuccessfulHits > 0)
@@ -87,7 +89,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (score.Mods.Any(m => m is ModFlashlight))
difficultyValue *= 1.050 * lengthBonus;
- return difficultyValue * Math.Pow(score.Accuracy, 2.0);
+ return difficultyValue * Math.Pow(accuracy, 2.0);
}
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
@@ -95,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (attributes.GreatHitWindow <= 0)
return 0;
- double accuracyValue = Math.Pow(60.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 8.0) * Math.Pow(attributes.StarRating, 0.4) * 27.0;
+ double accuracyValue = Math.Pow(60.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(accuracy, 8.0) * Math.Pow(attributes.StarRating, 0.4) * 27.0;
double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
accuracyValue *= lengthBonus;
@@ -110,5 +112,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalSuccessfulHits => countGreat + countOk + countMeh;
+
+ private double customAccuracy => totalHits > 0 ? (countGreat * 300 + countOk * 150) / (totalHits * 300.0) : 0;
}
}
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
index 66616486df..1caacdd1d7 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
@@ -4,7 +4,6 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Layout;
-using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
@@ -17,22 +16,14 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.12 : 1;
- [SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
- public override BindableFloat SizeMultiplier { get; } = new BindableFloat
+ public override BindableFloat SizeMultiplier { get; } = new BindableFloat(1)
{
MinValue = 0.5f,
MaxValue = 1.5f,
- Default = 1f,
- Value = 1f,
Precision = 0.1f
};
- [SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")]
- public override BindableBool ComboBasedSize { get; } = new BindableBool
- {
- Default = true,
- Value = true
- };
+ public override BindableBool ComboBasedSize { get; } = new BindableBool(true);
public override float DefaultFlashlightSize => 250;
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
index 705a0a8047..451c5a793b 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
Filled = HitObject.FirstTick
});
- protected override double MaximumJudgementOffset => HitObject.HitWindow;
+ public override double MaximumJudgementOffset => HitObject.HitWindow;
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index 9fc1eb7650..fdd0167ed3 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -306,7 +306,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
new Color4(128, 255, 128, 255),
new Color4(255, 187, 255, 255),
new Color4(255, 177, 140, 255),
- new Color4(100, 100, 100, 100),
+ new Color4(100, 100, 100, 255), // alpha is specified as 100, but should be ignored.
};
Assert.AreEqual(expectedColors.Length, comboColors.Count);
for (int i = 0; i < expectedColors.Length; i++)
diff --git a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
index 17709fb10f..da250c1e05 100644
--- a/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
+++ b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs
@@ -204,31 +204,23 @@ namespace osu.Game.Tests.Online
public override double ScoreMultiplier => 1;
[SettingSource("Initial rate", "The starting speed of the track")]
- public override BindableNumber InitialRate { get; } = new BindableDouble
+ public override BindableNumber InitialRate { get; } = new BindableDouble(1.5)
{
MinValue = 1,
MaxValue = 2,
- Default = 1.5,
- Value = 1.5,
Precision = 0.01,
};
[SettingSource("Final rate", "The speed increase to ramp towards")]
- public override BindableNumber FinalRate { get; } = new BindableDouble
+ public override BindableNumber FinalRate { get; } = new BindableDouble(0.5)
{
MinValue = 0,
MaxValue = 1,
- Default = 0.5,
- Value = 0.5,
Precision = 0.01,
};
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
- public override BindableBool AdjustPitch { get; } = new BindableBool
- {
- Default = true,
- Value = true
- };
+ public override BindableBool AdjustPitch { get; } = new BindableBool(true);
}
private class TestModDifficultyAdjust : ModDifficultyAdjust
diff --git a/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs b/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs
index b17414e026..1d8cbffcdb 100644
--- a/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs
+++ b/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs
@@ -124,31 +124,23 @@ namespace osu.Game.Tests.Online
public override double ScoreMultiplier => 1;
[SettingSource("Initial rate", "The starting speed of the track")]
- public override BindableNumber InitialRate { get; } = new BindableDouble
+ public override BindableNumber InitialRate { get; } = new BindableDouble(1.5)
{
MinValue = 1,
MaxValue = 2,
- Default = 1.5,
- Value = 1.5,
Precision = 0.01,
};
[SettingSource("Final rate", "The speed increase to ramp towards")]
- public override BindableNumber FinalRate { get; } = new BindableDouble
+ public override BindableNumber FinalRate { get; } = new BindableDouble(0.5)
{
MinValue = 0,
MaxValue = 1,
- Default = 0.5,
- Value = 0.5,
Precision = 0.01,
};
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
- public override BindableBool AdjustPitch { get; } = new BindableBool
- {
- Default = true,
- Value = true
- };
+ public override BindableBool AdjustPitch { get; } = new BindableBool(true);
}
private class TestModEnum : Mod
diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
index 3f20f843a7..e7590df3e0 100644
--- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
+++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
@@ -8,7 +8,6 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
-using System.Threading.Tasks;
using JetBrains.Annotations;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -78,7 +77,7 @@ namespace osu.Game.Tests.Online
}
};
- beatmaps.AllowImport = new TaskCompletionSource();
+ beatmaps.AllowImport.Reset();
testBeatmapFile = TestResources.GetQuickTestBeatmapForImport();
@@ -132,7 +131,7 @@ namespace osu.Game.Tests.Online
AddStep("finish download", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet))!.TriggerSuccess(testBeatmapFile));
addAvailabilityCheckStep("state importing", BeatmapAvailability.Importing);
- AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true));
+ AddStep("allow importing", () => beatmaps.AllowImport.Set());
AddUntilStep("wait for import", () => beatmaps.CurrentImport != null);
AddUntilStep("ensure beatmap available", () => beatmaps.IsAvailableLocally(testBeatmapSet));
addAvailabilityCheckStep("state is locally available", BeatmapAvailability.LocallyAvailable);
@@ -141,7 +140,7 @@ namespace osu.Game.Tests.Online
[Test]
public void TestTrackerRespectsSoftDeleting()
{
- AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true));
+ AddStep("allow importing", () => beatmaps.AllowImport.Set());
AddStep("import beatmap", () => beatmaps.Import(testBeatmapFile).WaitSafely());
addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable);
@@ -155,7 +154,7 @@ namespace osu.Game.Tests.Online
[Test]
public void TestTrackerRespectsChecksum()
{
- AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true));
+ AddStep("allow importing", () => beatmaps.AllowImport.Set());
AddStep("import beatmap", () => beatmaps.Import(testBeatmapFile).WaitSafely());
addAvailabilityCheckStep("initially locally available", BeatmapAvailability.LocallyAvailable);
@@ -202,7 +201,7 @@ namespace osu.Game.Tests.Online
private class TestBeatmapManager : BeatmapManager
{
- public TaskCompletionSource AllowImport = new TaskCompletionSource();
+ public readonly ManualResetEventSlim AllowImport = new ManualResetEventSlim();
public Live CurrentImport { get; private set; }
@@ -229,7 +228,9 @@ namespace osu.Game.Tests.Online
public override Live ImportModel(BeatmapSetInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default)
{
- testBeatmapManager.AllowImport.Task.WaitSafely();
+ if (!testBeatmapManager.AllowImport.Wait(TimeSpan.FromSeconds(10), cancellationToken))
+ throw new TimeoutException("Timeout waiting for import to be allowed.");
+
return (testBeatmapManager.CurrentImport = base.ImportModel(item, archive, batchImport, cancellationToken));
}
}
diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
index c3c10215a5..5c20f46787 100644
--- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
+++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
@@ -202,7 +202,7 @@ namespace osu.Game.Tests.Skins.IO
skinManager.CurrentSkinInfo.Value.PerformRead(s =>
{
Assert.IsFalse(s.Protected);
- Assert.AreEqual(typeof(DefaultSkin), s.CreateInstance(skinManager).GetType());
+ Assert.AreEqual(typeof(ArgonSkin), s.CreateInstance(skinManager).GetType());
new LegacySkinExporter(osu.Dependencies.Get()).ExportModelTo(s, exportStream);
@@ -215,7 +215,7 @@ namespace osu.Game.Tests.Skins.IO
{
Assert.IsFalse(s.Protected);
Assert.AreNotEqual(originalSkinId, s.ID);
- Assert.AreEqual(typeof(DefaultSkin), s.CreateInstance(skinManager).GetType());
+ Assert.AreEqual(typeof(ArgonSkin), s.CreateInstance(skinManager).GetType());
});
return Task.CompletedTask;
@@ -226,7 +226,7 @@ namespace osu.Game.Tests.Skins.IO
{
var skinManager = osu.Dependencies.Get();
- skinManager.CurrentSkinInfo.Value = skinManager.DefaultLegacySkin.SkinInfo;
+ skinManager.CurrentSkinInfo.Value = skinManager.DefaultClassicSkin.SkinInfo;
skinManager.EnsureMutableSkin();
diff --git a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs
index 419eb87b1a..6756f27ecd 100644
--- a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs
+++ b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Tests.Skins
new Color4(142, 199, 255, 255),
new Color4(255, 128, 128, 255),
new Color4(128, 255, 255, 255),
- new Color4(100, 100, 100, 100),
+ new Color4(100, 100, 100, 255), // alpha is specified as 100, but should be ignored.
};
Assert.AreEqual(expectedColors.Count, comboColors.Count);
diff --git a/osu.Game.Tests/Visual/Gameplay/SkinnableHUDComponentTestScene.cs b/osu.Game.Tests/Visual/Gameplay/SkinnableHUDComponentTestScene.cs
index 3ecf560eb1..ea4aa98f86 100644
--- a/osu.Game.Tests/Visual/Gameplay/SkinnableHUDComponentTestScene.cs
+++ b/osu.Game.Tests/Visual/Gameplay/SkinnableHUDComponentTestScene.cs
@@ -1,12 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
+using osu.Game.Skinning;
namespace osu.Game.Tests.Visual.Gameplay
{
@@ -19,7 +18,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
SetContents(skin =>
{
- var implementation = skin != null
+ var implementation = skin is LegacySkin
? CreateLegacyImplementation()
: CreateDefaultImplementation();
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
index e12be6d3b4..01cc856a4a 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestEmptyLegacyBeatmapSkinFallsBack()
{
- CreateSkinTest(DefaultSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null));
+ CreateSkinTest(TrianglesSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null));
AddUntilStep("wait for hud load", () => Player.ChildrenOfType().All(c => c.ComponentsLoaded));
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value));
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneColourHitErrorMeter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneColourHitErrorMeter.cs
new file mode 100644
index 0000000000..a953db4f19
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneColourHitErrorMeter.cs
@@ -0,0 +1,117 @@
+// 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.Diagnostics;
+using NUnit.Framework;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Testing;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Screens.Play.HUD.HitErrorMeters;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneColourHitErrorMeter : OsuTestScene
+ {
+ private DependencyProvidingContainer dependencyContainer = null!;
+
+ private readonly Bindable lastJudgementResult = new Bindable();
+ private ScoreProcessor scoreProcessor = null!;
+
+ private int iteration;
+
+ private ColourHitErrorMeter colourHitErrorMeter = null!;
+
+ public TestSceneColourHitErrorMeter()
+ {
+ AddSliderStep("Judgement spacing", 0, 10, 2, spacing =>
+ {
+ if (colourHitErrorMeter.IsNotNull())
+ colourHitErrorMeter.JudgementSpacing.Value = spacing;
+ });
+
+ AddSliderStep("Judgement count", 1, 50, 5, spacing =>
+ {
+ if (colourHitErrorMeter.IsNotNull())
+ colourHitErrorMeter.JudgementCount.Value = spacing;
+ });
+ }
+
+ [SetUpSteps]
+ public void SetupSteps() => AddStep("Create components", () =>
+ {
+ var ruleset = CreateRuleset();
+
+ Debug.Assert(ruleset != null);
+
+ scoreProcessor = new ScoreProcessor(ruleset);
+ Child = dependencyContainer = new DependencyProvidingContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ CachedDependencies = new (Type, object)[]
+ {
+ (typeof(ScoreProcessor), scoreProcessor)
+ }
+ };
+ dependencyContainer.Child = colourHitErrorMeter = new ColourHitErrorMeter
+ {
+ Margin = new MarginPadding
+ {
+ Top = 100
+ },
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Scale = new Vector2(2),
+ };
+ });
+
+ protected override Ruleset CreateRuleset() => new OsuRuleset();
+
+ [Test]
+ public void TestSpacingChange()
+ {
+ AddRepeatStep("Add judgement", applyOneJudgement, 5);
+ AddStep("Change spacing", () => colourHitErrorMeter.JudgementSpacing.Value = 10);
+ AddRepeatStep("Add judgement", applyOneJudgement, 5);
+ }
+
+ [Test]
+ public void TestJudgementAmountChange()
+ {
+ AddRepeatStep("Add judgement", applyOneJudgement, 10);
+ AddStep("Judgement count change to 4", () => colourHitErrorMeter.JudgementCount.Value = 4);
+ AddRepeatStep("Add judgement", applyOneJudgement, 8);
+ }
+
+ [Test]
+ public void TestHitErrorShapeChange()
+ {
+ AddRepeatStep("Add judgement", applyOneJudgement, 8);
+ AddStep("Change shape square", () => colourHitErrorMeter.JudgementShape.Value = ColourHitErrorMeter.ShapeStyle.Square);
+ AddRepeatStep("Add judgement", applyOneJudgement, 10);
+ AddStep("Change shape circle", () => colourHitErrorMeter.JudgementShape.Value = ColourHitErrorMeter.ShapeStyle.Circle);
+ }
+
+ private void applyOneJudgement()
+ {
+ lastJudgementResult.Value = new OsuJudgementResult(new HitObject
+ {
+ StartTime = iteration * 10000,
+ }, new OsuJudgement())
+ {
+ Type = HitResult.Great,
+ };
+ scoreProcessor.ApplyResult(lastJudgementResult.Value);
+
+ iteration++;
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs
index 663e398c01..171ae829a9 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs
@@ -6,7 +6,9 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.PolygonExtensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses;
@@ -18,37 +20,62 @@ namespace osu.Game.Tests.Visual.Gameplay
[TestFixture]
public class TestSceneGameplayLeaderboard : OsuTestScene
{
- private readonly TestGameplayLeaderboard leaderboard;
+ private TestGameplayLeaderboard leaderboard;
private readonly BindableDouble playerScore = new BindableDouble();
public TestSceneGameplayLeaderboard()
{
- Add(leaderboard = new TestGameplayLeaderboard
+ AddStep("toggle expanded", () =>
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- Scale = new Vector2(2),
+ if (leaderboard != null)
+ leaderboard.Expanded.Value = !leaderboard.Expanded.Value;
});
+
+ AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v);
}
- [SetUpSteps]
- public void SetUpSteps()
+ [Test]
+ public void TestLayoutWithManyScores()
{
- AddStep("reset leaderboard", () =>
+ createLeaderboard();
+
+ AddStep("add many scores in one go", () =>
{
- leaderboard.Clear();
- playerScore.Value = 1222333;
+ for (int i = 0; i < 32; i++)
+ createRandomScore(new APIUser { Username = $"Player {i + 1}" });
+
+ // Add player at end to force an animation down the whole list.
+ playerScore.Value = 0;
+ createLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true);
});
- AddStep("add local player", () => createLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true));
- AddStep("toggle expanded", () => leaderboard.Expanded.Value = !leaderboard.Expanded.Value);
- AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v);
+ // Gameplay leaderboard has custom scroll logic, which when coupled with LayoutDuration
+ // has caused layout to not work in the past.
+
+ AddUntilStep("wait for fill flow layout",
+ () => leaderboard.ChildrenOfType>().First().ScreenSpaceDrawQuad.Intersects(leaderboard.ScreenSpaceDrawQuad));
+
+ AddUntilStep("wait for some scores not masked away",
+ () => leaderboard.ChildrenOfType().Any(s => leaderboard.ScreenSpaceDrawQuad.Contains(s.ScreenSpaceDrawQuad.Centre)));
+
+ AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad));
+
+ AddStep("change score to middle", () => playerScore.Value = 1000000);
+ AddWaitStep("wait for movement", 5);
+ AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad));
+
+ AddStep("change score to first", () => playerScore.Value = 5000000);
+ AddWaitStep("wait for movement", 5);
+ AddUntilStep("wait for tracked score fully visible", () => leaderboard.ScreenSpaceDrawQuad.Intersects(leaderboard.TrackedScore!.ScreenSpaceDrawQuad));
}
[Test]
public void TestPlayerScore()
{
+ createLeaderboard();
+ addLocalPlayer();
+
var player2Score = new BindableDouble(1234567);
var player3Score = new BindableDouble(1111111);
@@ -73,6 +100,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestRandomScores()
{
+ createLeaderboard();
+ addLocalPlayer();
+
int playerNumber = 1;
AddRepeatStep("add player with random score", () => createRandomScore(new APIUser { Username = $"Player {playerNumber++}" }), 10);
}
@@ -80,6 +110,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestExistingUsers()
{
+ createLeaderboard();
+ addLocalPlayer();
+
AddStep("add peppy", () => createRandomScore(new APIUser { Username = "peppy", Id = 2 }));
AddStep("add smoogipoo", () => createRandomScore(new APIUser { Username = "smoogipoo", Id = 1040328 }));
AddStep("add flyte", () => createRandomScore(new APIUser { Username = "flyte", Id = 3103765 }));
@@ -89,6 +122,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestMaxHeight()
{
+ createLeaderboard();
+ addLocalPlayer();
+
int playerNumber = 1;
AddRepeatStep("add 3 other players", () => createRandomScore(new APIUser { Username = $"Player {playerNumber++}" }), 3);
checkHeight(4);
@@ -103,6 +139,28 @@ namespace osu.Game.Tests.Visual.Gameplay
=> AddAssert($"leaderboard height is {panelCount} panels high", () => leaderboard.DrawHeight == (GameplayLeaderboardScore.PANEL_HEIGHT + leaderboard.Spacing) * panelCount);
}
+ private void addLocalPlayer()
+ {
+ AddStep("add local player", () =>
+ {
+ playerScore.Value = 1222333;
+ createLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true);
+ });
+ }
+
+ private void createLeaderboard()
+ {
+ AddStep("create leaderboard", () =>
+ {
+ Child = leaderboard = new TestGameplayLeaderboard
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Scale = new Vector2(2),
+ };
+ });
+ }
+
private void createRandomScore(APIUser user) => createLeaderboardScore(new BindableDouble(RNG.Next(0, 5_000_000)), user);
private void createLeaderboardScore(BindableDouble score, APIUser user, bool isTracked = false)
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
index 707f807e64..7c668adba5 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs
@@ -107,13 +107,13 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("no bars added", () => !this.ChildrenOfType().Any());
AddAssert("circle added", () =>
this.ChildrenOfType().All(
- meter => meter.ChildrenOfType().Count() == 1));
+ meter => meter.ChildrenOfType().Count() == 1));
AddStep("miss", () => newJudgement(50, HitResult.Miss));
AddAssert("no bars added", () => !this.ChildrenOfType().Any());
AddAssert("circle added", () =>
this.ChildrenOfType().All(
- meter => meter.ChildrenOfType().Count() == 2));
+ meter => meter.ChildrenOfType().Count() == 2));
}
[Test]
@@ -123,11 +123,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("small bonus", () => newJudgement(result: HitResult.SmallBonus));
AddAssert("no bars added", () => !this.ChildrenOfType().Any());
- AddAssert("no circle added", () => !this.ChildrenOfType().Any());
+ AddAssert("no circle added", () => !this.ChildrenOfType().Any());
AddStep("large bonus", () => newJudgement(result: HitResult.LargeBonus));
AddAssert("no bars added", () => !this.ChildrenOfType().Any());
- AddAssert("no circle added", () => !this.ChildrenOfType().Any());
+ AddAssert("no circle added", () => !this.ChildrenOfType().Any());
}
[Test]
@@ -137,16 +137,17 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("ignore hit", () => newJudgement(result: HitResult.IgnoreHit));
AddAssert("no bars added", () => !this.ChildrenOfType().Any());
- AddAssert("no circle added", () => !this.ChildrenOfType().Any());
+ AddAssert("no circle added", () => !this.ChildrenOfType().Any());
AddStep("ignore miss", () => newJudgement(result: HitResult.IgnoreMiss));
AddAssert("no bars added", () => !this.ChildrenOfType().Any());
- AddAssert("no circle added", () => !this.ChildrenOfType().Any());
+ AddAssert("no circle added", () => !this.ChildrenOfType().Any());
}
[Test]
public void TestProcessingWhileHidden()
{
+ const int max_displayed_judgements = 20;
AddStep("OD 1", () => recreateDisplay(new OsuHitWindows(), 1));
AddStep("hide displays", () =>
@@ -155,16 +156,16 @@ namespace osu.Game.Tests.Visual.Gameplay
hitErrorMeter.Hide();
});
- AddRepeatStep("hit", () => newJudgement(), ColourHitErrorMeter.MAX_DISPLAYED_JUDGEMENTS * 2);
+ AddRepeatStep("hit", () => newJudgement(), max_displayed_judgements * 2);
AddAssert("bars added", () => this.ChildrenOfType().Any());
- AddAssert("circle added", () => this.ChildrenOfType().Any());
+ AddAssert("circle added", () => this.ChildrenOfType().Any());
AddUntilStep("wait for bars to disappear", () => !this.ChildrenOfType().Any());
AddUntilStep("ensure max circles not exceeded", () =>
{
return this.ChildrenOfType()
- .All(m => m.ChildrenOfType().Count() <= ColourHitErrorMeter.MAX_DISPLAYED_JUDGEMENTS);
+ .All(m => m.ChildrenOfType().Count() <= max_displayed_judgements);
});
AddStep("show displays", () =>
@@ -183,12 +184,12 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("bar added", () => this.ChildrenOfType().All(
meter => meter.ChildrenOfType().Count() == 1));
AddAssert("circle added", () => this.ChildrenOfType().All(
- meter => meter.ChildrenOfType().Count() == 1));
+ meter => meter.ChildrenOfType().Count() == 1));
AddStep("clear", () => this.ChildrenOfType().ForEach(meter => meter.Clear()));
AddAssert("bar cleared", () => !this.ChildrenOfType().Any());
- AddAssert("colour cleared", () => !this.ChildrenOfType().Any());
+ AddAssert("colour cleared", () => !this.ChildrenOfType().Any());
}
private void recreateDisplay(HitWindows hitWindows, float overallDifficulty)
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneParticleSpewer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneParticleSpewer.cs
index 26706d9465..66441c8bad 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneParticleSpewer.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneParticleSpewer.cs
@@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}
private TestParticleSpewer createSpewer() =>
- new TestParticleSpewer(skinManager.DefaultLegacySkin.GetTexture("star2"))
+ new TestParticleSpewer(skinManager.DefaultClassicSkin.GetTexture("star2"))
{
Origin = Anchor.Centre,
RelativePositionAxes = Axes.Both,
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
index 1d101383cc..6b24ac7384 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
@@ -264,13 +264,13 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestMutedNotificationMasterVolume()
{
- addVolumeSteps("master volume", () => audioManager.Volume.Value = 0, () => audioManager.Volume.IsDefault);
+ addVolumeSteps("master volume", () => audioManager.Volume.Value = 0, () => audioManager.Volume.Value == 0.5);
}
[Test]
public void TestMutedNotificationTrackVolume()
{
- addVolumeSteps("music volume", () => audioManager.VolumeTrack.Value = 0, () => audioManager.VolumeTrack.IsDefault);
+ addVolumeSteps("music volume", () => audioManager.VolumeTrack.Value = 0, () => audioManager.VolumeTrack.Value == 0.5);
}
[Test]
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs
index 247b822dc3..38a091dd85 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLocalScoreImport.cs
@@ -15,8 +15,10 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
@@ -101,6 +103,37 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for last played to update", () => getLastPlayed() != null);
}
+ [Test]
+ public void TestModReferenceNotRetained()
+ {
+ AddStep("allow fail", () => allowFail = false);
+
+ Mod[] originalMods = { new OsuModDaycore { SpeedChange = { Value = 0.8 } } };
+ Mod[] playerMods = null!;
+
+ AddStep("load player with mods", () => LoadPlayer(originalMods));
+ AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1);
+
+ AddStep("get mods at start of gameplay", () => playerMods = Player.Score.ScoreInfo.Mods.ToArray());
+
+ // Player creates new instance of mods during load.
+ AddAssert("player score has copied mods", () => playerMods.First(), () => Is.Not.SameAs(originalMods.First()));
+ AddAssert("player score has matching mods", () => playerMods.First(), () => Is.EqualTo(originalMods.First()));
+
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
+
+ AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
+
+ AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
+
+ // Player creates new instance of mods after gameplay to ensure any runtime references to drawables etc. are not retained.
+ AddAssert("results screen score has copied mods", () => (Player.GetChildScreen() as ResultsScreen)?.Score.Mods.First(), () => Is.Not.SameAs(playerMods.First()));
+ AddAssert("results screen score has matching", () => (Player.GetChildScreen() as ResultsScreen)?.Score.Mods.First(), () => Is.EqualTo(playerMods.First()));
+
+ AddUntilStep("score in database", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID) != null));
+ AddUntilStep("databased score has correct mods", () => Realm.Run(r => r.Find(Player.Score.ScoreInfo.ID)).Mods.First(), () => Is.EqualTo(playerMods.First()));
+ }
+
[Test]
public void TestScoreStoredLocally()
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
index 8a4818d2f8..156a1ee34a 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
@@ -11,8 +11,10 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Threading;
+using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
@@ -167,14 +169,39 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("add control points", () => addControlPoints(testControlPoints, Time.Current));
}
- private void addHitObject(double time)
+ [Test]
+ public void TestVeryFlowScroll()
+ {
+ const double long_time_range = 100000;
+ var manualClock = new ManualClock();
+
+ AddStep("set manual clock", () =>
+ {
+ manualClock.CurrentTime = 0;
+ scrollContainers.ForEach(c => c.Clock = new FramedClock(manualClock));
+
+ setScrollAlgorithm(ScrollVisualisationMethod.Constant);
+ scrollContainers.ForEach(c => c.TimeRange = long_time_range);
+ });
+
+ AddStep("add hit objects", () =>
+ {
+ addHitObject(long_time_range);
+ addHitObject(long_time_range + 100, 250);
+ });
+
+ AddAssert("hit objects are alive", () => playfields.All(p => p.HitObjectContainer.AliveObjects.Count() == 2));
+ }
+
+ private void addHitObject(double time, float size = 75)
{
playfields.ForEach(p =>
{
- var hitObject = new TestDrawableHitObject(time);
- setAnchor(hitObject, p);
+ var hitObject = new TestHitObject(size) { StartTime = time };
+ var drawable = new TestDrawableHitObject(hitObject);
- p.Add(hitObject);
+ setAnchor(drawable, p);
+ p.Add(drawable);
});
}
@@ -248,6 +275,8 @@ namespace osu.Game.Tests.Visual.Gameplay
}
};
}
+
+ protected override ScrollingHitObjectContainer CreateScrollingHitObjectContainer() => new TestScrollingHitObjectContainer();
}
private class TestDrawableControlPoint : DrawableHitObject
@@ -281,22 +310,41 @@ namespace osu.Game.Tests.Visual.Gameplay
}
}
- private class TestDrawableHitObject : DrawableHitObject
+ private class TestHitObject : HitObject
{
- public TestDrawableHitObject(double time)
- : base(new HitObject { StartTime = time, HitWindows = HitWindows.Empty })
- {
- Origin = Anchor.Custom;
- OriginPosition = new Vector2(75 / 4.0f);
+ public readonly float Size;
- AutoSizeAxes = Axes.Both;
+ public TestHitObject(float size)
+ {
+ Size = size;
+ }
+ }
+
+ private class TestDrawableHitObject : DrawableHitObject
+ {
+ public TestDrawableHitObject(TestHitObject hitObject)
+ : base(hitObject)
+ {
+ Origin = Anchor.Centre;
+ Size = new Vector2(hitObject.Size);
AddInternal(new Box
{
- Size = new Vector2(75),
+ RelativeSizeAxes = Axes.Both,
Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1)
});
}
}
+
+ private class TestScrollingHitObjectContainer : ScrollingHitObjectContainer
+ {
+ protected override RectangleF GetConservativeBoundingBox(HitObjectLifetimeEntry entry)
+ {
+ if (entry.HitObject is TestHitObject testObject)
+ return new RectangleF().Inflate(testObject.Size / 2);
+
+ return base.GetConservativeBoundingBox(entry);
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs
new file mode 100644
index 0000000000..60ed0012ae
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSoloGameplayLeaderboard.cs
@@ -0,0 +1,101 @@
+// 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.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Configuration;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+using osu.Game.Screens.Play.HUD;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneSoloGameplayLeaderboard : OsuTestScene
+ {
+ [Cached]
+ private readonly ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
+
+ private readonly BindableList scores = new BindableList();
+
+ private readonly Bindable configVisibility = new Bindable();
+
+ private SoloGameplayLeaderboard leaderboard = null!;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility);
+ }
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("clear scores", () => scores.Clear());
+
+ AddStep("create component", () =>
+ {
+ var trackingUser = new APIUser
+ {
+ Username = "local user",
+ Id = 2,
+ };
+
+ Child = leaderboard = new SoloGameplayLeaderboard(trackingUser)
+ {
+ Scores = { BindTarget = scores },
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AlwaysVisible = { Value = false },
+ Expanded = { Value = true },
+ };
+ });
+
+ AddStep("add scores", () => scores.AddRange(createSampleScores()));
+ }
+
+ [Test]
+ public void TestLocalUser()
+ {
+ AddSliderStep("score", 0, 1000000, 500000, v => scoreProcessor.TotalScore.Value = v);
+ AddSliderStep("accuracy", 0f, 1f, 0.5f, v => scoreProcessor.Accuracy.Value = v);
+ AddSliderStep("combo", 0, 10000, 0, v => scoreProcessor.HighestCombo.Value = v);
+ AddStep("toggle expanded", () => leaderboard.Expanded.Value = !leaderboard.Expanded.Value);
+ }
+
+ [Test]
+ public void TestVisibility()
+ {
+ AddStep("set config visible true", () => configVisibility.Value = true);
+ AddUntilStep("leaderboard visible", () => leaderboard.Alpha == 1);
+
+ AddStep("set config visible false", () => configVisibility.Value = false);
+ AddUntilStep("leaderboard not visible", () => leaderboard.Alpha == 0);
+
+ AddStep("set always visible", () => leaderboard.AlwaysVisible.Value = true);
+ AddUntilStep("leaderboard visible", () => leaderboard.Alpha == 1);
+
+ AddStep("set config visible true", () => configVisibility.Value = true);
+ AddAssert("leaderboard still visible", () => leaderboard.Alpha == 1);
+ }
+
+ private static List createSampleScores()
+ {
+ return new[]
+ {
+ new ScoreInfo { User = new APIUser { Username = @"peppy" }, TotalScore = RNG.Next(500000, 1000000) },
+ new ScoreInfo { User = new APIUser { Username = @"smoogipoo" }, TotalScore = RNG.Next(500000, 1000000) },
+ new ScoreInfo { User = new APIUser { Username = @"spaceman_atlas" }, TotalScore = RNG.Next(500000, 1000000) },
+ new ScoreInfo { User = new APIUser { Username = @"frenzibyte" }, TotalScore = RNG.Next(500000, 1000000) },
+ new ScoreInfo { User = new APIUser { Username = @"Susko3" }, TotalScore = RNG.Next(500000, 1000000) },
+ }.Concat(Enumerable.Range(0, 50).Select(i => new ScoreInfo { User = new APIUser { Username = $"User {i + 1}" }, TotalScore = 1000000 - i * 10000 })).ToList();
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs
index 3487f4dbff..6127aa304c 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSongProgress.cs
@@ -1,8 +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;
using System.Collections.Generic;
-using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
@@ -51,13 +51,14 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestToggleSeeking()
{
- DefaultSongProgress getDefaultProgress() => this.ChildrenOfType().Single();
+ void applyToDefaultProgress(Action action) =>
+ this.ChildrenOfType().ForEach(action);
- AddStep("allow seeking", () => getDefaultProgress().AllowSeeking.Value = true);
- AddStep("hide graph", () => getDefaultProgress().ShowGraph.Value = false);
- AddStep("disallow seeking", () => getDefaultProgress().AllowSeeking.Value = false);
- AddStep("allow seeking", () => getDefaultProgress().AllowSeeking.Value = true);
- AddStep("show graph", () => getDefaultProgress().ShowGraph.Value = true);
+ AddStep("allow seeking", () => applyToDefaultProgress(s => s.AllowSeeking.Value = true));
+ AddStep("hide graph", () => applyToDefaultProgress(s => s.ShowGraph.Value = false));
+ AddStep("disallow seeking", () => applyToDefaultProgress(s => s.AllowSeeking.Value = false));
+ AddStep("allow seeking", () => applyToDefaultProgress(s => s.AllowSeeking.Value = true));
+ AddStep("show graph", () => applyToDefaultProgress(s => s.ShowGraph.Value = true));
}
private void setHitObjects()
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
index e2c825df0b..a26a7e97be 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
@@ -32,6 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected new OutroPlayer Player => (OutroPlayer)base.Player;
+ private double currentBeatmapDuration;
private double currentStoryboardDuration;
private bool showResults = true;
@@ -45,7 +46,8 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("enable storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, true));
AddStep("set dim level to 0", () => LocalConfig.SetValue(OsuSetting.DimLevel, 0));
AddStep("reset fail conditions", () => currentFailConditions = (_, _) => false);
- AddStep("set storyboard duration to 2s", () => currentStoryboardDuration = 2000);
+ AddStep("set beatmap duration to 0s", () => currentBeatmapDuration = 0);
+ AddStep("set storyboard duration to 8s", () => currentStoryboardDuration = 8000);
AddStep("set ShowResults = true", () => showResults = true);
}
@@ -151,6 +153,24 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("player exited", () => Stack.CurrentScreen == null);
}
+ [Test]
+ public void TestPerformExitAfterOutro()
+ {
+ CreateTest(() =>
+ {
+ AddStep("set beatmap duration to 4s", () => currentBeatmapDuration = 4000);
+ AddStep("set storyboard duration to 1s", () => currentStoryboardDuration = 1000);
+ });
+
+ AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration);
+ AddStep("exit via pause", () => Player.ExitViaPause());
+ AddAssert("player paused", () => !Player.IsResuming);
+
+ AddStep("resume player", () => Player.Resume());
+ AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
+ AddUntilStep("wait for score shown", () => Player.IsScoreShown);
+ }
+
protected override bool AllowFail => true;
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
@@ -160,7 +180,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new Beatmap();
- beatmap.HitObjects.Add(new HitCircle());
+ beatmap.HitObjects.Add(new HitCircle { StartTime = currentBeatmapDuration });
return beatmap;
}
@@ -189,7 +209,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private event Func failConditions;
public OutroPlayer(Func failConditions, bool showResults = true)
- : base(false, showResults)
+ : base(showResults: showResults)
{
this.failConditions = failConditions;
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs
index 9e6941738a..4fda4c1c50 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorLeaderboard.cs
@@ -120,6 +120,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
=> AddStep($"set user {userId} time {time}", () => clocks[userId].CurrentTime = time);
private void assertCombo(int userId, int expectedCombo)
- => AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType().Single(s => s.User?.Id == userId).Combo.Value == expectedCombo);
+ => AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType().Single(s => s.User?.OnlineID == userId).Combo.Value == expectedCombo);
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
index 70f498e7f2..13fde4fd72 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
@@ -522,7 +522,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType().Single(p => p.UserId == userId);
- private GameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType().Single(s => s.User?.Id == userId);
+ private GameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType().Single(s => s.User?.OnlineID == userId);
private int[] getPlayerIds(int count) => Enumerable.Range(PLAYER_1_ID, count).ToArray();
}
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs b/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs
index 4ca55e8744..010ed23c9b 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Navigation
[Test]
public void TestEditDefaultSkin()
{
- AddAssert("is default skin", () => skinManager.CurrentSkinInfo.Value.ID == SkinInfo.DEFAULT_SKIN);
+ AddAssert("is default skin", () => skinManager.CurrentSkinInfo.Value.ID == SkinInfo.ARGON_SKIN);
AddStep("open settings", () => { Game.Settings.Show(); });
@@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("open skin editor", () => skinEditor.Show());
// Until step required as the skin editor may take time to load (and an extra scheduled frame for the mutable part).
- AddUntilStep("is modified default skin", () => skinManager.CurrentSkinInfo.Value.ID != SkinInfo.DEFAULT_SKIN);
+ AddUntilStep("is modified default skin", () => skinManager.CurrentSkinInfo.Value.ID != SkinInfo.ARGON_SKIN);
AddAssert("is not protected", () => skinManager.CurrentSkinInfo.Value.PerformRead(s => !s.Protected));
AddUntilStep("export button enabled", () => Game.Settings.ChildrenOfType().SingleOrDefault()?.Enabled.Value == true);
diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs
index 5e76fe1519..003cec0d07 100644
--- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs
@@ -9,8 +9,10 @@ using NUnit.Framework;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
+using osu.Game.Configuration;
using osu.Game.Online.API;
using osu.Game.Rulesets;
+using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
@@ -92,6 +94,31 @@ namespace osu.Game.Tests.Visual.Navigation
returnToMenu();
}
+ [Test]
+ public void TestFromSongSelectWithFilter([Values] ScorePresentType type)
+ {
+ AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke());
+ AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
+
+ AddStep("filter to nothing", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).FilterControl.CurrentTextSearch.Value = "fdsajkl;fgewq");
+ AddUntilStep("wait for no results", () => Beatmap.IsDefault);
+
+ var firstImport = importScore(1, new CatchRuleset().RulesetInfo);
+ presentAndConfirm(firstImport, type);
+ }
+
+ [Test]
+ public void TestFromSongSelectWithConvertRulesetChange([Values] ScorePresentType type)
+ {
+ AddStep("enter song select", () => Game.ChildrenOfType().Single().OnSolo.Invoke());
+ AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
+
+ AddStep("set convert to false", () => Game.LocalConfig.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
+
+ var firstImport = importScore(1, new CatchRuleset().RulesetInfo);
+ presentAndConfirm(firstImport, type);
+ }
+
[Test]
public void TestFromSongSelect([Values] ScorePresentType type)
{
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs
index b3d1966511..6ff53663ba 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs
@@ -29,11 +29,7 @@ namespace osu.Game.Tests.Visual.Settings
{
Child = textBox = new SettingsTextBox
{
- Current = new Bindable
- {
- Default = "test",
- Value = "test"
- }
+ Current = new Bindable("test")
};
});
AddUntilStep("wait for loaded", () => textBox.IsLoaded);
@@ -59,11 +55,7 @@ namespace osu.Game.Tests.Visual.Settings
{
Child = textBox = new SettingsTextBox
{
- Current = new Bindable
- {
- Default = "test",
- Value = "test"
- }
+ Current = new Bindable("test")
};
});
AddUntilStep("wait for loaded", () => textBox.IsLoaded);
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs
index dc2a687bd5..3cf6f7febf 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs
@@ -67,11 +67,7 @@ namespace osu.Game.Tests.Visual.Settings
};
[SettingSource("Sample number textbox", "Textbox number entry", SettingControlType = typeof(SettingsNumberBox))]
- public Bindable IntTextBoxBindable { get; } = new Bindable
- {
- Default = null,
- Value = null
- };
+ public Bindable IntTextBoxBindable { get; } = new Bindable();
}
private enum TestEnum
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
index 07da1790c8..1839821bb5 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -36,10 +34,9 @@ namespace osu.Game.Tests.Visual.SongSelect
[Cached(typeof(IDialogOverlay))]
private readonly DialogOverlay dialogOverlay;
- private ScoreManager scoreManager;
-
- private RulesetStore rulesetStore;
- private BeatmapManager beatmapManager;
+ private ScoreManager scoreManager = null!;
+ private RulesetStore rulesetStore = null!;
+ private BeatmapManager beatmapManager = null!;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
@@ -74,7 +71,7 @@ namespace osu.Game.Tests.Visual.SongSelect
[Test]
public void TestLocalScoresDisplay()
{
- BeatmapInfo beatmapInfo = null;
+ BeatmapInfo beatmapInfo = null!;
AddStep(@"Set scope", () => leaderboard.Scope = BeatmapLeaderboardScope.Local);
@@ -387,7 +384,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private class FailableLeaderboard : BeatmapLeaderboard
{
public new void SetErrorState(LeaderboardState state) => base.SetErrorState(state);
- public new void SetScores(IEnumerable scores, ScoreInfo userScore = default) => base.SetScores(scores, userScore);
+ public new void SetScores(IEnumerable? scores, ScoreInfo? userScore = null) => base.SetScores(scores, userScore);
}
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
index db380cfdb7..c1a9768cf0 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
@@ -163,7 +163,7 @@ namespace osu.Game.Tests.Visual.UserInterface
InputManager.PressButton(MouseButton.Left);
});
- AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
+ AddUntilStep("wait for fetch", () => leaderboard.Scores.Any());
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineID != scoreBeingDeleted.OnlineID));
// "Clean up"
@@ -174,7 +174,7 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestDeleteViaDatabase()
{
AddStep("delete top score", () => scoreManager.Delete(importedScores[0]));
- AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
+ AddUntilStep("wait for fetch", () => leaderboard.Scores.Any());
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineID != importedScores[0].OnlineID));
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs
index a97096e143..7ed08d8dff 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.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 System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@@ -10,6 +11,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.Sprites;
+using osu.Game.Online.Multiplayer;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Updater;
@@ -32,6 +34,8 @@ namespace osu.Game.Tests.Visual.UserInterface
[SetUp]
public void SetUp() => Schedule(() =>
{
+ InputManager.MoveMouseTo(Vector2.Zero);
+
TimeToCompleteProgress = 2000;
progressingNotifications.Clear();
@@ -103,9 +107,9 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("start drag", () =>
{
- InputManager.MoveMouseTo(notification.ChildrenOfType().Single());
+ InputManager.MoveMouseTo(notificationOverlay.ChildrenOfType().Single());
InputManager.PressButton(MouseButton.Left);
- InputManager.MoveMouseTo(notification.ChildrenOfType().Single().ScreenSpaceDrawQuad.Centre + new Vector2(-500, 0));
+ InputManager.MoveMouseTo(notificationOverlay.ChildrenOfType().Single().ScreenSpaceDrawQuad.Centre + new Vector2(-500, 0));
});
AddStep("fling away", () =>
@@ -119,6 +123,45 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("unread count zero", () => notificationOverlay.UnreadCount.Value == 0);
}
+ [Test]
+ public void TestProgressNotificationCantBeFlung()
+ {
+ bool activated = false;
+ ProgressNotification notification = null!;
+
+ AddStep("post", () =>
+ {
+ activated = false;
+ notificationOverlay.Post(notification = new ProgressNotification
+ {
+ Text = @"Uploading to BSS...",
+ CompletionText = "Uploaded to BSS!",
+ Activated = () => activated = true,
+ });
+
+ progressingNotifications.Add(notification);
+ });
+
+ AddStep("start drag", () =>
+ {
+ InputManager.MoveMouseTo(notificationOverlay.ChildrenOfType().Single());
+ InputManager.PressButton(MouseButton.Left);
+ InputManager.MoveMouseTo(notificationOverlay.ChildrenOfType().Single().ScreenSpaceDrawQuad.Centre + new Vector2(-500, 0));
+ });
+
+ AddStep("attempt fling", () =>
+ {
+ InputManager.ReleaseButton(MouseButton.Left);
+ });
+
+ AddUntilStep("was not closed", () => !notification.WasClosed);
+ AddUntilStep("was not cancelled", () => notification.State == ProgressNotificationState.Active);
+ AddAssert("was not activated", () => !activated);
+ AddStep("reset mouse position", () => InputManager.MoveMouseTo(Vector2.Zero));
+
+ AddUntilStep("was completed", () => notification.State == ProgressNotificationState.Completed);
+ }
+
[Test]
public void TestDismissWithoutActivationCloseButton()
{
@@ -228,6 +271,31 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("wait overlay not present", () => !notificationOverlay.IsPresent);
}
+ [Test]
+ public void TestProgressClick()
+ {
+ ProgressNotification notification = null!;
+
+ AddStep("add progress notification", () =>
+ {
+ notification = new ProgressNotification
+ {
+ Text = @"Uploading to BSS...",
+ CompletionText = "Uploaded to BSS!",
+ };
+ notificationOverlay.Post(notification);
+ progressingNotifications.Add(notification);
+ });
+
+ AddStep("hover over notification", () => InputManager.MoveMouseTo(notificationOverlay.ChildrenOfType().Single()));
+
+ AddStep("left click", () => InputManager.Click(MouseButton.Left));
+ AddAssert("not cancelled", () => notification.State == ProgressNotificationState.Active);
+
+ AddStep("right click", () => InputManager.Click(MouseButton.Right));
+ AddAssert("cancelled", () => notification.State == ProgressNotificationState.Cancelled);
+ }
+
[Test]
public void TestCompleteProgress()
{
@@ -299,7 +367,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
SimpleNotification notification = null!;
AddStep(@"post", () => notificationOverlay.Post(notification = new BackgroundNotification { Text = @"Welcome to osu!. Enjoy your stay!" }));
- AddUntilStep("check is toast", () => !notification.IsInToastTray);
+ AddUntilStep("check is toast", () => notification.IsInToastTray);
AddAssert("light is not visible", () => notification.ChildrenOfType().Single().Alpha == 0);
AddUntilStep("wait for forward to overlay", () => !notification.IsInToastTray);
@@ -424,11 +492,19 @@ namespace osu.Game.Tests.Visual.UserInterface
AddRepeatStep("send barrage", sendBarrage, 10);
}
+ [Test]
+ public void TestServerShuttingDownNotification()
+ {
+ AddStep("post with 5 seconds", () => notificationOverlay.Post(new ServerShutdownNotification(TimeSpan.FromSeconds(5))));
+ AddStep("post with 30 seconds", () => notificationOverlay.Post(new ServerShutdownNotification(TimeSpan.FromSeconds(30))));
+ AddStep("post with 6 hours", () => notificationOverlay.Post(new ServerShutdownNotification(TimeSpan.FromHours(6))));
+ }
+
protected override void Update()
{
base.Update();
- progressingNotifications.RemoveAll(n => n.State == ProgressNotificationState.Completed);
+ progressingNotifications.RemoveAll(n => n.State == ProgressNotificationState.Completed && n.WasClosed);
if (progressingNotifications.Count(n => n.State == ProgressNotificationState.Active) < 3)
{
diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
index c199d1da59..e6f1609d7f 100644
--- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
@@ -24,7 +24,6 @@ namespace osu.Game.Beatmaps.ControlPoints
public readonly BindableDouble SliderVelocityBindable = new BindableDouble(1)
{
Precision = 0.01,
- Default = 1,
MinValue = 0.1,
MaxValue = 10
};
diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
index ead07b4eaa..7c4313a015 100644
--- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
@@ -28,7 +28,6 @@ namespace osu.Game.Beatmaps.ControlPoints
public readonly BindableDouble ScrollSpeedBindable = new BindableDouble(1)
{
Precision = 0.01,
- Default = 1,
MinValue = 0.01,
MaxValue = 10
};
diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
index 78dec67937..c454439c5c 100644
--- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
@@ -45,7 +45,6 @@ namespace osu.Game.Beatmaps.ControlPoints
{
MinValue = 0,
MaxValue = 100,
- Default = 100
};
///
diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
index 23d4d10fd8..61cc060594 100644
--- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
@@ -49,7 +49,6 @@ namespace osu.Game.Beatmaps.ControlPoints
///
public readonly BindableDouble BeatLengthBindable = new BindableDouble(DEFAULT_BEAT_LENGTH)
{
- Default = DEFAULT_BEAT_LENGTH,
MinValue = 6,
MaxValue = 60000
};
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index 3d65ab8e0f..9c066ada08 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -79,7 +79,7 @@ namespace osu.Game.Beatmaps.Formats
switch (section)
{
case Section.Colours:
- HandleColours(output, line);
+ HandleColours(output, line, false);
return;
}
}
@@ -93,7 +93,7 @@ namespace osu.Game.Beatmaps.Formats
return line;
}
- protected void HandleColours(TModel output, string line)
+ protected void HandleColours(TModel output, string line, bool allowAlpha)
{
var pair = SplitKeyVal(line);
@@ -108,7 +108,7 @@ namespace osu.Game.Beatmaps.Formats
try
{
- byte alpha = split.Length == 4 ? byte.Parse(split[3]) : (byte)255;
+ byte alpha = allowAlpha && split.Length == 4 ? byte.Parse(split[3]) : (byte)255;
colour = new Color4(byte.Parse(split[0]), byte.Parse(split[1]), byte.Parse(split[2]), alpha);
}
catch
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index 5f49557685..e3bfb6b1e9 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Configuration
{
// UI/selection defaults
SetDefault(OsuSetting.Ruleset, string.Empty);
- SetDefault(OsuSetting.Skin, SkinInfo.DEFAULT_SKIN.ToString());
+ SetDefault(OsuSetting.Skin, SkinInfo.ARGON_SKIN.ToString());
SetDefault(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Details);
SetDefault(OsuSetting.BeatmapDetailModsFilter, false);
@@ -131,6 +131,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true);
SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true);
SetDefault(OsuSetting.KeyOverlay, false);
+ SetDefault(OsuSetting.GameplayLeaderboard, true);
SetDefault(OsuSetting.AlwaysPlayFirstComboBreak, true);
SetDefault(OsuSetting.FloatingComments, false);
@@ -294,6 +295,7 @@ namespace osu.Game.Configuration
LightenDuringBreaks,
ShowStoryboard,
KeyOverlay,
+ GameplayLeaderboard,
PositionalHitsounds,
PositionalHitsoundsLevel,
AlwaysPlayFirstComboBreak,
diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs
index cefc7da503..edcd020226 100644
--- a/osu.Game/Database/RealmAccess.cs
+++ b/osu.Game/Database/RealmAccess.cs
@@ -69,8 +69,9 @@ namespace osu.Game.Database
/// 22 2022-07-31 Added ModPreset.
/// 23 2022-08-01 Added LastLocalUpdate to BeatmapInfo.
/// 24 2022-08-22 Added MaximumStatistics to ScoreInfo.
+ /// 25 2022-09-18 Remove skins to add with new naming.
///
- private const int schema_version = 24;
+ private const int schema_version = 25;
///
/// Lock object which is held during sections, blocking realm retrieval during blocking periods.
@@ -870,6 +871,11 @@ namespace osu.Game.Database
}
break;
+
+ case 25:
+ // Remove the default skins so they can be added back by SkinManager with updated naming.
+ migration.NewRealm.RemoveRange(migration.NewRealm.All().Where(s => s.Protected));
+ break;
}
}
diff --git a/osu.Game/Localisation/GameplaySettingsStrings.cs b/osu.Game/Localisation/GameplaySettingsStrings.cs
index 13cfcc3a19..40f39d927d 100644
--- a/osu.Game/Localisation/GameplaySettingsStrings.cs
+++ b/osu.Game/Localisation/GameplaySettingsStrings.cs
@@ -79,6 +79,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString AlwaysShowKeyOverlay => new TranslatableString(getKey(@"key_overlay"), @"Always show key overlay");
+ ///
+ /// "Always show gameplay leaderboard"
+ ///
+ public static LocalisableString AlwaysShowGameplayLeaderboard => new TranslatableString(getKey(@"gameplay_leaderboard"), @"Always show gameplay leaderboard");
+
///
/// "Always play first combo break sound"
///
diff --git a/osu.Game/Online/API/Requests/GetBeatmapRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapRequest.cs
index efecc0fc25..3383d21dfc 100644
--- a/osu.Game/Online/API/Requests/GetBeatmapRequest.cs
+++ b/osu.Game/Online/API/Requests/GetBeatmapRequest.cs
@@ -9,27 +9,25 @@ namespace osu.Game.Online.API.Requests
{
public class GetBeatmapRequest : APIRequest
{
- private readonly IBeatmapInfo beatmapInfo;
-
- private readonly string filename;
+ public readonly IBeatmapInfo BeatmapInfo;
+ public readonly string Filename;
public GetBeatmapRequest(IBeatmapInfo beatmapInfo)
{
- this.beatmapInfo = beatmapInfo;
-
- filename = (beatmapInfo as BeatmapInfo)?.Path ?? string.Empty;
+ BeatmapInfo = beatmapInfo;
+ Filename = (beatmapInfo as BeatmapInfo)?.Path ?? string.Empty;
}
protected override WebRequest CreateWebRequest()
{
var request = base.CreateWebRequest();
- if (beatmapInfo.OnlineID > 0)
- request.AddParameter(@"id", beatmapInfo.OnlineID.ToString());
- if (!string.IsNullOrEmpty(beatmapInfo.MD5Hash))
- request.AddParameter(@"checksum", beatmapInfo.MD5Hash);
- if (!string.IsNullOrEmpty(filename))
- request.AddParameter(@"filename", filename);
+ if (BeatmapInfo.OnlineID > 0)
+ request.AddParameter(@"id", BeatmapInfo.OnlineID.ToString());
+ if (!string.IsNullOrEmpty(BeatmapInfo.MD5Hash))
+ request.AddParameter(@"checksum", BeatmapInfo.MD5Hash);
+ if (!string.IsNullOrEmpty(Filename))
+ request.AddParameter(@"filename", Filename);
return request;
}
diff --git a/osu.Game/Online/API/Requests/Responses/APIUser.cs b/osu.Game/Online/API/Requests/Responses/APIUser.cs
index 5f843e9a7b..d3ddcffaf5 100644
--- a/osu.Game/Online/API/Requests/Responses/APIUser.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIUser.cs
@@ -228,7 +228,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"rank_history")]
private APIRankHistory rankHistory
{
- set => statistics.RankHistory = value;
+ set => Statistics.RankHistory = value;
}
[JsonProperty("badges")]
diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs
index 58b1ea62aa..69b4e5b209 100644
--- a/osu.Game/Online/Leaderboards/Leaderboard.cs
+++ b/osu.Game/Online/Leaderboards/Leaderboard.cs
@@ -1,14 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
-using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Development;
@@ -39,7 +36,9 @@ namespace osu.Game.Online.Leaderboards
///
/// The currently displayed scores.
///
- public IEnumerable Scores => scores;
+ public IBindableList Scores => scores;
+
+ private readonly BindableList scores = new BindableList();
///
/// Whether the current scope should refetch in response to changes in API connectivity state.
@@ -52,25 +51,23 @@ namespace osu.Game.Online.Leaderboards
private readonly Container placeholderContainer;
private readonly UserTopScoreContainer userScoreContainer;
- private FillFlowContainer scoreFlowContainer;
+ private FillFlowContainer? scoreFlowContainer;
private readonly LoadingSpinner loading;
- private CancellationTokenSource currentFetchCancellationSource;
- private CancellationTokenSource currentScoresAsyncLoadCancellationSource;
+ private CancellationTokenSource? currentFetchCancellationSource;
+ private CancellationTokenSource? currentScoresAsyncLoadCancellationSource;
- private APIRequest fetchScoresRequest;
+ private APIRequest? fetchScoresRequest;
private LeaderboardState state;
[Resolved(CanBeNull = true)]
- private IAPIProvider api { get; set; }
+ private IAPIProvider? api { get; set; }
private readonly IBindable apiState = new Bindable();
- private ICollection scores;
-
- private TScope scope;
+ private TScope scope = default!;
public TScope Scope
{
@@ -169,7 +166,7 @@ namespace osu.Game.Online.Leaderboards
throw new InvalidOperationException($"State {state} cannot be set by a leaderboard implementation.");
}
- Debug.Assert(scores?.Any() != true);
+ Debug.Assert(!scores.Any());
setState(state);
}
@@ -179,17 +176,33 @@ namespace osu.Game.Online.Leaderboards
///
/// The scores to display.
/// The user top score, if any.
- protected void SetScores(IEnumerable scores, TScoreInfo userScore = default)
+ protected void SetScores(IEnumerable? scores, TScoreInfo? userScore = default)
{
- this.scores = scores?.ToList();
- userScoreContainer.Score.Value = userScore;
+ this.scores.Clear();
+ if (scores != null)
+ this.scores.AddRange(scores);
- if (userScore == null)
- userScoreContainer.Hide();
- else
- userScoreContainer.Show();
+ // Non-delayed schedule may potentially run inline (due to IsMainThread check passing) after leaderboard is disposed.
+ // This is guarded against in BeatmapLeaderboard via web request cancellation, but let's be extra safe.
+ if (!IsDisposed)
+ {
+ // Schedule needs to be non-delayed here for the weird logic in refetchScores to work.
+ // If it is removed, the placeholder will be incorrectly updated to "no scores" rather than "retrieving".
+ // This whole flow should be refactored in the future.
+ Scheduler.Add(applyNewScores, false);
+ }
- Scheduler.Add(updateScoresDrawables, false);
+ void applyNewScores()
+ {
+ userScoreContainer.Score.Value = userScore;
+
+ if (userScore == null)
+ userScoreContainer.Hide();
+ else
+ userScoreContainer.Show();
+
+ updateScoresDrawables();
+ }
}
///
@@ -197,8 +210,7 @@ namespace osu.Game.Online.Leaderboards
///
///
/// An responsible for the fetch operation. This will be queued and performed automatically.
- [CanBeNull]
- protected abstract APIRequest FetchScores(CancellationToken cancellationToken);
+ protected abstract APIRequest? FetchScores(CancellationToken cancellationToken);
protected abstract LeaderboardScore CreateDrawableScore(TScoreInfo model, int index);
@@ -209,8 +221,8 @@ namespace osu.Game.Online.Leaderboards
Debug.Assert(ThreadSafety.IsUpdateThread);
cancelPendingWork();
- SetScores(null);
+ SetScores(null);
setState(LeaderboardState.Retrieving);
currentFetchCancellationSource = new CancellationTokenSource();
@@ -247,7 +259,7 @@ namespace osu.Game.Online.Leaderboards
.Expire();
scoreFlowContainer = null;
- if (scores?.Any() != true)
+ if (!scores.Any())
{
setState(LeaderboardState.NoScores);
return;
@@ -282,7 +294,7 @@ namespace osu.Game.Online.Leaderboards
#region Placeholder handling
- private Placeholder placeholder;
+ private Placeholder? placeholder;
private void setState(LeaderboardState state)
{
@@ -309,7 +321,7 @@ namespace osu.Game.Online.Leaderboards
placeholder.FadeInFromZero(fade_duration, Easing.OutQuint);
}
- private Placeholder getPlaceholderFor(LeaderboardState state)
+ private Placeholder? getPlaceholderFor(LeaderboardState state)
{
switch (state)
{
diff --git a/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs b/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs
index 2d2d82821c..391e8804f0 100644
--- a/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs
+++ b/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Threading;
using osu.Framework.Bindables;
@@ -18,13 +16,15 @@ namespace osu.Game.Online.Leaderboards
{
private const int duration = 500;
- public Bindable Score = new Bindable();
+ public Bindable Score = new Bindable();
private readonly Container scoreContainer;
private readonly Func createScoreDelegate;
protected override bool StartHidden => true;
+ private CancellationTokenSource? loadScoreCancellation;
+
public UserTopScoreContainer(Func createScoreDelegate)
{
this.createScoreDelegate = createScoreDelegate;
@@ -65,9 +65,7 @@ namespace osu.Game.Online.Leaderboards
Score.BindValueChanged(onScoreChanged);
}
- private CancellationTokenSource loadScoreCancellation;
-
- private void onScoreChanged(ValueChangedEvent score)
+ private void onScoreChanged(ValueChangedEvent score)
{
var newScore = score.NewValue;
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index c398d72118..75334952f0 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -18,6 +18,7 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
+using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
@@ -26,6 +27,8 @@ namespace osu.Game.Online.Multiplayer
{
public abstract class MultiplayerClient : Component, IMultiplayerClient, IMultiplayerRoomServer
{
+ public Action? PostNotification { protected get; set; }
+
///
/// Invoked when any change occurs to the multiplayer room.
///
@@ -207,6 +210,8 @@ namespace osu.Game.Online.Multiplayer
updateLocalRoomSettings(joinedRoom.Settings);
+ postServerShuttingDownNotification();
+
OnRoomJoined();
}, cancellationSource.Token).ConfigureAwait(false);
}, cancellationSource.Token).ConfigureAwait(false);
@@ -554,6 +559,14 @@ namespace osu.Game.Online.Multiplayer
{
case CountdownStartedEvent countdownStartedEvent:
Room.ActiveCountdowns.Add(countdownStartedEvent.Countdown);
+
+ switch (countdownStartedEvent.Countdown)
+ {
+ case ServerShuttingDownCountdown:
+ postServerShuttingDownNotification();
+ break;
+ }
+
break;
case CountdownStoppedEvent countdownStoppedEvent:
@@ -569,6 +582,16 @@ namespace osu.Game.Online.Multiplayer
return Task.CompletedTask;
}
+ private void postServerShuttingDownNotification()
+ {
+ ServerShuttingDownCountdown? countdown = room?.ActiveCountdowns.OfType().FirstOrDefault();
+
+ if (countdown == null)
+ return;
+
+ PostNotification?.Invoke(new ServerShutdownNotification(countdown.TimeRemaining));
+ }
+
Task IMultiplayerClient.UserBeatmapAvailabilityChanged(int userId, BeatmapAvailability beatmapAvailability)
{
Scheduler.Add(() =>
diff --git a/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs b/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs
index fd22420b99..c59f5937b0 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerCountdown.cs
@@ -13,6 +13,7 @@ namespace osu.Game.Online.Multiplayer
[MessagePackObject]
[Union(0, typeof(MatchStartCountdown))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
[Union(1, typeof(ForceGameplayStartCountdown))]
+ [Union(2, typeof(ServerShuttingDownCountdown))]
public abstract class MultiplayerCountdown
{
///
diff --git a/osu.Game/Online/Multiplayer/ServerShutdownNotification.cs b/osu.Game/Online/Multiplayer/ServerShutdownNotification.cs
new file mode 100644
index 0000000000..c114741be8
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/ServerShutdownNotification.cs
@@ -0,0 +1,62 @@
+// 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 Humanizer.Localisation;
+using osu.Framework.Allocation;
+using osu.Framework.Threading;
+using osu.Game.Overlays.Notifications;
+using osu.Game.Utils;
+
+namespace osu.Game.Online.Multiplayer
+{
+ public class ServerShutdownNotification : SimpleNotification
+ {
+ private readonly DateTimeOffset endDate;
+ private ScheduledDelegate? updateDelegate;
+
+ public ServerShutdownNotification(TimeSpan duration)
+ {
+ endDate = DateTimeOffset.UtcNow + duration;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ updateTime();
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ updateDelegate = Scheduler.Add(updateTimeWithReschedule);
+ }
+
+ private void updateTimeWithReschedule()
+ {
+ updateTime();
+
+ // The remaining time on a countdown may be at a fractional portion between two seconds.
+ // We want to align certain audio/visual cues to the point at which integer seconds change.
+ // To do so, we schedule to the next whole second. Note that scheduler invocation isn't
+ // guaranteed to be accurate, so this may still occur slightly late, but even in such a case
+ // the next invocation will be roughly correct.
+ double timeToNextSecond = endDate.Subtract(DateTimeOffset.UtcNow).TotalMilliseconds % 1000;
+
+ updateDelegate = Scheduler.AddDelayed(updateTimeWithReschedule, timeToNextSecond);
+ }
+
+ private void updateTime()
+ {
+ TimeSpan remaining = endDate.Subtract(DateTimeOffset.Now);
+
+ if (remaining.TotalSeconds <= 5)
+ {
+ updateDelegate?.Cancel();
+ Text = "The multiplayer server will be right back...";
+ }
+ else
+ Text = $"The multiplayer server is restarting in {HumanizerUtils.Humanize(remaining, precision: 3, minUnit: TimeUnit.Second)}.";
+ }
+ }
+}
diff --git a/osu.Game/Online/Multiplayer/ServerShuttingDownCountdown.cs b/osu.Game/Online/Multiplayer/ServerShuttingDownCountdown.cs
new file mode 100644
index 0000000000..b0a45dc768
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/ServerShuttingDownCountdown.cs
@@ -0,0 +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 MessagePack;
+
+namespace osu.Game.Online.Multiplayer
+{
+ ///
+ /// A countdown that indicates the current multiplayer server is shutting down.
+ ///
+ [MessagePackObject]
+ public class ServerShuttingDownCountdown : MultiplayerCountdown
+ {
+ }
+}
diff --git a/osu.Game/Online/SignalRWorkaroundTypes.cs b/osu.Game/Online/SignalRWorkaroundTypes.cs
index 3518fbb4fe..0b545821ee 100644
--- a/osu.Game/Online/SignalRWorkaroundTypes.cs
+++ b/osu.Game/Online/SignalRWorkaroundTypes.cs
@@ -28,7 +28,8 @@ namespace osu.Game.Online
(typeof(TeamVersusRoomState), typeof(MatchRoomState)),
(typeof(TeamVersusUserState), typeof(MatchUserState)),
(typeof(MatchStartCountdown), typeof(MultiplayerCountdown)),
- (typeof(ForceGameplayStartCountdown), typeof(MultiplayerCountdown))
+ (typeof(ForceGameplayStartCountdown), typeof(MultiplayerCountdown)),
+ (typeof(ServerShuttingDownCountdown), typeof(MultiplayerCountdown)),
};
}
}
diff --git a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs b/osu.Game/Online/Spectator/OnlineSpectatorClient.cs
index a012bf49b6..48d5c0bea9 100644
--- a/osu.Game/Online/Spectator/OnlineSpectatorClient.cs
+++ b/osu.Game/Online/Spectator/OnlineSpectatorClient.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 System;
using System.Diagnostics;
using System.Threading.Tasks;
-using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Client;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -58,7 +58,7 @@ namespace osu.Game.Online.Spectator
{
await connection.InvokeAsync(nameof(ISpectatorServer.BeginPlaySession), state);
}
- catch (HubException exception)
+ catch (Exception exception)
{
if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE)
{
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 0d115f62fe..939d3a63ed 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -24,6 +24,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
+using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Framework.Screens;
@@ -187,7 +188,8 @@ namespace osu.Game
{
this.args = args;
- forwardLoggedErrorsToNotifications();
+ forwardGeneralLogsToNotifications();
+ forwardTabletLogsToNotifications();
SentryLogger = new SentryLogger(this);
}
@@ -559,9 +561,11 @@ namespace osu.Game
return;
}
+ // This should be able to be performed from song select, but that is disabled for now
+ // due to the weird decoupled ruleset logic (which can cause a crash in certain filter scenarios).
PerformFromScreen(screen =>
{
- Logger.Log($"{nameof(PresentScore)} updating beatmap ({databasedBeatmap}) and ruleset ({databasedScore.ScoreInfo.Ruleset} to match score");
+ Logger.Log($"{nameof(PresentScore)} updating beatmap ({databasedBeatmap}) and ruleset ({databasedScore.ScoreInfo.Ruleset}) to match score");
Ruleset.Value = databasedScore.ScoreInfo.Ruleset;
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap);
@@ -576,7 +580,7 @@ namespace osu.Game
screen.Push(new SoloResultsScreen(databasedScore.ScoreInfo, false));
break;
}
- }, validScreens: new[] { typeof(PlaySongSelect) });
+ });
}
public override Task Import(params ImportTask[] imports)
@@ -716,6 +720,8 @@ namespace osu.Game
ScoreManager.PostNotification = n => Notifications.Post(n);
ScoreManager.PresentImport = items => PresentScore(items.First().Value);
+ MultiplayerClient.PostNotification = n => Notifications.Post(n);
+
// make config aware of how to lookup skins for on-screen display purposes.
// if this becomes a more common thing, tracked settings should be reconsidered to allow local DI.
LocalConfig.LookupSkinName = id => SkinManager.Query(s => s.ID == id)?.ToString() ?? "Unknown";
@@ -992,7 +998,7 @@ namespace osu.Game
overlay.Depth = (float)-Clock.CurrentTime;
}
- private void forwardLoggedErrorsToNotifications()
+ private void forwardGeneralLogsToNotifications()
{
int recentLogCount = 0;
@@ -1000,7 +1006,7 @@ namespace osu.Game
Logger.NewEntry += entry =>
{
- if (entry.Level < LogLevel.Important || entry.Target == null) return;
+ if (entry.Level < LogLevel.Important || entry.Target > LoggingTarget.Database) return;
const int short_term_display_limit = 3;
@@ -1033,6 +1039,52 @@ namespace osu.Game
};
}
+ private void forwardTabletLogsToNotifications()
+ {
+ const string tablet_prefix = @"[Tablet] ";
+ bool notifyOnWarning = true;
+
+ Logger.NewEntry += entry =>
+ {
+ if (entry.Level < LogLevel.Important || entry.Target != LoggingTarget.Input || !entry.Message.StartsWith(tablet_prefix, StringComparison.OrdinalIgnoreCase))
+ return;
+
+ string message = entry.Message.Replace(tablet_prefix, string.Empty);
+
+ if (entry.Level == LogLevel.Error)
+ {
+ Schedule(() => Notifications.Post(new SimpleNotification
+ {
+ Text = $"Encountered tablet error: \"{message}\"",
+ Icon = FontAwesome.Solid.PenSquare,
+ IconColour = Colours.RedDark,
+ }));
+ }
+ else if (notifyOnWarning)
+ {
+ Schedule(() => Notifications.Post(new SimpleNotification
+ {
+ Text = @"Encountered tablet warning, your tablet may not function correctly. Click here for a list of all tablets supported.",
+ Icon = FontAwesome.Solid.PenSquare,
+ IconColour = Colours.YellowDark,
+ Activated = () =>
+ {
+ OpenUrlExternally("https://opentabletdriver.net/Tablets", true);
+ return true;
+ }
+ }));
+
+ notifyOnWarning = false;
+ }
+ };
+
+ Schedule(() =>
+ {
+ ITabletHandler tablet = Host.AvailableInputHandlers.OfType().SingleOrDefault();
+ tablet?.Tablet.BindValueChanged(_ => notifyOnWarning = true, true);
+ });
+ }
+
private Task asyncLoadStream;
///
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index b30a065371..478f154d58 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
@@ -124,6 +125,8 @@ namespace osu.Game
protected SessionStatics SessionStatics { get; private set; }
+ protected OsuColour Colours { get; private set; }
+
protected BeatmapManager BeatmapManager { get; private set; }
protected BeatmapModelDownloader BeatmapDownloader { get; private set; }
@@ -179,7 +182,7 @@ namespace osu.Game
private SpectatorClient spectatorClient;
- private MultiplayerClient multiplayerClient;
+ protected MultiplayerClient MultiplayerClient { get; private set; }
private MetadataClient metadataClient;
@@ -255,6 +258,8 @@ namespace osu.Game
InitialiseFonts();
+ addFilesWarning();
+
Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY;
dependencies.Cache(SkinManager = new SkinManager(Storage, realm, Host, Resources, Audio, Scheduler));
@@ -284,7 +289,7 @@ namespace osu.Game
// TODO: OsuGame or OsuGameBase?
dependencies.CacheAs(beatmapUpdater = new BeatmapUpdater(BeatmapManager, difficultyCache, API, Storage));
dependencies.CacheAs(spectatorClient = new OnlineSpectatorClient(endpoints));
- dependencies.CacheAs(multiplayerClient = new OnlineMultiplayerClient(endpoints));
+ dependencies.CacheAs(MultiplayerClient = new OnlineMultiplayerClient(endpoints));
dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints));
AddInternal(new BeatmapOnlineChangeIngest(beatmapUpdater, realm, metadataClient));
@@ -308,7 +313,7 @@ namespace osu.Game
dependencies.CacheAs(powerStatus);
dependencies.Cache(SessionStatics = new SessionStatics());
- dependencies.Cache(new OsuColour());
+ dependencies.Cache(Colours = new OsuColour());
RegisterImportHandler(BeatmapManager);
RegisterImportHandler(ScoreManager);
@@ -329,7 +334,7 @@ namespace osu.Game
AddInternal(apiAccess);
AddInternal(spectatorClient);
- AddInternal(multiplayerClient);
+ AddInternal(MultiplayerClient);
AddInternal(metadataClient);
AddInternal(rulesetConfigCache);
@@ -373,6 +378,29 @@ namespace osu.Game
Beatmap.BindValueChanged(onBeatmapChanged);
}
+ private void addFilesWarning()
+ {
+ var realmStore = new RealmFileStore(realm, Storage);
+
+ const string filename = "IMPORTANT READ ME.txt";
+
+ if (!realmStore.Storage.Exists(filename))
+ {
+ using (var stream = realmStore.Storage.CreateFileSafely(filename))
+ using (var textWriter = new StreamWriter(stream))
+ {
+ textWriter.WriteLine(@"This folder contains all your user files (beatmaps, skins, replays etc.)");
+ textWriter.WriteLine(@"Please do not touch or delete this folder!!");
+ textWriter.WriteLine();
+ textWriter.WriteLine(@"If you are really looking to completely delete user data, please delete");
+ textWriter.WriteLine(@"the parent folder including all other files and directories");
+ textWriter.WriteLine();
+ textWriter.WriteLine(@"For more information on how these files are organised,");
+ textWriter.WriteLine(@"see https://github.com/ppy/osu/wiki/User-file-storage");
+ }
+ }
+ }
+
private void onTrackChanged(WorkingBeatmap beatmap, TrackChangeDirection direction)
{
// FramedBeatmapClock uses a decoupled clock internally which will mutate the source if it is an `IAdjustableClock`.
diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs
index 82f49e0aef..4963de7251 100644
--- a/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs
+++ b/osu.Game/Overlays/FirstRunSetup/ScreenBeatmaps.cs
@@ -123,7 +123,7 @@ namespace osu.Game.Overlays.FirstRunSetup
beatmapSubscription?.Dispose();
}
- private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes, Exception error)
+ private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes, Exception error) => Schedule(() =>
{
currentlyLoadedBeatmaps.Text = FirstRunSetupBeatmapScreenStrings.CurrentlyLoadedBeatmaps(sender.Count);
@@ -139,7 +139,7 @@ namespace osu.Game.Overlays.FirstRunSetup
currentlyLoadedBeatmaps.ScaleTo(1.1f)
.ScaleTo(1, 1500, Easing.OutQuint);
}
- }
+ });
private void downloadTutorial()
{
diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs
index 9fe2fd5279..63f1aa248c 100644
--- a/osu.Game/Overlays/Music/PlaylistOverlay.cs
+++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs
@@ -8,6 +8,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
@@ -24,7 +25,7 @@ namespace osu.Game.Overlays.Music
public class PlaylistOverlay : VisibilityContainer
{
private const float transition_duration = 600;
- private const float playlist_height = 510;
+ public const float PLAYLIST_HEIGHT = 510;
private readonly BindableList> beatmapSets = new BindableList>();
@@ -130,7 +131,7 @@ namespace osu.Game.Overlays.Music
filter.Search.HoldFocus = true;
Schedule(() => filter.Search.TakeFocus());
- this.ResizeTo(new Vector2(1, playlist_height), transition_duration, Easing.OutQuint);
+ this.ResizeTo(new Vector2(1, RelativeSizeAxes.HasFlagFast(Axes.Y) ? 1f : PLAYLIST_HEIGHT), transition_duration, Easing.OutQuint);
this.FadeIn(transition_duration, Easing.OutQuint);
}
diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs
index da87336039..793b7e294f 100644
--- a/osu.Game/Overlays/MusicController.cs
+++ b/osu.Game/Overlays/MusicController.cs
@@ -58,12 +58,11 @@ namespace osu.Game.Overlays
[Resolved]
private RealmAccess realm { get; set; }
- [BackgroundDependencyLoader]
- private void load()
+ protected override void LoadComplete()
{
- // Todo: These binds really shouldn't be here, but are unlikely to cause any issues for now.
- // They are placed here for now since some tests rely on setting the beatmap _and_ their hierarchies inside their load(), which runs before the MusicController's load().
- beatmap.BindValueChanged(beatmapChanged, true);
+ base.LoadComplete();
+
+ beatmap.BindValueChanged(b => changeBeatmap(b.NewValue), true);
mods.BindValueChanged(_ => ResetTrackAdjustments(), true);
}
@@ -263,8 +262,6 @@ namespace osu.Game.Overlays
private IQueryable getBeatmapSets() => realm.Realm.All().Where(s => !s.DeletePending);
- private void beatmapChanged(ValueChangedEvent beatmap) => changeBeatmap(beatmap.NewValue);
-
private void changeBeatmap(WorkingBeatmap newWorking)
{
// This method can potentially be triggered multiple times as it is eagerly fired in next() / prev() to ensure correct execution order
diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs
index fad8afd371..36548c893c 100644
--- a/osu.Game/Overlays/NotificationOverlay.cs
+++ b/osu.Game/Overlays/NotificationOverlay.cs
@@ -113,9 +113,12 @@ namespace osu.Game.Overlays
if (enabled)
// we want a slight delay before toggling notifications on to avoid the user becoming overwhelmed.
- notificationsEnabler = Scheduler.AddDelayed(() => processingPosts = true, State.Value == Visibility.Visible ? 0 : 100);
+ notificationsEnabler = Scheduler.AddDelayed(() => processingPosts = true, State.Value == Visibility.Visible ? 0 : 250);
else
+ {
processingPosts = false;
+ toastTray.FlushAllToasts();
+ }
}
protected override void LoadComplete()
diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs
index 4e7cebf0ae..8be9d2072b 100644
--- a/osu.Game/Overlays/Notifications/Notification.cs
+++ b/osu.Game/Overlays/Notifications/Notification.cs
@@ -68,6 +68,8 @@ namespace osu.Game.Overlays.Notifications
public virtual bool Read { get; set; }
+ protected virtual bool AllowFlingDismiss => true;
+
public new bool IsDragged => dragContainer.IsDragged;
protected virtual IconUsage CloseButtonIcon => FontAwesome.Solid.Check;
@@ -229,8 +231,8 @@ namespace osu.Game.Overlays.Notifications
protected override bool OnClick(ClickEvent e)
{
// Clicking with anything but left button should dismiss but not perform the activation action.
- if (e.Button == MouseButton.Left)
- Activated?.Invoke();
+ if (e.Button == MouseButton.Left && Activated?.Invoke() == false)
+ return true;
Close(false);
return true;
@@ -315,7 +317,7 @@ namespace osu.Game.Overlays.Notifications
protected override void OnDragEnd(DragEndEvent e)
{
- if (Rotation < -10 || velocity.X < -0.3f)
+ if (notification.AllowFlingDismiss && (Rotation < -10 || velocity.X < -0.3f))
notification.Close(true);
else if (X > 30 || velocity.X > 0.3f)
notification.ForwardToOverlay?.Invoke();
diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs
index 61bb22041e..4cf47013bd 100644
--- a/osu.Game/Overlays/Notifications/ProgressNotification.cs
+++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs
@@ -25,6 +25,8 @@ namespace osu.Game.Overlays.Notifications
public Func? CancelRequested { get; set; }
+ protected override bool AllowFlingDismiss => false;
+
///
/// The function to post completion notifications back to.
///
diff --git a/osu.Game/Overlays/Notifications/SimpleNotification.cs b/osu.Game/Overlays/Notifications/SimpleNotification.cs
index 1dba60fb5f..f3bb6a0578 100644
--- a/osu.Game/Overlays/Notifications/SimpleNotification.cs
+++ b/osu.Game/Overlays/Notifications/SimpleNotification.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
@@ -41,6 +42,12 @@ namespace osu.Game.Overlays.Notifications
}
}
+ public ColourInfo IconColour
+ {
+ get => IconContent.Colour;
+ set => IconContent.Colour = value;
+ }
+
private TextFlowContainer? textDrawable;
private SpriteIcon? iconDrawable;
diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs
index 6eddc7da83..900b4bebf0 100644
--- a/osu.Game/Overlays/NowPlayingOverlay.cs
+++ b/osu.Game/Overlays/NowPlayingOverlay.cs
@@ -38,6 +38,7 @@ namespace osu.Game.Overlays
private const float transition_length = 800;
private const float progress_height = 10;
private const float bottom_black_area_height = 55;
+ private const float margin = 10;
private Drawable background;
private ProgressBar progressBar;
@@ -53,6 +54,7 @@ namespace osu.Game.Overlays
private Container dragContainer;
private Container playerContainer;
+ private Container playlistContainer;
protected override string PopInSampleName => "UI/now-playing-pop-in";
protected override string PopOutSampleName => "UI/now-playing-pop-out";
@@ -69,7 +71,7 @@ namespace osu.Game.Overlays
public NowPlayingOverlay()
{
Width = 400;
- Margin = new MarginPadding(10);
+ Margin = new MarginPadding(margin);
}
[BackgroundDependencyLoader]
@@ -82,7 +84,6 @@ namespace osu.Game.Overlays
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
playerContainer = new Container
@@ -182,8 +183,13 @@ namespace osu.Game.Overlays
}
},
},
+ playlistContainer = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ Y = player_height + margin,
+ }
}
- }
+ },
};
}
@@ -193,11 +199,10 @@ namespace osu.Game.Overlays
{
LoadComponentAsync(playlist = new PlaylistOverlay
{
- RelativeSizeAxes = Axes.X,
- Y = player_height + 10,
+ RelativeSizeAxes = Axes.Both,
}, _ =>
{
- dragContainer.Add(playlist);
+ playlistContainer.Add(playlist);
playlist.State.BindValueChanged(s => playlistButton.FadeColour(s.NewValue == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint), true);
@@ -242,7 +247,18 @@ namespace osu.Game.Overlays
{
base.UpdateAfterChildren();
- Height = dragContainer.Height;
+ playlistContainer.Height = MathF.Min(Parent.DrawHeight - margin * 3 - player_height, PlaylistOverlay.PLAYLIST_HEIGHT);
+
+ float height = player_height;
+
+ if (playlist != null)
+ {
+ height += playlist.DrawHeight;
+ if (playlist.State.Value == Visibility.Visible)
+ height += margin;
+ }
+
+ Height = dragContainer.Height = height;
}
protected override void Update()
diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs
index 0893af7d3e..88a27840d8 100644
--- a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs
@@ -38,6 +38,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
Current = config.GetBindable(OsuSetting.KeyOverlay),
Keywords = new[] { "counter" },
},
+ new SettingsCheckbox
+ {
+ LabelText = GameplaySettingsStrings.AlwaysShowGameplayLeaderboard,
+ Current = config.GetBindable(OsuSetting.GameplayLeaderboard),
+ },
};
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
index 28642f12a1..c64a3101b7 100644
--- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
@@ -73,8 +73,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
windowModes.BindTo(host.Window.SupportedWindowModes);
}
- if (host.Window is WindowsWindow windowsWindow)
- fullscreenCapability.BindTo(windowsWindow.FullscreenCapability);
+ if (host.Renderer is IWindowsRenderer windowsRenderer)
+ fullscreenCapability.BindTo(windowsRenderer.FullscreenCapability);
Children = new Drawable[]
{
diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index e32639f476..f1e216f538 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -214,21 +214,21 @@ namespace osu.Game.Overlays.Settings.Sections.Input
rotation.BindTo(tabletHandler.Rotation);
areaOffset.BindTo(tabletHandler.AreaOffset);
- areaOffset.BindValueChanged(val =>
+ areaOffset.BindValueChanged(val => Schedule(() =>
{
offsetX.Value = val.NewValue.X;
offsetY.Value = val.NewValue.Y;
- }, true);
+ }), true);
offsetX.BindValueChanged(val => areaOffset.Value = new Vector2(val.NewValue, areaOffset.Value.Y));
offsetY.BindValueChanged(val => areaOffset.Value = new Vector2(areaOffset.Value.X, val.NewValue));
areaSize.BindTo(tabletHandler.AreaSize);
- areaSize.BindValueChanged(val =>
+ areaSize.BindValueChanged(val => Schedule(() =>
{
sizeX.Value = val.NewValue.X;
sizeY.Value = val.NewValue.Y;
- }, true);
+ }), true);
sizeX.BindValueChanged(val =>
{
@@ -254,7 +254,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
});
tablet.BindTo(tabletHandler.Tablet);
- tablet.BindValueChanged(val =>
+ tablet.BindValueChanged(val => Schedule(() =>
{
Scheduler.AddOnce(updateVisibility);
@@ -273,7 +273,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
sizeY.Default = sizeY.MaxValue = tab.Size.Y;
areaSize.Default = new Vector2(sizeX.Default, sizeY.Default);
- }, true);
+ }), true);
}
private void updateVisibility()
diff --git a/osu.Game/Overlays/Settings/Sections/InputSection.cs b/osu.Game/Overlays/Settings/Sections/InputSection.cs
index 4d75537f6b..a8fe3d04be 100644
--- a/osu.Game/Overlays/Settings/Sections/InputSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/InputSection.cs
@@ -3,6 +3,8 @@
#nullable disable
+using System.Collections.Generic;
+using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
@@ -20,6 +22,8 @@ namespace osu.Game.Overlays.Settings.Sections
public override LocalisableString Header => InputSettingsStrings.InputSectionHeader;
+ public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "keybindings" });
+
public override Drawable CreateIcon() => new SpriteIcon
{
Icon = FontAwesome.Solid.Keyboard
diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
index 4787b07af8..f602b73065 100644
--- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
@@ -78,8 +78,7 @@ namespace osu.Game.Overlays.Settings.Sections
realmSubscription = realm.RegisterForNotifications(_ => realm.Realm.All()
.Where(s => !s.DeletePending)
- .OrderByDescending(s => s.Protected) // protected skins should be at the top.
- .ThenBy(s => s.Name, StringComparer.OrdinalIgnoreCase), skinsChanged);
+ .OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase), skinsChanged);
skinDropdown.Current.BindValueChanged(skin =>
{
@@ -101,14 +100,18 @@ namespace osu.Game.Overlays.Settings.Sections
if (!sender.Any())
return;
- int protectedCount = sender.Count(s => s.Protected);
-
// For simplicity repopulate the full list.
// In the future we should change this to properly handle ChangeSet events.
dropdownItems.Clear();
- foreach (var skin in sender)
+
+ dropdownItems.Add(sender.Single(s => s.ID == SkinInfo.ARGON_SKIN).ToLive(realm));
+ dropdownItems.Add(sender.Single(s => s.ID == SkinInfo.TRIANGLES_SKIN).ToLive(realm));
+ dropdownItems.Add(sender.Single(s => s.ID == SkinInfo.CLASSIC_SKIN).ToLive(realm));
+
+ dropdownItems.Add(random_skin_info);
+
+ foreach (var skin in sender.Where(s => !s.Protected))
dropdownItems.Add(skin.ToLive(realm));
- dropdownItems.Insert(protectedCount, random_skin_info);
Schedule(() => skinDropdown.Items = dropdownItems);
}
diff --git a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs
index 5a03d66b84..4ff4f66665 100644
--- a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs
+++ b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs
@@ -58,6 +58,9 @@ namespace osu.Game.Rulesets.Configuration
pendingWrites.Clear();
}
+ if (!changed.Any())
+ return true;
+
realm?.Write(r =>
{
foreach (var c in changed)
diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs
index 697b303689..e7127abcf0 100644
--- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs
+++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs
@@ -36,32 +36,24 @@ namespace osu.Game.Rulesets.Mods
public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp), typeof(ModAutoplay) };
[SettingSource("Initial rate", "The starting speed of the track")]
- public BindableNumber InitialRate { get; } = new BindableDouble
+ public BindableNumber InitialRate { get; } = new BindableDouble(1)
{
MinValue = 0.5,
MaxValue = 2,
- Default = 1,
- Value = 1,
Precision = 0.01
};
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
- public BindableBool AdjustPitch { get; } = new BindableBool
- {
- Default = true,
- Value = true
- };
+ public BindableBool AdjustPitch { get; } = new BindableBool(true);
///
/// The instantaneous rate of the track.
/// Every frame this mod will attempt to smoothly adjust this to meet .
///
- public BindableNumber SpeedChange { get; } = new BindableDouble
+ public BindableNumber SpeedChange { get; } = new BindableDouble(1)
{
MinValue = min_allowable_rate,
MaxValue = max_allowable_rate,
- Default = 1,
- Value = 1
};
// The two constants below denote the maximum allowable range of rates that `SpeedChange` can take.
diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs
index d8a41ae658..9e4469bf25 100644
--- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs
+++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs
@@ -18,12 +18,10 @@ namespace osu.Game.Rulesets.Mods
public override LocalisableString Description => "Zoooooooooom...";
[SettingSource("Speed increase", "The actual increase to apply")]
- public override BindableNumber SpeedChange { get; } = new BindableDouble
+ public override BindableNumber SpeedChange { get; } = new BindableDouble(1.5)
{
MinValue = 1.01,
MaxValue = 2,
- Default = 1.5,
- Value = 1.5,
Precision = 0.01,
};
}
diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs
index 8d8b97e79e..7d858dca6f 100644
--- a/osu.Game/Rulesets/Mods/ModHalfTime.cs
+++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs
@@ -18,12 +18,10 @@ namespace osu.Game.Rulesets.Mods
public override LocalisableString Description => "Less zoom...";
[SettingSource("Speed decrease", "The actual decrease to apply")]
- public override BindableNumber SpeedChange { get; } = new BindableDouble
+ public override BindableNumber SpeedChange { get; } = new BindableDouble(0.75)
{
MinValue = 0.5,
MaxValue = 0.99,
- Default = 0.75,
- Value = 0.75,
Precision = 0.01,
};
}
diff --git a/osu.Game/Rulesets/Mods/ModMuted.cs b/osu.Game/Rulesets/Mods/ModMuted.cs
index 9735d6b536..05ecd37000 100644
--- a/osu.Game/Rulesets/Mods/ModMuted.cs
+++ b/osu.Game/Rulesets/Mods/ModMuted.cs
@@ -36,34 +36,20 @@ namespace osu.Game.Rulesets.Mods
private readonly BindableNumber currentCombo = new BindableInt();
[SettingSource("Enable metronome", "Add a metronome beat to help you keep track of the rhythm.")]
- public BindableBool EnableMetronome { get; } = new BindableBool
- {
- Default = true,
- Value = true
- };
+ public BindableBool EnableMetronome { get; } = new BindableBool(true);
[SettingSource("Final volume at combo", "The combo count at which point the track reaches its final volume.", SettingControlType = typeof(SettingsSlider))]
- public BindableInt MuteComboCount { get; } = new BindableInt
+ public BindableInt MuteComboCount { get; } = new BindableInt(100)
{
- Default = 100,
- Value = 100,
MinValue = 0,
MaxValue = 500,
};
[SettingSource("Start muted", "Increase volume as combo builds.")]
- public BindableBool InverseMuting { get; } = new BindableBool
- {
- Default = false,
- Value = false
- };
+ public BindableBool InverseMuting { get; } = new BindableBool();
[SettingSource("Mute hit sounds", "Hit sounds are also muted alongside the track.")]
- public BindableBool AffectsHitSounds { get; } = new BindableBool
- {
- Default = true,
- Value = true
- };
+ public BindableBool AffectsHitSounds { get; } = new BindableBool(true);
protected ModMuted()
{
diff --git a/osu.Game/Rulesets/Mods/ModNoScope.cs b/osu.Game/Rulesets/Mods/ModNoScope.cs
index 1b9ce833ad..36fbb88943 100644
--- a/osu.Game/Rulesets/Mods/ModNoScope.cs
+++ b/osu.Game/Rulesets/Mods/ModNoScope.cs
@@ -6,7 +6,9 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
+using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
@@ -34,6 +36,11 @@ namespace osu.Game.Rulesets.Mods
protected float ComboBasedAlpha;
+ [SettingSource(
+ "Hidden at combo",
+ "The combo count at which the cursor becomes completely hidden",
+ SettingControlType = typeof(SettingsSlider)
+ )]
public abstract BindableInt HiddenComboCount { get; }
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
diff --git a/osu.Game/Rulesets/Mods/ModRandom.cs b/osu.Game/Rulesets/Mods/ModRandom.cs
index 1f7742b075..178b9fb619 100644
--- a/osu.Game/Rulesets/Mods/ModRandom.cs
+++ b/osu.Game/Rulesets/Mods/ModRandom.cs
@@ -18,10 +18,6 @@ namespace osu.Game.Rulesets.Mods
public override double ScoreMultiplier => 1;
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))]
- public Bindable Seed { get; } = new Bindable
- {
- Default = null,
- Value = null
- };
+ public Bindable Seed { get; } = new Bindable();
}
}
diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs
index 72a7f4b9a3..c4cb41fb6a 100644
--- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs
+++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs
@@ -39,10 +39,8 @@ namespace osu.Game.Rulesets.Mods
private double finalRateTime;
private double beginRampTime;
- public BindableNumber SpeedChange { get; } = new BindableDouble
+ public BindableNumber SpeedChange { get; } = new BindableDouble(1)
{
- Default = 1,
- Value = 1,
Precision = 0.01,
};
diff --git a/osu.Game/Rulesets/Mods/ModWindDown.cs b/osu.Game/Rulesets/Mods/ModWindDown.cs
index 22ed7c2efd..35a673093b 100644
--- a/osu.Game/Rulesets/Mods/ModWindDown.cs
+++ b/osu.Game/Rulesets/Mods/ModWindDown.cs
@@ -6,7 +6,6 @@ using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
-using osu.Game.Configuration;
namespace osu.Game.Rulesets.Mods
{
@@ -17,32 +16,21 @@ namespace osu.Game.Rulesets.Mods
public override LocalisableString Description => "Sloooow doooown...";
public override IconUsage? Icon => FontAwesome.Solid.ChevronCircleDown;
- [SettingSource("Initial rate", "The starting speed of the track")]
- public override BindableNumber InitialRate { get; } = new BindableDouble
+ public override BindableNumber InitialRate { get; } = new BindableDouble(1)
{
MinValue = 0.51,
MaxValue = 2,
- Default = 1,
- Value = 1,
Precision = 0.01,
};
- [SettingSource("Final rate", "The speed increase to ramp towards")]
- public override BindableNumber FinalRate { get; } = new BindableDouble
+ public override BindableNumber FinalRate { get; } = new BindableDouble(0.75)
{
MinValue = 0.5,
MaxValue = 1.99,
- Default = 0.75,
- Value = 0.75,
Precision = 0.01,
};
- [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
- public override BindableBool AdjustPitch { get; } = new BindableBool
- {
- Default = true,
- Value = true
- };
+ public override BindableBool AdjustPitch { get; } = new BindableBool(true);
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindUp)).ToArray();
diff --git a/osu.Game/Rulesets/Mods/ModWindUp.cs b/osu.Game/Rulesets/Mods/ModWindUp.cs
index 13ece6d9a3..bbc8382055 100644
--- a/osu.Game/Rulesets/Mods/ModWindUp.cs
+++ b/osu.Game/Rulesets/Mods/ModWindUp.cs
@@ -6,7 +6,6 @@ using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
-using osu.Game.Configuration;
namespace osu.Game.Rulesets.Mods
{
@@ -17,32 +16,21 @@ namespace osu.Game.Rulesets.Mods
public override LocalisableString Description => "Can you keep up?";
public override IconUsage? Icon => FontAwesome.Solid.ChevronCircleUp;
- [SettingSource("Initial rate", "The starting speed of the track")]
- public override BindableNumber InitialRate { get; } = new BindableDouble
+ public override BindableNumber InitialRate { get; } = new BindableDouble(1)
{
MinValue = 0.5,
MaxValue = 1.99,
- Default = 1,
- Value = 1,
Precision = 0.01,
};
- [SettingSource("Final rate", "The speed increase to ramp towards")]
- public override BindableNumber FinalRate { get; } = new BindableDouble
+ public override BindableNumber FinalRate { get; } = new BindableDouble(1.5)
{
MinValue = 0.51,
MaxValue = 2,
- Default = 1.5,
- Value = 1.5,
Precision = 0.01,
};
- [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
- public override BindableBool AdjustPitch { get; } = new BindableBool
- {
- Default = true,
- Value = true
- };
+ public override BindableBool AdjustPitch { get; } = new BindableBool(true);
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindDown)).ToArray();
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index 39ccaa2e5d..dec68a6c22 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -172,7 +172,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
{
config.BindWith(OsuSetting.PositionalHitsoundsLevel, positionalHitsoundsLevel);
- // Explicit non-virtual function call.
+ // Explicit non-virtual function call in case a DrawableHitObject overrides AddInternal.
base.AddInternal(Samples = new PausableSkinnableSound());
CurrentSkin = skinSource;
@@ -405,7 +405,10 @@ namespace osu.Game.Rulesets.Objects.Drawables
///
public event Action ApplyCustomUpdateState;
- protected override void ClearInternal(bool disposeChildren = true) => throw new InvalidOperationException($"Should never clear a {nameof(DrawableHitObject)}");
+ protected override void ClearInternal(bool disposeChildren = true) =>
+ // See sample addition in load method.
+ throw new InvalidOperationException(
+ $"Should never clear a {nameof(DrawableHitObject)} as the base implementation adds components. If attempting to use {nameof(InternalChild)} or {nameof(InternalChildren)}, using {nameof(AddInternal)} or {nameof(AddRangeInternal)} instead.");
private void updateState(ArmedState newState, bool force = false)
{
@@ -648,7 +651,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
///
/// This does not affect the time offset provided to invocations of .
///
- protected virtual double MaximumJudgementOffset => HitObject.HitWindows?.WindowFor(HitResult.Miss) ?? 0;
+ public virtual double MaximumJudgementOffset => HitObject.HitWindows?.WindowFor(HitResult.Miss) ?? 0;
///
/// Applies the of this , notifying responders such as
diff --git a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
index d8a8a6ccd8..825aba5bc2 100644
--- a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
@@ -60,7 +60,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
///
protected readonly BindableDouble TimeRange = new BindableDouble(time_span_default)
{
- Default = time_span_default,
MinValue = time_span_min,
MaxValue = time_span_max
};
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
index 0ed3ca1e63..37da157cc1 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
@@ -3,11 +3,12 @@
#nullable disable
+using System;
using System.Collections.Generic;
-using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Primitives;
using osu.Framework.Layout;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
@@ -126,6 +127,16 @@ namespace osu.Game.Rulesets.UI.Scrolling
private float scrollLength => scrollingAxis == Direction.Horizontal ? DrawWidth : DrawHeight;
+ public override void Add(HitObjectLifetimeEntry entry)
+ {
+ // Scroll info is not available until loaded.
+ // The lifetime of all entries will be updated in the first Update.
+ if (IsLoaded)
+ setComputedLifetimeStart(entry);
+
+ base.Add(entry);
+ }
+
protected override void AddDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable)
{
base.AddDrawable(entry, drawable);
@@ -144,7 +155,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
private void invalidateHitObject(DrawableHitObject hitObject)
{
- hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject);
layoutComputed.Remove(hitObject);
}
@@ -156,10 +166,8 @@ namespace osu.Game.Rulesets.UI.Scrolling
layoutComputed.Clear();
- // Reset lifetime to the conservative estimation.
- // If a drawable becomes alive by this lifetime, its lifetime will be updated to a more precise lifetime in the next update.
foreach (var entry in Entries)
- entry.SetInitialLifetime();
+ setComputedLifetimeStart(entry);
scrollingInfo.Algorithm.Reset();
@@ -186,35 +194,46 @@ namespace osu.Game.Rulesets.UI.Scrolling
}
}
- private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject)
+ ///
+ /// Get a conservative maximum bounding box of a corresponding to .
+ /// It is used to calculate when the hit object appears.
+ ///
+ protected virtual RectangleF GetConservativeBoundingBox(HitObjectLifetimeEntry entry) => new RectangleF().Inflate(100);
+
+ private double computeDisplayStartTime(HitObjectLifetimeEntry entry)
{
- // Origin position may be relative to the parent size
- Debug.Assert(hitObject.Parent != null);
+ RectangleF boundingBox = GetConservativeBoundingBox(entry);
+ float startOffset = 0;
- float originAdjustment = 0.0f;
-
- // calculate the dimension of the part of the hitobject that should already be visible
- // when the hitobject origin first appears inside the scrolling container
switch (direction.Value)
{
- case ScrollingDirection.Up:
- originAdjustment = hitObject.OriginPosition.Y;
+ case ScrollingDirection.Right:
+ startOffset = boundingBox.Right;
break;
case ScrollingDirection.Down:
- originAdjustment = hitObject.DrawHeight - hitObject.OriginPosition.Y;
+ startOffset = boundingBox.Bottom;
break;
case ScrollingDirection.Left:
- originAdjustment = hitObject.OriginPosition.X;
+ startOffset = -boundingBox.Left;
break;
- case ScrollingDirection.Right:
- originAdjustment = hitObject.DrawWidth - hitObject.OriginPosition.X;
+ case ScrollingDirection.Up:
+ startOffset = -boundingBox.Top;
break;
}
- return scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength);
+ return scrollingInfo.Algorithm.GetDisplayStartTime(entry.HitObject.StartTime, startOffset, timeRange.Value, scrollLength);
+ }
+
+ private void setComputedLifetimeStart(HitObjectLifetimeEntry entry)
+ {
+ double computedStartTime = computeDisplayStartTime(entry);
+
+ // always load the hitobject before its first judgement offset
+ double judgementOffset = entry.HitObject.HitWindows?.WindowFor(Scoring.HitResult.Miss) ?? 0;
+ entry.LifetimeStart = Math.Min(entry.HitObject.StartTime - judgementOffset, computedStartTime);
}
private void updateLayoutRecursive(DrawableHitObject hitObject)
@@ -232,8 +251,9 @@ namespace osu.Game.Rulesets.UI.Scrolling
{
updateLayoutRecursive(obj);
- // Nested hitobjects don't need to scroll, but they do need accurate positions
+ // Nested hitobjects don't need to scroll, but they do need accurate positions and start lifetime
updatePosition(obj, hitObject.HitObject.StartTime);
+ setComputedLifetimeStart(obj.Entry);
}
}
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs
index 078f06b745..34e5b7f9de 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs
@@ -38,6 +38,8 @@ namespace osu.Game.Rulesets.UI.Scrolling
///
public virtual Vector2 ScreenSpacePositionAtTime(double time) => HitObjectContainer.ScreenSpacePositionAtTime(time);
- protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer();
+ protected sealed override HitObjectContainer CreateHitObjectContainer() => CreateScrollingHitObjectContainer();
+
+ protected virtual ScrollingHitObjectContainer CreateScrollingHitObjectContainer() => new ScrollingHitObjectContainer();
}
}
diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs
index 25a7bad9e8..1b36ae176d 100644
--- a/osu.Game/Scoring/ScoreInfo.cs
+++ b/osu.Game/Scoring/ScoreInfo.cs
@@ -137,6 +137,11 @@ namespace osu.Game.Scoring
clone.Statistics = new Dictionary(clone.Statistics);
clone.MaximumStatistics = new Dictionary(clone.MaximumStatistics);
+
+ // Ensure we have fresh mods to avoid any references (ie. after gameplay).
+ clone.clearAllMods();
+ clone.ModsJson = ModsJson;
+
clone.RealmUser = new RealmUser
{
OnlineID = RealmUser.OnlineID,
diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
index c794c768c6..f8546d6ed0 100644
--- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
+++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
@@ -129,11 +129,19 @@ namespace osu.Game.Screens.Backgrounds
}
case BackgroundSource.Skin:
- // default skins should use the default background rotation, which won't be the case if a SkinBackground is created for them.
- if (skin.Value is DefaultSkin || skin.Value is DefaultLegacySkin)
- break;
+ switch (skin.Value)
+ {
+ case TrianglesSkin:
+ case ArgonSkin:
+ case DefaultLegacySkin:
+ // default skins should use the default background rotation, which won't be the case if a SkinBackground is created for them.
+ break;
+
+ default:
+ newBackground = new SkinBackground(skin.Value, getBackgroundTextureName());
+ break;
+ }
- newBackground = new SkinBackground(skin.Value, getBackgroundTextureName());
break;
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs
index 076ce224f0..c1c9b2493b 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs
@@ -4,11 +4,12 @@
#nullable disable
using System;
-using System.Linq;
+using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics;
+using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
@@ -34,8 +35,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved]
private OsuColour colours { get; set; }
- private static readonly int highest_divisor = BindableBeatDivisor.PREDEFINED_DIVISORS.Last();
-
public TimelineTickDisplay()
{
RelativeSizeAxes = Axes.Both;
@@ -80,20 +79,19 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
base.Update();
- if (timeline != null)
+ if (timeline == null || DrawWidth <= 0) return;
+
+ (float, float) newRange = (
+ (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X,
+ (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X);
+
+ if (visibleRange != newRange)
{
- var newRange = (
- (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X,
- (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + PointVisualisation.MAX_WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X);
+ visibleRange = newRange;
- if (visibleRange != newRange)
- {
- visibleRange = newRange;
-
- // actual regeneration only needs to occur if we've passed one of the known next min/max tick boundaries.
- if (nextMinTick == null || nextMaxTick == null || (visibleRange.min < nextMinTick || visibleRange.max > nextMaxTick))
- tickCache.Invalidate();
- }
+ // actual regeneration only needs to occur if we've passed one of the known next min/max tick boundaries.
+ if (nextMinTick == null || nextMaxTick == null || (visibleRange.min < nextMinTick || visibleRange.max > nextMaxTick))
+ tickCache.Invalidate();
}
if (!tickCache.IsValid)
@@ -151,6 +149,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
}
+ if (Children.Count > 512)
+ {
+ // There should always be a sanely small number of ticks rendered.
+ // If this assertion triggers, either the zoom logic is broken or a beatmap is
+ // probably doing weird things...
+ //
+ // Let's hope the latter never happens.
+ // If it does, we can choose to either fix it or ignore it as an outlier.
+ string message = $"Timeline is rendering many ticks ({Children.Count})";
+
+ Logger.Log(message);
+ Debug.Fail(message);
+ }
+
int usedDrawables = drawableIndex;
// save a few drawables beyond the currently used for edge cases.
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs
index 0fb59a8a1f..839b2b5bad 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs
@@ -56,7 +56,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected ZoomableScrollContainer()
: base(Direction.Horizontal)
{
- base.Content.Add(zoomedContent = new Container { RelativeSizeAxes = Axes.Y });
+ base.Content.Add(zoomedContent = new Container
+ {
+ RelativeSizeAxes = Axes.Y,
+ // We must hide content until SetupZoom is called.
+ // If not, a child component that relies on its DrawWidth (via RelativeSizeAxes) may see a very incorrect value
+ // momentarily, as noticed in the TimelineTickDisplay, which would render thousands of ticks incorrectly.
+ Alpha = 0,
+ });
AddLayout(zoomedContentWidthCache);
}
@@ -94,6 +101,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
maxZoom = maximum;
CurrentZoom = zoomTarget = initial;
isZoomSetUp = true;
+
+ zoomedContent.Show();
}
///
@@ -118,9 +127,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
CurrentZoom = zoomTarget = newZoom;
}
- protected override void Update()
+ protected override void UpdateAfterChildren()
{
- base.Update();
+ base.UpdateAfterChildren();
if (!zoomedContentWidthCache.IsValid)
updateZoomedContentWidth();
diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs
index 64644be965..ee5ee576d8 100644
--- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs
+++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -16,7 +14,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
public class MatchLeaderboard : Leaderboard
{
[Resolved(typeof(Room), nameof(Room.RoomID))]
- private Bindable roomId { get; set; }
+ private Bindable roomId { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
@@ -33,7 +31,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
protected override bool IsOnlineScope => true;
- protected override APIRequest FetchScores(CancellationToken cancellationToken)
+ protected override APIRequest? FetchScores(CancellationToken cancellationToken)
{
if (roomId.Value == null)
return null;
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
index 75bd6eb04d..bbdfed0a00 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs
@@ -54,6 +54,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
private const float disabled_alpha = 0.2f;
+ public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
+
public Action? SettingsApplied;
public OsuTextBox NameField = null!;
@@ -424,7 +426,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private void hideError() => ErrorText.FadeOut(50);
- private void onSuccess(Room room)
+ private void onSuccess(Room room) => Schedule(() =>
{
Debug.Assert(applyingSettingsOperation != null);
@@ -432,9 +434,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
applyingSettingsOperation.Dispose();
applyingSettingsOperation = null;
- }
+ });
- private void onError(string text)
+ private void onError(string text) => Schedule(() =>
{
Debug.Assert(applyingSettingsOperation != null);
@@ -455,7 +457,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
applyingSettingsOperation.Dispose();
applyingSettingsOperation = null;
- }
+ });
}
public class CreateOrUpdateButton : TriangleButton
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
index 773e68162e..a2c43898f7 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
@@ -9,8 +9,6 @@ using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Graphics.UserInterface;
@@ -21,7 +19,6 @@ using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Ranking;
using osu.Game.Users;
-using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
@@ -41,14 +38,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private readonly TaskCompletionSource resultsReady = new TaskCompletionSource();
- private MultiplayerGameplayLeaderboard leaderboard;
-
private readonly MultiplayerRoomUser[] users;
- private readonly Bindable leaderboardExpanded = new BindableBool();
-
private LoadingLayer loadingDisplay;
- private FillFlowContainer leaderboardFlow;
+
+ private MultiplayerGameplayLeaderboard multiplayerLeaderboard;
///
/// Construct a multiplayer player.
@@ -62,7 +56,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
AllowPause = false,
AllowRestart = false,
AllowSkipping = room.AutoSkip.Value,
- AutomaticallySkipIntro = room.AutoSkip.Value
+ AutomaticallySkipIntro = room.AutoSkip.Value,
+ AlwaysShowLeaderboard = true,
})
{
this.users = users;
@@ -74,45 +69,33 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (!LoadedBeatmapSuccessfully)
return;
- HUDOverlay.Add(leaderboardFlow = new FillFlowContainer
- {
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Vertical,
- Spacing = new Vector2(5)
- });
-
- HUDOverlay.HoldingForHUD.BindValueChanged(_ => updateLeaderboardExpandedState());
- LocalUserPlaying.BindValueChanged(_ => updateLeaderboardExpandedState(), true);
-
- // todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area.
- LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(users), l =>
- {
- if (!LoadedBeatmapSuccessfully)
- return;
-
- leaderboard.Expanded.BindTo(leaderboardExpanded);
-
- leaderboardFlow.Insert(0, l);
-
- if (leaderboard.TeamScores.Count >= 2)
- {
- LoadComponentAsync(new GameplayMatchScoreDisplay
- {
- Team1Score = { BindTarget = leaderboard.TeamScores.First().Value },
- Team2Score = { BindTarget = leaderboard.TeamScores.Last().Value },
- Expanded = { BindTarget = HUDOverlay.ShowHud },
- }, scoreDisplay => leaderboardFlow.Insert(1, scoreDisplay));
- }
- });
-
LoadComponentAsync(new GameplayChatDisplay(Room)
{
- Expanded = { BindTarget = leaderboardExpanded },
- }, chat => leaderboardFlow.Insert(2, chat));
+ Expanded = { BindTarget = LeaderboardExpandedState },
+ }, chat => HUDOverlay.LeaderboardFlow.Insert(2, chat));
HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue });
}
+ protected override GameplayLeaderboard CreateGameplayLeaderboard() => multiplayerLeaderboard = new MultiplayerGameplayLeaderboard(users);
+
+ protected override void AddLeaderboardToHUD(GameplayLeaderboard leaderboard)
+ {
+ Debug.Assert(leaderboard == multiplayerLeaderboard);
+
+ HUDOverlay.LeaderboardFlow.Insert(0, leaderboard);
+
+ if (multiplayerLeaderboard.TeamScores.Count >= 2)
+ {
+ LoadComponentAsync(new GameplayMatchScoreDisplay
+ {
+ Team1Score = { BindTarget = multiplayerLeaderboard.TeamScores.First().Value },
+ Team2Score = { BindTarget = multiplayerLeaderboard.TeamScores.Last().Value },
+ Expanded = { BindTarget = HUDOverlay.ShowHud },
+ }, scoreDisplay => HUDOverlay.LeaderboardFlow.Insert(1, scoreDisplay));
+ }
+ }
+
protected override void LoadAsyncComplete()
{
base.LoadAsyncComplete();
@@ -167,9 +150,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}
}
- private void updateLeaderboardExpandedState() =>
- leaderboardExpanded.Value = !LocalUserPlaying.Value || HUDOverlay.HoldingForHUD.Value;
-
private void failAndBail(string message = null)
{
if (!string.IsNullOrEmpty(message))
@@ -178,23 +158,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Schedule(() => PerformExit(false));
}
- protected override void Update()
- {
- base.Update();
-
- if (!LoadedBeatmapSuccessfully)
- return;
-
- adjustLeaderboardPosition();
- }
-
- private void adjustLeaderboardPosition()
- {
- const float padding = 44; // enough margin to avoid the hit error display.
-
- leaderboardFlow.Position = new Vector2(padding, padding + HUDOverlay.TopScoringElementsHeight);
- }
-
private void onGameplayStarted() => Scheduler.Add(() =>
{
if (!this.IsCurrentScreen())
@@ -232,8 +195,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
Debug.Assert(Room.RoomID.Value != null);
- return leaderboard.TeamScores.Count == 2
- ? new MultiplayerTeamResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem, leaderboard.TeamScores)
+ return multiplayerLeaderboard.TeamScores.Count == 2
+ ? new MultiplayerTeamResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem, multiplayerLeaderboard.TeamScores)
: new MultiplayerResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem);
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs
index 9c05c19d1b..ecef7509d9 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs
@@ -1,10 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
-using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -25,9 +22,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{
private const double fade_time = 50;
- private SpriteIcon icon;
- private OsuSpriteText text;
- private ProgressBar progressBar;
+ private SpriteIcon icon = null!;
+ private OsuSpriteText text = null!;
+ private ProgressBar progressBar = null!;
public StateDisplay()
{
@@ -86,7 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
};
}
- private OsuColour colours;
+ private OsuColour colours = null!;
public void UpdateStatus(MultiplayerUserState state, BeatmapAvailability availability)
{
@@ -164,10 +161,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
break;
case DownloadState.Downloading:
- Debug.Assert(availability.DownloadProgress != null);
-
progressBar.FadeIn(fade_time);
- progressBar.CurrentTime = availability.DownloadProgress.Value;
+ progressBar.CurrentTime = availability.DownloadProgress ?? 0;
text.Text = "downloading map";
icon.Icon = FontAwesome.Solid.ArrowAltCircleDown;
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs
index c7af87a91d..4e9ab07e4c 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorLeaderboard.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Framework.Timing;
using osu.Game.Online.Multiplayer;
diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
index 61ea7d68ee..7e5d90bd4f 100644
--- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs
@@ -105,7 +105,8 @@ namespace osu.Game.Screens.OnlinePlay
while (this.IsCurrentScreen())
this.Exit();
}
- else
+ // Also handle the case where a child screen is current (ie. gameplay).
+ else if (this.GetChildScreen() != null)
{
this.MakeCurrent();
Schedule(forcefullyExit);
diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs
index f21ce5e36a..fde895a1ca 100644
--- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs
+++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs
@@ -1,11 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Linq;
-using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Extensions.Color4Extensions;
@@ -13,15 +10,14 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers;
-using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Users;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD
{
- public class GameplayLeaderboard : CompositeDrawable
+ public abstract class GameplayLeaderboard : CompositeDrawable
{
- private readonly int maxPanels;
private readonly Cached sorting = new Cached();
public Bindable Expanded = new Bindable();
@@ -31,22 +27,22 @@ namespace osu.Game.Screens.Play.HUD
private bool requiresScroll;
private readonly OsuScrollContainer scroll;
- private GameplayLeaderboardScore trackedScore;
+ public GameplayLeaderboardScore? TrackedScore { get; private set; }
+
+ private const int max_panels = 8;
///
/// Create a new leaderboard.
///
- /// The maximum panels to show at once. Defines the maximum height of this component.
- public GameplayLeaderboard(int maxPanels = 8)
+ protected GameplayLeaderboard()
{
- this.maxPanels = maxPanels;
-
Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH;
InternalChildren = new Drawable[]
{
scroll = new InputDisabledScrollContainer
{
+ ClampExtension = 0,
RelativeSizeAxes = Axes.Both,
Child = Flow = new FillFlowContainer
{
@@ -77,24 +73,25 @@ namespace osu.Game.Screens.Play.HUD
/// Whether the player should be tracked on the leaderboard.
/// Set to true for the local player or a player whose replay is currently being played.
///
- public ILeaderboardScore Add([CanBeNull] APIUser user, bool isTracked)
+ public ILeaderboardScore Add(IUser? user, bool isTracked)
{
var drawable = CreateLeaderboardScoreDrawable(user, isTracked);
if (isTracked)
{
- if (trackedScore != null)
+ if (TrackedScore != null)
throw new InvalidOperationException("Cannot track more than one score.");
- trackedScore = drawable;
+ TrackedScore = drawable;
}
drawable.Expanded.BindTo(Expanded);
Flow.Add(drawable);
drawable.TotalScore.BindValueChanged(_ => sorting.Invalidate(), true);
+ drawable.DisplayOrder.BindValueChanged(_ => sorting.Invalidate(), true);
- int displayCount = Math.Min(Flow.Count, maxPanels);
+ int displayCount = Math.Min(Flow.Count, max_panels);
Height = displayCount * (GameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y);
requiresScroll = displayCount != Flow.Count;
@@ -104,21 +101,22 @@ namespace osu.Game.Screens.Play.HUD
public void Clear()
{
Flow.Clear();
- trackedScore = null;
+ TrackedScore = null;
scroll.ScrollToStart(false);
}
- protected virtual GameplayLeaderboardScore CreateLeaderboardScoreDrawable(APIUser user, bool isTracked) =>
+ protected virtual GameplayLeaderboardScore CreateLeaderboardScoreDrawable(IUser? user, bool isTracked) =>
new GameplayLeaderboardScore(user, isTracked);
protected override void Update()
{
base.Update();
- if (requiresScroll && trackedScore != null)
+ if (requiresScroll && TrackedScore != null)
{
- float scrollTarget = scroll.GetChildPosInContent(trackedScore) + trackedScore.DrawHeight / 2 - scroll.DrawHeight / 2;
- scroll.ScrollTo(scrollTarget, false);
+ float scrollTarget = scroll.GetChildPosInContent(TrackedScore) + TrackedScore.DrawHeight / 2 - scroll.DrawHeight / 2;
+
+ scroll.ScrollTo(scrollTarget);
}
const float panel_height = GameplayLeaderboardScore.PANEL_HEIGHT;
@@ -165,7 +163,10 @@ namespace osu.Game.Screens.Play.HUD
if (sorting.IsValid)
return;
- var orderedByScore = Flow.OrderByDescending(i => i.TotalScore.Value).ToList();
+ var orderedByScore = Flow
+ .OrderByDescending(i => i.TotalScore.Value)
+ .ThenBy(i => i.DisplayOrder.Value)
+ .ToList();
for (int i = 0; i < Flow.Count; i++)
{
diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs
index 0f007cd1cb..2eec8253b3 100644
--- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs
+++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -12,7 +13,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
-using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Users;
using osu.Game.Users.Drawables;
using osu.Game.Utils;
using osuTK;
@@ -39,8 +40,6 @@ namespace osu.Game.Screens.Play.HUD
private const float rank_text_width = 35f;
- private const float score_components_width = 85f;
-
private const float avatar_size = 25f;
private const double panel_transition_duration = 500;
@@ -55,6 +54,7 @@ namespace osu.Game.Screens.Play.HUD
public BindableDouble Accuracy { get; } = new BindableDouble(1);
public BindableInt Combo { get; } = new BindableInt();
public BindableBool HasQuit { get; } = new BindableBool();
+ public Bindable DisplayOrder { get; } = new Bindable();
public Color4? BackgroundColour { get; set; }
@@ -81,7 +81,7 @@ namespace osu.Game.Screens.Play.HUD
}
[CanBeNull]
- public APIUser User { get; }
+ public IUser User { get; }
///
/// Whether this score is the local user or a replay player (and should be focused / always visible).
@@ -103,7 +103,7 @@ namespace osu.Game.Screens.Play.HUD
///
/// The score's player.
/// Whether the player is the local user or a replay player.
- public GameplayLeaderboardScore([CanBeNull] APIUser user, bool tracked)
+ public GameplayLeaderboardScore([CanBeNull] IUser user, bool tracked)
{
User = user;
Tracked = tracked;
@@ -160,7 +160,7 @@ namespace osu.Game.Screens.Play.HUD
{
new Dimension(GridSizeMode.Absolute, rank_text_width),
new Dimension(),
- new Dimension(GridSizeMode.AutoSize, maxSize: score_components_width),
+ new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
@@ -285,8 +285,19 @@ namespace osu.Game.Screens.Play.HUD
LoadComponentAsync(new DrawableAvatar(User), avatarContainer.Add);
TotalScore.BindValueChanged(v => scoreText.Text = v.NewValue.ToString("N0"), true);
- Accuracy.BindValueChanged(v => accuracyText.Text = v.NewValue.FormatAccuracy(), true);
- Combo.BindValueChanged(v => comboText.Text = $"{v.NewValue}x", true);
+
+ Accuracy.BindValueChanged(v =>
+ {
+ accuracyText.Text = v.NewValue.FormatAccuracy();
+ updateDetailsWidth();
+ }, true);
+
+ Combo.BindValueChanged(v =>
+ {
+ comboText.Text = $"{v.NewValue}x";
+ updateDetailsWidth();
+ }, true);
+
HasQuit.BindValueChanged(_ => updateState());
}
@@ -302,13 +313,10 @@ namespace osu.Game.Screens.Play.HUD
private void changeExpandedState(ValueChangedEvent expanded)
{
- scoreComponents.ClearTransforms();
-
if (expanded.NewValue)
{
gridContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutQuint);
- scoreComponents.ResizeWidthTo(score_components_width, panel_transition_duration, Easing.OutQuint);
scoreComponents.FadeIn(panel_transition_duration, Easing.OutQuint);
usernameText.FadeIn(panel_transition_duration, Easing.OutQuint);
@@ -317,11 +325,29 @@ namespace osu.Game.Screens.Play.HUD
{
gridContainer.ResizeWidthTo(compact_width, panel_transition_duration, Easing.OutQuint);
- scoreComponents.ResizeWidthTo(0, panel_transition_duration, Easing.OutQuint);
scoreComponents.FadeOut(text_transition_duration, Easing.OutQuint);
usernameText.FadeOut(text_transition_duration, Easing.OutQuint);
}
+
+ updateDetailsWidth();
+ }
+
+ private float? scoreComponentsTargetWidth;
+
+ private void updateDetailsWidth()
+ {
+ const float score_components_min_width = 88f;
+
+ float newWidth = Expanded.Value
+ ? Math.Max(score_components_min_width, comboText.DrawWidth + accuracyText.DrawWidth + 25)
+ : 0;
+
+ if (scoreComponentsTargetWidth == newWidth)
+ return;
+
+ scoreComponentsTargetWidth = newWidth;
+ scoreComponents.ResizeWidthTo(newWidth, panel_transition_duration, Easing.OutQuint);
}
private void updateState()
diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs
index 335d956e39..dadec7c06b 100644
--- a/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs
+++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/ColourHitErrorMeter.cs
@@ -1,13 +1,13 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Game.Configuration;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osuTK;
@@ -17,18 +17,37 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
{
public class ColourHitErrorMeter : HitErrorMeter
{
- internal const int MAX_DISPLAYED_JUDGEMENTS = 20;
-
private const int animation_duration = 200;
private const int drawable_judgement_size = 8;
- private const int spacing = 2;
+
+ [SettingSource("Judgement count", "The number of displayed judgements")]
+ public BindableNumber JudgementCount { get; } = new BindableNumber(20)
+ {
+ MinValue = 1,
+ MaxValue = 50,
+ };
+
+ [SettingSource("Judgement spacing", "The space between each displayed judgement")]
+ public BindableNumber JudgementSpacing { get; } = new BindableNumber(2)
+ {
+ MinValue = 0,
+ MaxValue = 10,
+ };
+
+ [SettingSource("Judgement shape", "The shape of each displayed judgement")]
+ public Bindable JudgementShape { get; } = new Bindable();
private readonly JudgementFlow judgementsFlow;
public ColourHitErrorMeter()
{
AutoSizeAxes = Axes.Both;
- InternalChild = judgementsFlow = new JudgementFlow();
+ InternalChild = judgementsFlow = new JudgementFlow
+ {
+ JudgementShape = { BindTarget = JudgementShape },
+ JudgementSpacing = { BindTarget = JudgementSpacing },
+ JudgementCount = { BindTarget = JudgementCount }
+ };
}
protected override void OnNewJudgement(JudgementResult judgement)
@@ -41,53 +60,105 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
public override void Clear() => judgementsFlow.Clear();
- private class JudgementFlow : FillFlowContainer
+ private class JudgementFlow : FillFlowContainer
{
public override IEnumerable FlowingChildren => base.FlowingChildren.Reverse();
+ public readonly Bindable JudgementShape = new Bindable();
+
+ public readonly Bindable JudgementSpacing = new Bindable();
+
+ public readonly Bindable JudgementCount = new Bindable();
+
public JudgementFlow()
{
- AutoSizeAxes = Axes.X;
- Height = MAX_DISPLAYED_JUDGEMENTS * (drawable_judgement_size + spacing) - spacing;
- Spacing = new Vector2(0, spacing);
+ Width = drawable_judgement_size;
Direction = FillDirection.Vertical;
LayoutDuration = animation_duration;
LayoutEasing = Easing.OutQuint;
}
- public void Push(Color4 colour)
- {
- Add(new HitErrorCircle(colour, drawable_judgement_size));
-
- if (Children.Count > MAX_DISPLAYED_JUDGEMENTS)
- Children.FirstOrDefault(c => !c.IsRemoved)?.Remove();
- }
- }
-
- internal class HitErrorCircle : Container
- {
- public bool IsRemoved { get; private set; }
-
- private readonly Circle circle;
-
- public HitErrorCircle(Color4 colour, int size)
- {
- Size = new Vector2(size);
- Child = circle = new Circle
- {
- RelativeSizeAxes = Axes.Both,
- Alpha = 0,
- Colour = colour
- };
- }
-
protected override void LoadComplete()
{
base.LoadComplete();
- circle.FadeInFromZero(animation_duration, Easing.OutQuint);
- circle.MoveToY(-DrawSize.Y);
- circle.MoveToY(0, animation_duration, Easing.OutQuint);
+ JudgementCount.BindValueChanged(count =>
+ {
+ removeExtraJudgements();
+ updateMetrics();
+ });
+
+ JudgementSpacing.BindValueChanged(_ => updateMetrics(), true);
+ }
+
+ public void Push(Color4 colour)
+ {
+ Add(new HitErrorShape(colour, drawable_judgement_size)
+ {
+ Shape = { BindTarget = JudgementShape },
+ });
+
+ removeExtraJudgements();
+ }
+
+ private void removeExtraJudgements()
+ {
+ var remainingChildren = Children.Where(c => !c.IsRemoved);
+
+ while (remainingChildren.Count() > JudgementCount.Value)
+ remainingChildren.First().Remove();
+ }
+
+ private void updateMetrics()
+ {
+ Height = JudgementCount.Value * (drawable_judgement_size + JudgementSpacing.Value) - JudgementSpacing.Value;
+ Spacing = new Vector2(0, JudgementSpacing.Value);
+ }
+ }
+
+ public class HitErrorShape : Container
+ {
+ public bool IsRemoved { get; private set; }
+
+ public readonly Bindable Shape = new Bindable();
+
+ private readonly Color4 colour;
+
+ private Container content = null!;
+
+ public HitErrorShape(Color4 colour, int size)
+ {
+ this.colour = colour;
+ Size = new Vector2(size);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Child = content = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colour
+ };
+
+ Shape.BindValueChanged(shape =>
+ {
+ switch (shape.NewValue)
+ {
+ case ShapeStyle.Circle:
+ content.Child = new Circle { RelativeSizeAxes = Axes.Both };
+ break;
+
+ case ShapeStyle.Square:
+ content.Child = new Box { RelativeSizeAxes = Axes.Both };
+ break;
+ }
+ }, true);
+
+ content.FadeInFromZero(animation_duration, Easing.OutQuint);
+ content.MoveToY(-DrawSize.Y);
+ content.MoveToY(0, animation_duration, Easing.OutQuint);
}
public void Remove()
@@ -97,5 +168,11 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
this.FadeOut(animation_duration, Easing.OutQuint).Expire();
}
}
+
+ public enum ShapeStyle
+ {
+ Circle,
+ Square
+ }
}
}
diff --git a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs
index 20bf7045b8..aa06bb08a5 100644
--- a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs
+++ b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs
@@ -14,5 +14,11 @@ namespace osu.Game.Screens.Play.HUD
BindableInt Combo { get; }
BindableBool HasQuit { get; }
+
+ ///
+ /// An optional value to guarantee stable ordering.
+ /// Lower numbers will appear higher in cases of ties.
+ ///
+ Bindable DisplayOrder { get; }
}
}
diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
index ac58325060..4201b3f4c9 100644
--- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
+++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
@@ -12,6 +10,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics;
@@ -21,6 +20,7 @@ using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Users;
using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD
@@ -33,19 +33,20 @@ namespace osu.Game.Screens.Play.HUD
public readonly SortedDictionary TeamScores = new SortedDictionary();
[Resolved]
- private OsuColour colours { get; set; }
+ private OsuColour colours { get; set; } = null!;
[Resolved]
- private SpectatorClient spectatorClient { get; set; }
+ private SpectatorClient spectatorClient { get; set; } = null!;
[Resolved]
- private MultiplayerClient multiplayerClient { get; set; }
+ private MultiplayerClient multiplayerClient { get; set; } = null!;
[Resolved]
- private UserLookupCache userLookupCache { get; set; }
+ private UserLookupCache userLookupCache { get; set; } = null!;
+
+ private Bindable scoringMode = null!;
private readonly MultiplayerRoomUser[] playingUsers;
- private Bindable scoringMode;
private readonly IBindableList playingUserIds = new BindableList();
@@ -125,14 +126,17 @@ namespace osu.Game.Screens.Play.HUD
playingUserIds.BindCollectionChanged(playingUsersChanged);
}
- protected override GameplayLeaderboardScore CreateLeaderboardScoreDrawable(APIUser user, bool isTracked)
+ protected override GameplayLeaderboardScore CreateLeaderboardScoreDrawable(IUser? user, bool isTracked)
{
var leaderboardScore = base.CreateLeaderboardScoreDrawable(user, isTracked);
- if (UserScores[user.Id].Team is int team)
+ if (user != null)
{
- leaderboardScore.BackgroundColour = getTeamColour(team).Lighten(1.2f);
- leaderboardScore.TextColour = Color4.White;
+ if (UserScores[user.OnlineID].Team is int team)
+ {
+ leaderboardScore.BackgroundColour = getTeamColour(team).Lighten(1.2f);
+ leaderboardScore.TextColour = Color4.White;
+ }
}
return leaderboardScore;
@@ -188,7 +192,7 @@ namespace osu.Game.Screens.Play.HUD
{
base.Dispose(isDisposing);
- if (spectatorClient != null)
+ if (spectatorClient.IsNotNull())
{
foreach (var user in playingUsers)
spectatorClient.StopWatchingUser(user.UserID);
diff --git a/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.cs
new file mode 100644
index 0000000000..ab3cf2950c
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/SoloGameplayLeaderboard.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 System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+using osu.Game.Users;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ public class SoloGameplayLeaderboard : GameplayLeaderboard
+ {
+ private const int duration = 100;
+
+ private readonly Bindable configVisibility = new Bindable();
+ private readonly IUser trackingUser;
+
+ public readonly IBindableList Scores = new BindableList();
+
+ // hold references to ensure bindables are updated.
+ private readonly List> scoreBindables = new List>();
+
+ [Resolved]
+ private ScoreProcessor scoreProcessor { get; set; } = null!;
+
+ [Resolved]
+ private ScoreManager scoreManager { get; set; } = null!;
+
+ ///
+ /// Whether the leaderboard should be visible regardless of the configuration value.
+ /// This is true by default, but can be changed.
+ ///
+ public readonly Bindable AlwaysVisible = new Bindable(true);
+
+ public SoloGameplayLeaderboard(IUser trackingUser)
+ {
+ this.trackingUser = trackingUser;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ config.BindWith(OsuSetting.GameplayLeaderboard, configVisibility);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ Scores.BindCollectionChanged((_, _) => Scheduler.AddOnce(showScores), true);
+
+ // Alpha will be updated via `updateVisibility` below.
+ Alpha = 0;
+
+ AlwaysVisible.BindValueChanged(_ => updateVisibility());
+ configVisibility.BindValueChanged(_ => updateVisibility(), true);
+ }
+
+ private void showScores()
+ {
+ Clear();
+ scoreBindables.Clear();
+
+ if (!Scores.Any())
+ return;
+
+ foreach (var s in Scores)
+ {
+ var score = Add(s.User, false);
+
+ var bindableTotal = scoreManager.GetBindableTotalScore(s);
+
+ // Direct binding not possible due to differing types (see https://github.com/ppy/osu/issues/20298).
+ bindableTotal.BindValueChanged(total => score.TotalScore.Value = total.NewValue, true);
+ scoreBindables.Add(bindableTotal);
+
+ score.Accuracy.Value = s.Accuracy;
+ score.Combo.Value = s.MaxCombo;
+ score.DisplayOrder.Value = s.OnlineID > 0 ? s.OnlineID : s.Date.ToUnixTimeSeconds();
+ }
+
+ ILeaderboardScore local = Add(trackingUser, true);
+
+ local.TotalScore.BindTarget = scoreProcessor.TotalScore;
+ local.Accuracy.BindTarget = scoreProcessor.Accuracy;
+ local.Combo.BindTarget = scoreProcessor.HighestCombo;
+
+ // Local score should always show lower than any existing scores in cases of ties.
+ local.DisplayOrder.Value = long.MaxValue;
+ }
+
+ private void updateVisibility() =>
+ this.FadeTo(AlwaysVisible.Value || configVisibility.Value ? 1 : 0, duration);
+ }
+}
diff --git a/osu.Game/Screens/Play/HUD/SongProgressInfo.cs b/osu.Game/Screens/Play/HUD/SongProgressInfo.cs
index b3d5066a9e..d0eb8f8ca1 100644
--- a/osu.Game/Screens/Play/HUD/SongProgressInfo.cs
+++ b/osu.Game/Screens/Play/HUD/SongProgressInfo.cs
@@ -47,10 +47,7 @@ namespace osu.Game.Screens.Play.HUD
if (clock != null)
gameplayClock = clock;
- // Lock height so changes in text autosize (if character height changes)
- // don't cause parent invalidation.
- Height = 14;
-
+ AutoSizeAxes = Axes.Y;
Children = new Drawable[]
{
new Container
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index f9f3693385..3fbb051c3b 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -9,7 +9,6 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
-using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
@@ -35,11 +34,6 @@ namespace osu.Game.Screens.Play
public const Easing FADE_EASING = Easing.OutQuint;
- ///
- /// The total height of all the top of screen scoring elements.
- ///
- public float TopScoringElementsHeight { get; private set; }
-
///
/// The total height of all the bottom of screen scoring elements.
///
@@ -80,9 +74,15 @@ namespace osu.Game.Screens.Play
private readonly SkinnableTargetContainer mainComponents;
- private IEnumerable hideTargets => new Drawable[] { mainComponents, KeyCounter, topRightElements };
+ ///
+ /// A flow which sits at the left side of the screen to house leaderboard (and related) components.
+ /// Will automatically be positioned to avoid colliding with top scoring elements.
+ ///
+ public readonly FillFlowContainer LeaderboardFlow;
- public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList mods)
+ private readonly List hideTargets;
+
+ public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList mods, bool alwaysShowLeaderboard = true)
{
this.drawableRuleset = drawableRuleset;
this.mods = mods;
@@ -127,8 +127,20 @@ namespace osu.Game.Screens.Play
HoldToQuit = CreateHoldForMenuButton(),
}
},
- clicksPerSecondCalculator = new ClicksPerSecondCalculator()
+ LeaderboardFlow = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Padding = new MarginPadding(44), // enough margin to avoid the hit error display
+ Spacing = new Vector2(5)
+ },
+ clicksPerSecondCalculator = new ClicksPerSecondCalculator(),
};
+
+ hideTargets = new List { mainComponents, KeyCounter, topRightElements };
+
+ if (!alwaysShowLeaderboard)
+ hideTargets.Add(LeaderboardFlow);
}
[BackgroundDependencyLoader(true)]
@@ -177,22 +189,36 @@ namespace osu.Game.Screens.Play
{
base.Update();
- Vector2? lowestTopScreenSpace = null;
+ float? lowestTopScreenSpaceLeft = null;
+ float? lowestTopScreenSpaceRight = null;
+
Vector2? highestBottomScreenSpace = null;
// LINQ cast can be removed when IDrawable interface includes Anchor / RelativeSizeAxes.
foreach (var element in mainComponents.Components.Cast())
{
- // for now align top-right components with the bottom-edge of the lowest top-anchored hud element.
- if (element.Anchor.HasFlagFast(Anchor.TopRight) || (element.Anchor.HasFlagFast(Anchor.y0) && element.RelativeSizeAxes == Axes.X))
+ // for now align some top components with the bottom-edge of the lowest top-anchored hud element.
+ if (element.Anchor.HasFlagFast(Anchor.y0))
{
// health bars are excluded for the sake of hacky legacy skins which extend the health bar to take up the full screen area.
if (element is LegacyHealthDisplay)
continue;
- var bottomRight = element.ScreenSpaceDrawQuad.BottomRight;
- if (lowestTopScreenSpace == null || bottomRight.Y > lowestTopScreenSpace.Value.Y)
- lowestTopScreenSpace = bottomRight;
+ float bottom = element.ScreenSpaceDrawQuad.BottomRight.Y;
+
+ bool isRelativeX = element.RelativeSizeAxes == Axes.X;
+
+ if (element.Anchor.HasFlagFast(Anchor.TopRight) || isRelativeX)
+ {
+ if (lowestTopScreenSpaceRight == null || bottom > lowestTopScreenSpaceRight.Value)
+ lowestTopScreenSpaceRight = bottom;
+ }
+
+ if (element.Anchor.HasFlagFast(Anchor.TopLeft) || isRelativeX)
+ {
+ if (lowestTopScreenSpaceLeft == null || bottom > lowestTopScreenSpaceLeft.Value)
+ lowestTopScreenSpaceLeft = bottom;
+ }
}
// and align bottom-right components with the top-edge of the highest bottom-anchored hud element.
else if (element.Anchor.HasFlagFast(Anchor.BottomRight) || (element.Anchor.HasFlagFast(Anchor.y2) && element.RelativeSizeAxes == Axes.X))
@@ -203,11 +229,16 @@ namespace osu.Game.Screens.Play
}
}
- if (lowestTopScreenSpace.HasValue)
- topRightElements.Y = TopScoringElementsHeight = MathHelper.Clamp(ToLocalSpace(lowestTopScreenSpace.Value).Y, 0, DrawHeight - topRightElements.DrawHeight);
+ if (lowestTopScreenSpaceRight.HasValue)
+ topRightElements.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceRight.Value)).Y, 0, DrawHeight - topRightElements.DrawHeight);
else
topRightElements.Y = 0;
+ if (lowestTopScreenSpaceLeft.HasValue)
+ LeaderboardFlow.Y = MathHelper.Clamp(ToLocalSpace(new Vector2(0, lowestTopScreenSpaceLeft.Value)).Y, 0, DrawHeight - LeaderboardFlow.DrawHeight);
+ else
+ LeaderboardFlow.Y = 0;
+
if (highestBottomScreenSpace.HasValue)
bottomRightElements.Y = BottomScoringElementsHeight = -MathHelper.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - bottomRightElements.DrawHeight);
else
diff --git a/osu.Game/Screens/Play/KeyCounterDisplay.cs b/osu.Game/Screens/Play/KeyCounterDisplay.cs
index b6094726c0..1b726b0f7b 100644
--- a/osu.Game/Screens/Play/KeyCounterDisplay.cs
+++ b/osu.Game/Screens/Play/KeyCounterDisplay.cs
@@ -39,6 +39,7 @@ namespace osu.Game.Screens.Play
{
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
+ Alpha = 0,
};
}
diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs
index 047f25a111..c3c351ac36 100644
--- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs
+++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs
@@ -34,7 +34,6 @@ namespace osu.Game.Screens.Play
public readonly BindableNumber UserPlaybackRate = new BindableDouble(1)
{
- Default = 1,
MinValue = 0.5,
MaxValue = 2,
Precision = 0.1,
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 91e9c3b58f..7721d5b912 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -9,6 +9,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@@ -34,6 +35,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
+using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Ranking;
using osu.Game.Skinning;
using osu.Game.Users;
@@ -375,6 +377,8 @@ namespace osu.Game.Screens.Play
if (Configuration.AutomaticallySkipIntro)
skipIntroOverlay.SkipWhenReady();
+
+ loadLeaderboard();
}
protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart);
@@ -417,7 +421,7 @@ namespace osu.Game.Screens.Play
// display the cursor above some HUD elements.
DrawableRuleset.Cursor?.CreateProxy() ?? new Container(),
DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(),
- HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods)
+ HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods, Configuration.AlwaysShowLeaderboard)
{
HoldToQuit =
{
@@ -562,9 +566,6 @@ namespace osu.Game.Screens.Play
///
protected void PerformExit(bool showDialogFirst)
{
- // if an exit has been requested, cancel any pending completion (the user has shown intention to exit).
- resultsDisplayDelegate?.Cancel();
-
// there is a chance that an exit request occurs after the transition to results has already started.
// even in such a case, the user has shown intent, so forcefully return to this screen (to proceed with the upwards exit process).
if (!this.IsCurrentScreen())
@@ -599,6 +600,9 @@ namespace osu.Game.Screens.Play
}
}
+ // if an exit has been requested, cancel any pending completion (the user has shown intention to exit).
+ resultsDisplayDelegate?.Cancel();
+
// The actual exit is performed if
// - the pause / fail dialog was not requested
// - the pause / fail dialog was requested but is already displayed (user showing intention to exit).
@@ -776,19 +780,11 @@ namespace osu.Game.Screens.Play
///
///
/// A final display will only occur once all work is completed in . This means that even after calling this method, the results screen will never be shown until ScoreProcessor.HasCompleted becomes .
- ///
- /// Calling this method multiple times will have no effect.
///
/// Whether a minimum delay () should be added before the screen is displayed.
private void progressToResults(bool withDelay)
{
- if (resultsDisplayDelegate != null)
- // Note that if progressToResults is called one withDelay=true and then withDelay=false, this no-delay timing will not be
- // accounted for. shouldn't be a huge concern (a user pressing the skip button after a results progression has already been queued
- // may take x00 more milliseconds than expected in the very rare edge case).
- //
- // If required we can handle this more correctly by rescheduling here.
- return;
+ resultsDisplayDelegate?.Cancel();
double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0;
@@ -820,6 +816,41 @@ namespace osu.Game.Screens.Play
return mouseWheelDisabled.Value && !e.AltPressed;
}
+ #region Gameplay leaderboard
+
+ protected readonly Bindable LeaderboardExpandedState = new BindableBool();
+
+ private void loadLeaderboard()
+ {
+ HUDOverlay.HoldingForHUD.BindValueChanged(_ => updateLeaderboardExpandedState());
+ LocalUserPlaying.BindValueChanged(_ => updateLeaderboardExpandedState(), true);
+
+ var gameplayLeaderboard = CreateGameplayLeaderboard();
+
+ if (gameplayLeaderboard != null)
+ {
+ LoadComponentAsync(gameplayLeaderboard, leaderboard =>
+ {
+ if (!LoadedBeatmapSuccessfully)
+ return;
+
+ leaderboard.Expanded.BindTo(LeaderboardExpandedState);
+
+ AddLeaderboardToHUD(leaderboard);
+ });
+ }
+ }
+
+ [CanBeNull]
+ protected virtual GameplayLeaderboard CreateGameplayLeaderboard() => null;
+
+ protected virtual void AddLeaderboardToHUD(GameplayLeaderboard leaderboard) => HUDOverlay.LeaderboardFlow.Add(leaderboard);
+
+ private void updateLeaderboardExpandedState() =>
+ LeaderboardExpandedState.Value = !LocalUserPlaying.Value || HUDOverlay.HoldingForHUD.Value;
+
+ #endregion
+
#region Fail Logic
protected FailOverlay FailOverlay { get; private set; }
diff --git a/osu.Game/Screens/Play/PlayerConfiguration.cs b/osu.Game/Screens/Play/PlayerConfiguration.cs
index b1b0e01d80..b82925ccb8 100644
--- a/osu.Game/Screens/Play/PlayerConfiguration.cs
+++ b/osu.Game/Screens/Play/PlayerConfiguration.cs
@@ -36,5 +36,10 @@ namespace osu.Game.Screens.Play
/// Whether the intro should be skipped by default.
///
public bool AutomaticallySkipIntro { get; set; }
+
+ ///
+ /// Whether the gameplay leaderboard should always be shown (usually in a contracted state).
+ ///
+ public bool AlwaysShowLeaderboard { get; set; }
}
}
diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs
index 6373633b5a..e32d3d90be 100644
--- a/osu.Game/Screens/Play/PlayerLoader.cs
+++ b/osu.Game/Screens/Play/PlayerLoader.cs
@@ -502,7 +502,7 @@ namespace osu.Game.Screens.Play
private int restartCount;
- private const double volume_requirement = 0.05;
+ private const double volume_requirement = 0.01;
private void showMuteWarningIfNeeded()
{
@@ -539,10 +539,11 @@ namespace osu.Game.Screens.Play
volumeOverlay.IsMuted.Value = false;
// Check values before resetting, as the user may have only had mute enabled, in which case we might not need to adjust volumes.
+ // Note that we only restore halfway to ensure the user isn't suddenly overloaded by unexpectedly high volume.
if (audioManager.Volume.Value <= volume_requirement)
- audioManager.Volume.SetDefault();
+ audioManager.Volume.Value = 0.5f;
if (audioManager.VolumeTrack.Value <= volume_requirement)
- audioManager.VolumeTrack.SetDefault();
+ audioManager.VolumeTrack.Value = 0.5f;
return true;
};
diff --git a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs
index 75da8e7b9d..537f4d811a 100644
--- a/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs
+++ b/osu.Game/Screens/Play/PlayerSettings/BeatmapOffsetControl.cs
@@ -31,8 +31,6 @@ namespace osu.Game.Screens.Play.PlayerSettings
public BindableDouble Current { get; } = new BindableDouble
{
- Default = 0,
- Value = 0,
MinValue = -50,
MaxValue = 50,
Precision = 0.1,
diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs
index 12646d656a..14e3123028 100644
--- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs
+++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs
@@ -17,7 +17,6 @@ namespace osu.Game.Screens.Play.PlayerSettings
public readonly Bindable UserPlaybackRate = new BindableDouble(1)
{
- Default = 1,
MinValue = 0.5,
MaxValue = 2,
Precision = 0.1,
diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs
index e82238945b..5382e283e0 100644
--- a/osu.Game/Screens/Play/ReplayPlayer.cs
+++ b/osu.Game/Screens/Play/ReplayPlayer.cs
@@ -7,6 +7,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
+using osu.Framework.Bindables;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
@@ -14,6 +15,7 @@ using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Scoring;
+using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Ranking;
namespace osu.Game.Screens.Play
@@ -55,6 +57,15 @@ namespace osu.Game.Screens.Play
// Don't re-import replay scores as they're already present in the database.
protected override Task ImportScore(Score score) => Task.CompletedTask;
+ public readonly BindableList LeaderboardScores = new BindableList();
+
+ protected override GameplayLeaderboard CreateGameplayLeaderboard() =>
+ new SoloGameplayLeaderboard(Score.ScoreInfo.User)
+ {
+ AlwaysVisible = { Value = true },
+ Scores = { BindTarget = LeaderboardScores }
+ };
+
protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false);
public bool OnPressed(KeyBindingPressEvent e)
diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs
index 565f256277..ee19391b89 100644
--- a/osu.Game/Screens/Play/SoloPlayer.cs
+++ b/osu.Game/Screens/Play/SoloPlayer.cs
@@ -5,12 +5,15 @@
using System;
using System.Diagnostics;
+using System.Threading.Tasks;
+using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Online.Solo;
using osu.Game.Scoring;
+using osu.Game.Screens.Play.HUD;
namespace osu.Game.Screens.Play
{
@@ -40,8 +43,27 @@ namespace osu.Game.Screens.Play
return new CreateSoloScoreRequest(Beatmap.Value.BeatmapInfo, rulesetId, Game.VersionHash);
}
+ public readonly BindableList LeaderboardScores = new BindableList();
+
+ protected override GameplayLeaderboard CreateGameplayLeaderboard() =>
+ new SoloGameplayLeaderboard(Score.ScoreInfo.User)
+ {
+ AlwaysVisible = { Value = false },
+ Scores = { BindTarget = LeaderboardScores }
+ };
+
protected override bool HandleTokenRetrievalFailure(Exception exception) => false;
+ protected override Task ImportScore(Score score)
+ {
+ // Before importing a score, stop binding the leaderboard with its score source.
+ // This avoids a case where the imported score may cause a leaderboard refresh
+ // (if the leaderboard's source is local).
+ LeaderboardScores.UnbindBindings();
+
+ return base.ImportScore(score);
+ }
+
protected override APIRequest CreateSubmissionRequest(Score score, long token)
{
IBeatmapInfo beatmap = score.ScoreInfo.BeatmapInfo;
diff --git a/osu.Game/Screens/Play/SquareGraph.cs b/osu.Game/Screens/Play/SquareGraph.cs
index 00d6ede3bf..9ac673ae52 100644
--- a/osu.Game/Screens/Play/SquareGraph.cs
+++ b/osu.Game/Screens/Play/SquareGraph.cs
@@ -15,7 +15,6 @@ using osuTK;
using osuTK.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Allocation;
-using osu.Framework.Layout;
using osu.Framework.Threading;
namespace osu.Game.Screens.Play
@@ -24,11 +23,6 @@ namespace osu.Game.Screens.Play
{
private BufferedContainer columns;
- public SquareGraph()
- {
- AddLayout(layout);
- }
-
public int ColumnCount => columns?.Children.Count ?? 0;
private int progress;
@@ -57,7 +51,7 @@ namespace osu.Game.Screens.Play
if (value == values) return;
values = value;
- layout.Invalidate();
+ graphNeedsUpdate = true;
}
}
@@ -75,21 +69,25 @@ namespace osu.Game.Screens.Play
}
}
- private readonly LayoutValue layout = new LayoutValue(Invalidation.DrawSize);
private ScheduledDelegate scheduledCreate;
+ private bool graphNeedsUpdate;
+
+ private Vector2 previousDrawSize;
+
protected override void Update()
{
base.Update();
- if (values != null && !layout.IsValid)
+ if (graphNeedsUpdate || (values != null && DrawSize != previousDrawSize))
{
columns?.FadeOut(500, Easing.OutQuint).Expire();
scheduledCreate?.Cancel();
scheduledCreate = Scheduler.AddDelayed(RecreateGraph, 500);
- layout.Validate();
+ previousDrawSize = DrawSize;
+ graphNeedsUpdate = false;
}
}
diff --git a/osu.Game/Screens/Select/FooterButtonRandom.cs b/osu.Game/Screens/Select/FooterButtonRandom.cs
index 1f56915f62..aad7fdff39 100644
--- a/osu.Game/Screens/Select/FooterButtonRandom.cs
+++ b/osu.Game/Screens/Select/FooterButtonRandom.cs
@@ -138,7 +138,8 @@ namespace osu.Game.Screens.Select
return false;
}
- TriggerClick();
+ if (!e.Repeat)
+ TriggerClick();
return true;
}
diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
index 343b815e9f..161d4847bf 100644
--- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
+++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -25,11 +23,11 @@ namespace osu.Game.Screens.Select.Leaderboards
{
public class BeatmapLeaderboard : Leaderboard
{
- public Action ScoreSelected;
+ public Action? ScoreSelected;
- private BeatmapInfo beatmapInfo;
+ private BeatmapInfo? beatmapInfo;
- public BeatmapInfo BeatmapInfo
+ public BeatmapInfo? BeatmapInfo
{
get => beatmapInfo;
set
@@ -41,6 +39,11 @@ namespace osu.Game.Screens.Select.Leaderboards
return;
beatmapInfo = value;
+
+ // Refetch is scheduled, which can cause scores to be outdated if the leaderboard is not currently updating.
+ // As scores are potentially used by other components, clear them eagerly to ensure a more correct state.
+ SetScores(null);
+
RefetchScores();
}
}
@@ -65,24 +68,26 @@ namespace osu.Game.Screens.Select.Leaderboards
}
[Resolved]
- private ScoreManager scoreManager { get; set; }
+ private ScoreManager scoreManager { get; set; } = null!;
[Resolved]
- private IBindable ruleset { get; set; }
+ private IBindable ruleset { get; set; } = null!;
[Resolved]
- private IBindable> mods { get; set; }
+ private IBindable> mods { get; set; } = null!;
[Resolved]
- private IAPIProvider api { get; set; }
+ private IAPIProvider api { get; set; } = null!;
[Resolved]
- private RulesetStore rulesets { get; set; }
+ private RulesetStore rulesets { get; set; } = null!;
[Resolved]
- private RealmAccess realm { get; set; }
+ private RealmAccess realm { get; set; } = null!;
- private IDisposable scoreSubscription;
+ private IDisposable? scoreSubscription;
+
+ private GetScoresRequest? scoreRetrievalRequest;
[BackgroundDependencyLoader]
private void load()
@@ -97,10 +102,9 @@ namespace osu.Game.Screens.Select.Leaderboards
protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local;
- protected override APIRequest FetchScores(CancellationToken cancellationToken)
+ protected override APIRequest? FetchScores(CancellationToken cancellationToken)
{
var fetchBeatmapInfo = BeatmapInfo;
- var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset;
if (fetchBeatmapInfo == null)
{
@@ -108,13 +112,15 @@ namespace osu.Game.Screens.Select.Leaderboards
return null;
}
+ var fetchRuleset = ruleset.Value ?? fetchBeatmapInfo.Ruleset;
+
if (Scope == BeatmapLeaderboardScope.Local)
{
subscribeToLocalScores(fetchBeatmapInfo, cancellationToken);
return null;
}
- if (api?.IsLoggedIn != true)
+ if (!api.IsLoggedIn)
{
SetErrorState(LeaderboardState.NotLoggedIn);
return null;
@@ -138,7 +144,7 @@ namespace osu.Game.Screens.Select.Leaderboards
return null;
}
- IReadOnlyList requestMods = null;
+ IReadOnlyList? requestMods = null;
if (filterMods && !mods.Value.Any())
// add nomod for the request
@@ -146,16 +152,14 @@ namespace osu.Game.Screens.Select.Leaderboards
else if (filterMods)
requestMods = mods.Value;
- var req = new GetScoresRequest(fetchBeatmapInfo, fetchRuleset, Scope, requestMods);
+ scoreRetrievalRequest = new GetScoresRequest(fetchBeatmapInfo, fetchRuleset, Scope, requestMods);
- req.Success += r => Schedule(() =>
- {
- SetScores(
- scoreManager.OrderByTotalScore(r.Scores.Select(s => s.ToScoreInfo(rulesets, fetchBeatmapInfo))),
- r.UserScore?.CreateScoreInfo(rulesets, fetchBeatmapInfo));
- });
+ scoreRetrievalRequest.Success += response => SetScores(
+ scoreManager.OrderByTotalScore(response.Scores.Select(s => s.ToScoreInfo(rulesets, fetchBeatmapInfo))),
+ response.UserScore?.CreateScoreInfo(rulesets, fetchBeatmapInfo)
+ );
- return req;
+ return scoreRetrievalRequest;
}
protected override LeaderboardScore CreateDrawableScore(ScoreInfo model, int index) => new LeaderboardScore(model, index, IsOnlineScope)
@@ -181,7 +185,7 @@ namespace osu.Game.Screens.Select.Leaderboards
+ $" AND {nameof(ScoreInfo.DeletePending)} == false"
, beatmapInfo.ID, ruleset.Value.ShortName), localScoresChanged);
- void localScoresChanged(IRealmCollection sender, ChangeSet changes, Exception exception)
+ void localScoresChanged(IRealmCollection sender, ChangeSet? changes, Exception exception)
{
if (cancellationToken.IsCancellationRequested)
return;
@@ -208,14 +212,16 @@ namespace osu.Game.Screens.Select.Leaderboards
scores = scoreManager.OrderByTotalScore(scores.Detach());
- Schedule(() => SetScores(scores));
+ SetScores(scores);
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
+
scoreSubscription?.Dispose();
+ scoreRetrievalRequest?.Cancel();
}
}
}
diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs
index c24ca9a7cf..94e4215175 100644
--- a/osu.Game/Screens/Select/PlaySongSelect.cs
+++ b/osu.Game/Screens/Select/PlaySongSelect.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
@@ -24,27 +22,38 @@ namespace osu.Game.Screens.Select
{
public class PlaySongSelect : SongSelect
{
- private OsuScreen playerLoader;
+ private OsuScreen? playerLoader;
[Resolved(CanBeNull = true)]
- private INotificationOverlay notifications { get; set; }
+ private INotificationOverlay? notifications { get; set; }
public override bool AllowExternalScreenChange => true;
protected override UserActivity InitialActivity => new UserActivity.ChoosingBeatmap();
+ private PlayBeatmapDetailArea playBeatmapDetailArea = null!;
+
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
BeatmapOptions.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, () => Edit());
-
- ((PlayBeatmapDetailArea)BeatmapDetails).Leaderboard.ScoreSelected += PresentScore;
}
protected void PresentScore(ScoreInfo score) =>
FinaliseSelection(score.BeatmapInfo, score.Ruleset, () => this.Push(new SoloResultsScreen(score, false)));
- protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
+ protected override BeatmapDetailArea CreateBeatmapDetailArea()
+ {
+ playBeatmapDetailArea = new PlayBeatmapDetailArea
+ {
+ Leaderboard =
+ {
+ ScoreSelected = PresentScore
+ }
+ };
+
+ return playBeatmapDetailArea;
+ }
protected override bool OnKeyDown(KeyDownEvent e)
{
@@ -61,9 +70,9 @@ namespace osu.Game.Screens.Select
return base.OnKeyDown(e);
}
- private IReadOnlyList modsAtGameplayStart;
+ private IReadOnlyList? modsAtGameplayStart;
- private ModAutoplay getAutoplayMod() => Ruleset.Value.CreateInstance().GetAutoplayMod();
+ private ModAutoplay? getAutoplayMod() => Ruleset.Value.CreateInstance().GetAutoplayMod();
protected override bool OnStart()
{
@@ -100,14 +109,26 @@ namespace osu.Game.Screens.Select
Player createPlayer()
{
+ Player player;
+
var replayGeneratingMod = Mods.Value.OfType().FirstOrDefault();
if (replayGeneratingMod != null)
{
- return new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods));
+ player = new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateScoreFromReplayData(beatmap, mods))
+ {
+ LeaderboardScores = { BindTarget = playBeatmapDetailArea.Leaderboard.Scores }
+ };
+ }
+ else
+ {
+ player = new SoloPlayer
+ {
+ LeaderboardScores = { BindTarget = playBeatmapDetailArea.Leaderboard.Scores }
+ };
}
- return new SoloPlayer();
+ return player;
}
}
diff --git a/osu.Game/Screens/Utility/LatencyCertifierScreen.cs b/osu.Game/Screens/Utility/LatencyCertifierScreen.cs
index c9d4dc7811..bacaccd68e 100644
--- a/osu.Game/Screens/Utility/LatencyCertifierScreen.cs
+++ b/osu.Game/Screens/Utility/LatencyCertifierScreen.cs
@@ -261,8 +261,8 @@ namespace osu.Game.Screens.Utility
string exclusive = "unknown";
- if (host.Window is WindowsWindow windowsWindow)
- exclusive = windowsWindow.FullscreenCapability.ToString();
+ if (host.Renderer is IWindowsRenderer windowsRenderer)
+ exclusive = windowsRenderer.FullscreenCapability.ToString();
statusText.Clear();
diff --git a/osu.Game/Skinning/ArgonSkin.cs b/osu.Game/Skinning/ArgonSkin.cs
new file mode 100644
index 0000000000..010e2175e1
--- /dev/null
+++ b/osu.Game/Skinning/ArgonSkin.cs
@@ -0,0 +1,220 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+#nullable disable
+using System.Collections.Generic;
+using System.Linq;
+using JetBrains.Annotations;
+using osu.Framework.Audio.Sample;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Audio;
+using osu.Game.Beatmaps.Formats;
+using osu.Game.Extensions;
+using osu.Game.IO;
+using osu.Game.Screens.Play.HUD;
+using osu.Game.Screens.Play.HUD.HitErrorMeters;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Skinning
+{
+ public class ArgonSkin : Skin
+ {
+ public static SkinInfo CreateInfo() => new SkinInfo
+ {
+ ID = osu.Game.Skinning.SkinInfo.ARGON_SKIN,
+ Name = "osu! \"argon\" (2022)",
+ Creator = "team osu!",
+ Protected = true,
+ InstantiationInfo = typeof(ArgonSkin).GetInvariantInstantiationInfo()
+ };
+
+ private readonly IStorageResourceProvider resources;
+
+ public ArgonSkin(IStorageResourceProvider resources)
+ : this(CreateInfo(), resources)
+ {
+ }
+
+ [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)]
+ public ArgonSkin(SkinInfo skin, IStorageResourceProvider resources)
+ : base(skin, resources)
+ {
+ this.resources = resources;
+
+ Configuration.CustomComboColours = new List
+ {
+ // Standard combo progression order is green - blue - red - yellow.
+ // But for whatever reason, this starts from index 1, not 0.
+ //
+ // We've added two new combo colours in argon, so to ensure the initial rotation matches,
+ // this same progression is in slots 1 - 4.
+
+ // Orange
+ new Color4(241, 116, 0, 255),
+ // Green
+ new Color4(0, 241, 53, 255),
+ // Blue
+ new Color4(0, 82, 241, 255),
+ // Red
+ new Color4(241, 0, 0, 255),
+ // Yellow
+ new Color4(232, 235, 0, 255),
+ // Purple
+ new Color4(92, 0, 241, 255),
+ };
+ }
+
+ public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => Textures?.Get(componentName, wrapModeS, wrapModeT);
+
+ public override ISample GetSample(ISampleInfo sampleInfo)
+ {
+ foreach (string lookup in sampleInfo.LookupNames)
+ {
+ var sample = Samples?.Get(lookup) ?? resources.AudioManager?.Samples.Get(lookup);
+ if (sample != null)
+ return sample;
+ }
+
+ return null;
+ }
+
+ public override Drawable GetDrawableComponent(ISkinComponent component)
+ {
+ if (base.GetDrawableComponent(component) is Drawable c)
+ return c;
+
+ switch (component)
+ {
+ case SkinnableTargetComponent target:
+ switch (target.Target)
+ {
+ case SkinnableTarget.SongSelect:
+ var songSelectComponents = new SkinnableTargetComponentsContainer(_ =>
+ {
+ // do stuff when we need to.
+ });
+
+ return songSelectComponents;
+
+ case SkinnableTarget.MainHUDComponents:
+ var skinnableTargetWrapper = new SkinnableTargetComponentsContainer(container =>
+ {
+ var score = container.OfType().FirstOrDefault();
+ var accuracy = container.OfType().FirstOrDefault();
+ var combo = container.OfType().FirstOrDefault();
+ var ppCounter = container.OfType().FirstOrDefault();
+
+ if (score != null)
+ {
+ score.Anchor = Anchor.TopCentre;
+ score.Origin = Anchor.TopCentre;
+
+ // elements default to beneath the health bar
+ const float vertical_offset = 30;
+
+ const float horizontal_padding = 20;
+
+ score.Position = new Vector2(0, vertical_offset);
+
+ if (ppCounter != null)
+ {
+ ppCounter.Y = score.Position.Y + ppCounter.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).Y - 4;
+ ppCounter.Origin = Anchor.TopCentre;
+ ppCounter.Anchor = Anchor.TopCentre;
+ }
+
+ if (accuracy != null)
+ {
+ accuracy.Position = new Vector2(-accuracy.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).X / 2 - horizontal_padding, vertical_offset + 5);
+ accuracy.Origin = Anchor.TopRight;
+ accuracy.Anchor = Anchor.TopCentre;
+
+ if (combo != null)
+ {
+ combo.Position = new Vector2(accuracy.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).X / 2 + horizontal_padding, vertical_offset + 5);
+ combo.Anchor = Anchor.TopCentre;
+ }
+ }
+
+ var hitError = container.OfType().FirstOrDefault();
+
+ if (hitError != null)
+ {
+ hitError.Anchor = Anchor.CentreLeft;
+ hitError.Origin = Anchor.CentreLeft;
+ }
+
+ var hitError2 = container.OfType().LastOrDefault();
+
+ if (hitError2 != null)
+ {
+ hitError2.Anchor = Anchor.CentreRight;
+ hitError2.Scale = new Vector2(-1, 1);
+ // origin flipped to match scale above.
+ hitError2.Origin = Anchor.CentreLeft;
+ }
+ }
+ })
+ {
+ Children = new Drawable[]
+ {
+ new DefaultComboCounter(),
+ new DefaultScoreCounter(),
+ new DefaultAccuracyCounter(),
+ new DefaultHealthDisplay(),
+ new DefaultSongProgress(),
+ new BarHitErrorMeter(),
+ new BarHitErrorMeter(),
+ new PerformancePointsCounter()
+ }
+ };
+
+ return skinnableTargetWrapper;
+ }
+
+ return null;
+ }
+
+ switch (component.LookupName)
+ {
+ // Temporary until default skin has a valid hit lighting.
+ case @"lighting":
+ return Drawable.Empty();
+ }
+
+ if (GetTexture(component.LookupName) is Texture t)
+ return new Sprite { Texture = t };
+
+ return null;
+ }
+
+ public override IBindable GetConfig(TLookup lookup)
+ {
+ // todo: this code is pulled from LegacySkin and should not exist.
+ // will likely change based on how databased storage of skin configuration goes.
+ switch (lookup)
+ {
+ case GlobalSkinColours global:
+ switch (global)
+ {
+ case GlobalSkinColours.ComboColours:
+ return SkinUtils.As(new Bindable>(Configuration.ComboColours));
+ }
+
+ break;
+
+ case SkinComboColourLookup comboColour:
+ return SkinUtils.As(new Bindable(getComboColour(Configuration, comboColour.ColourIndex)));
+ }
+
+ return null;
+ }
+
+ private static Color4 getComboColour(IHasComboColours source, int colourIndex)
+ => source.ComboColours[colourIndex % source.ComboColours.Count];
+ }
+}
diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs
index 2ebcc98c53..04f1286dc7 100644
--- a/osu.Game/Skinning/DefaultLegacySkin.cs
+++ b/osu.Game/Skinning/DefaultLegacySkin.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Skinning
public static SkinInfo CreateInfo() => new SkinInfo
{
ID = Skinning.SkinInfo.CLASSIC_SKIN, // this is temporary until database storage is decided upon.
- Name = "osu!classic",
+ Name = "osu! \"classic\" (2013)",
Creator = "team osu!",
Protected = true,
InstantiationInfo = typeof(DefaultLegacySkin).GetInvariantInstantiationInfo()
diff --git a/osu.Game/Skinning/ISkinTransformer.cs b/osu.Game/Skinning/ISkinTransformer.cs
new file mode 100644
index 0000000000..f985b8afcd
--- /dev/null
+++ b/osu.Game/Skinning/ISkinTransformer.cs
@@ -0,0 +1,17 @@
+// 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.Skinning
+{
+ ///
+ /// A skin transformer takes in an and applies transformations to it.
+ /// The most common use case is allowing individual rulesets to add skinnable components without directly coupling to underlying skins.
+ ///
+ public interface ISkinTransformer : ISkin
+ {
+ ///
+ /// The original skin that is being transformed.
+ ///
+ ISkin Skin { get; }
+ }
+}
diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs
index 49914c53aa..0aafdd4db0 100644
--- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs
+++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs
@@ -121,7 +121,7 @@ namespace osu.Game.Skinning
break;
case string when pair.Key.StartsWith("Colour", StringComparison.Ordinal):
- HandleColours(currentConfig, line);
+ HandleColours(currentConfig, line, true);
break;
// Custom sprite paths
diff --git a/osu.Game/Skinning/LegacySkinDecoder.cs b/osu.Game/Skinning/LegacySkinDecoder.cs
index e5f87b3230..11c21d432f 100644
--- a/osu.Game/Skinning/LegacySkinDecoder.cs
+++ b/osu.Game/Skinning/LegacySkinDecoder.cs
@@ -48,7 +48,7 @@ namespace osu.Game.Skinning
// osu!catch section only has colour settings
// so no harm in handling the entire section
case Section.CatchTheBeat:
- HandleColours(skin, line);
+ HandleColours(skin, line, true);
return;
}
diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs
index 8f2526db37..2de1564a5c 100644
--- a/osu.Game/Skinning/LegacySkinTransformer.cs
+++ b/osu.Game/Skinning/LegacySkinTransformer.cs
@@ -1,14 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
-using System;
-using JetBrains.Annotations;
using osu.Framework.Audio.Sample;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Legacy;
using static osu.Game.Skinning.SkinConfiguration;
@@ -18,27 +11,14 @@ namespace osu.Game.Skinning
///
/// Transformer used to handle support of legacy features for individual rulesets.
///
- public abstract class LegacySkinTransformer : ISkin
+ public abstract class LegacySkinTransformer : SkinTransformer
{
- ///
- /// The which is being transformed.
- ///
- [NotNull]
- public ISkin Skin { get; }
-
- protected LegacySkinTransformer([NotNull] ISkin skin)
+ protected LegacySkinTransformer(ISkin skin)
+ : base(skin)
{
- Skin = skin ?? throw new ArgumentNullException(nameof(skin));
}
- public virtual Drawable GetDrawableComponent(ISkinComponent component) => Skin.GetDrawableComponent(component);
-
- public Texture GetTexture(string componentName) => GetTexture(componentName, default, default);
-
- public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
- => Skin.GetTexture(componentName, wrapModeS, wrapModeT);
-
- public virtual ISample GetSample(ISampleInfo sampleInfo)
+ public override ISample? GetSample(ISampleInfo sampleInfo)
{
if (!(sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample))
return Skin.GetSample(sampleInfo);
@@ -47,9 +27,7 @@ namespace osu.Game.Skinning
if (legacySample.IsLayered && playLayeredHitSounds?.Value == false)
return new SampleVirtual();
- return Skin.GetSample(sampleInfo);
+ return base.GetSample(sampleInfo);
}
-
- public virtual IBindable GetConfig(TLookup lookup) => Skin.GetConfig(lookup);
}
}
diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
index d5690710bb..6ad5d64e4b 100644
--- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
+++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
@@ -81,7 +81,8 @@ namespace osu.Game.Skinning
}
}
- int lastDefaultSkinIndex = sources.IndexOf(sources.OfType().LastOrDefault());
+ // TODO: check
+ int lastDefaultSkinIndex = sources.IndexOf(sources.OfType().LastOrDefault());
// Ruleset resources should be given the ability to override game-wide defaults
// This is achieved by placing them before the last instance of DefaultSkin.
diff --git a/osu.Game/Skinning/SkinImporter.cs b/osu.Game/Skinning/SkinImporter.cs
index f270abd163..701dcdfc2d 100644
--- a/osu.Game/Skinning/SkinImporter.cs
+++ b/osu.Game/Skinning/SkinImporter.cs
@@ -232,6 +232,9 @@ namespace osu.Game.Skinning
{
skin.SkinInfo.PerformWrite(s =>
{
+ // Update for safety
+ s.InstantiationInfo = skin.GetType().GetInvariantInstantiationInfo();
+
// Serialise out the SkinInfo itself.
string skinInfoJson = JsonConvert.SerializeObject(s, new JsonSerializerSettings { Formatting = Formatting.Indented });
diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs
index bf3cf77257..d051149155 100644
--- a/osu.Game/Skinning/SkinInfo.cs
+++ b/osu.Game/Skinning/SkinInfo.cs
@@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Newtonsoft.Json;
-using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.IO;
@@ -19,7 +18,8 @@ namespace osu.Game.Skinning
[JsonObject(MemberSerialization.OptIn)]
public class SkinInfo : RealmObject, IHasRealmFiles, IEquatable, IHasGuidPrimaryKey, ISoftDelete, IHasNamedFiles
{
- internal static readonly Guid DEFAULT_SKIN = new Guid("2991CFD8-2140-469A-BCB9-2EC23FBCE4AD");
+ internal static readonly Guid TRIANGLES_SKIN = new Guid("2991CFD8-2140-469A-BCB9-2EC23FBCE4AD");
+ internal static readonly Guid ARGON_SKIN = new Guid("CFFA69DE-B3E3-4DEE-8563-3C4F425C05D0");
internal static readonly Guid CLASSIC_SKIN = new Guid("81F02CD3-EEC6-4865-AC23-FAE26A386187");
internal static readonly Guid RANDOM_SKIN = new Guid("D39DFEFB-477C-4372-B1EA-2BCEA5FB8908");
@@ -45,7 +45,16 @@ namespace osu.Game.Skinning
var type = string.IsNullOrEmpty(InstantiationInfo)
// handle the case of skins imported before InstantiationInfo was added.
? typeof(LegacySkin)
- : Type.GetType(InstantiationInfo).AsNonNull();
+ : Type.GetType(InstantiationInfo);
+
+ if (type == null)
+ {
+ // Since the class was renamed from "DefaultSkin" to "TrianglesSkin", the type retrieval would fail
+ // for user modified skins. This aims to amicably handle that.
+ // If we ever add more default skins in the future this will need some kind of proper migration rather than
+ // a single fallback.
+ return new TrianglesSkin(this, resources);
+ }
return (Skin)Activator.CreateInstance(type, this, resources);
}
diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs
index 7ffea3b54f..0e66278fc0 100644
--- a/osu.Game/Skinning/SkinManager.cs
+++ b/osu.Game/Skinning/SkinManager.cs
@@ -39,6 +39,11 @@ namespace osu.Game.Skinning
[ExcludeFromDynamicCompile]
public class SkinManager : ModelManager, ISkinSource, IStorageResourceProvider, IModelImporter
{
+ ///
+ /// The default "classic" skin.
+ ///
+ public Skin DefaultClassicSkin { get; }
+
private readonly AudioManager audio;
private readonly Scheduler scheduler;
@@ -49,24 +54,15 @@ namespace osu.Game.Skinning
public readonly Bindable CurrentSkin = new Bindable();
- public readonly Bindable> CurrentSkinInfo = new Bindable>(Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged())
- {
- Default = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged()
- };
+ public readonly Bindable> CurrentSkinInfo = new Bindable>(ArgonSkin.CreateInfo().ToLiveUnmanaged());
private readonly SkinImporter skinImporter;
private readonly IResourceStore userFiles;
- ///
- /// The default skin.
- ///
- public Skin DefaultSkin { get; }
+ private Skin argonSkin { get; }
- ///
- /// The default legacy skin.
- ///
- public Skin DefaultLegacySkin { get; }
+ private Skin trianglesSkin { get; }
public SkinManager(Storage storage, RealmAccess realm, GameHost host, IResourceStore resources, AudioManager audio, Scheduler scheduler)
: base(storage, realm)
@@ -85,8 +81,9 @@ namespace osu.Game.Skinning
var defaultSkins = new[]
{
- DefaultLegacySkin = new DefaultLegacySkin(this),
- DefaultSkin = new DefaultSkin(this),
+ DefaultClassicSkin = new DefaultLegacySkin(this),
+ trianglesSkin = new TrianglesSkin(this),
+ argonSkin = new ArgonSkin(this),
};
// Ensure the default entries are present.
@@ -104,7 +101,7 @@ namespace osu.Game.Skinning
CurrentSkin.Value = skin.NewValue.PerformRead(GetSkin);
};
- CurrentSkin.Value = DefaultSkin;
+ CurrentSkin.Value = argonSkin;
CurrentSkin.ValueChanged += skin =>
{
if (!skin.NewValue.SkinInfo.Equals(CurrentSkinInfo.Value))
@@ -125,7 +122,7 @@ namespace osu.Game.Skinning
if (randomChoices.Length == 0)
{
- CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged();
+ CurrentSkinInfo.Value = ArgonSkin.CreateInfo().ToLiveUnmanaged();
return;
}
@@ -229,11 +226,15 @@ namespace osu.Game.Skinning
{
yield return CurrentSkin.Value;
- if (CurrentSkin.Value is LegacySkin && CurrentSkin.Value != DefaultLegacySkin)
- yield return DefaultLegacySkin;
+ // Skin manager provides default fallbacks.
+ // This handles cases where a user skin doesn't have the required resources for complete display of
+ // certain elements.
- if (CurrentSkin.Value != DefaultSkin)
- yield return DefaultSkin;
+ if (CurrentSkin.Value is LegacySkin && CurrentSkin.Value != DefaultClassicSkin)
+ yield return DefaultClassicSkin;
+
+ if (CurrentSkin.Value != trianglesSkin)
+ yield return trianglesSkin;
}
}
@@ -294,7 +295,7 @@ namespace osu.Game.Skinning
Guid currentUserSkin = CurrentSkinInfo.Value.ID;
if (items.Any(s => s.ID == currentUserSkin))
- scheduler.Add(() => CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged());
+ scheduler.Add(() => CurrentSkinInfo.Value = ArgonSkin.CreateInfo().ToLiveUnmanaged());
Delete(items.ToList(), silent);
});
@@ -310,10 +311,10 @@ namespace osu.Game.Skinning
if (skinInfo == null)
{
if (guid == SkinInfo.CLASSIC_SKIN)
- skinInfo = DefaultLegacySkin.SkinInfo;
+ skinInfo = DefaultClassicSkin.SkinInfo;
}
- CurrentSkinInfo.Value = skinInfo ?? DefaultSkin.SkinInfo;
+ CurrentSkinInfo.Value = skinInfo ?? trianglesSkin.SkinInfo;
}
}
}
diff --git a/osu.Game/Skinning/SkinTransformer.cs b/osu.Game/Skinning/SkinTransformer.cs
new file mode 100644
index 0000000000..4da60f1e43
--- /dev/null
+++ b/osu.Game/Skinning/SkinTransformer.cs
@@ -0,0 +1,32 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Audio.Sample;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Audio;
+
+namespace osu.Game.Skinning
+{
+ public abstract class SkinTransformer : ISkinTransformer
+ {
+ public ISkin Skin { get; }
+
+ protected SkinTransformer(ISkin skin)
+ {
+ Skin = skin ?? throw new ArgumentNullException(nameof(skin));
+ }
+
+ public virtual Drawable? GetDrawableComponent(ISkinComponent component) => Skin.GetDrawableComponent(component);
+
+ public virtual Texture? GetTexture(string componentName) => GetTexture(componentName, default, default);
+
+ public virtual Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => Skin.GetTexture(componentName, wrapModeS, wrapModeT);
+
+ public virtual ISample? GetSample(ISampleInfo sampleInfo) => Skin.GetSample(sampleInfo);
+
+ public virtual IBindable? GetConfig(TLookup lookup) where TLookup : notnull where TValue : notnull => Skin.GetConfig(lookup);
+ }
+}
diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs
index 57beb6e803..5a39121b16 100644
--- a/osu.Game/Skinning/SkinnableSprite.cs
+++ b/osu.Game/Skinning/SkinnableSprite.cs
@@ -100,7 +100,7 @@ namespace osu.Game.Skinning
{
foreach (var skin in skins)
{
- if (skin is LegacySkinTransformer transformer && isUserSkin(transformer.Skin))
+ if (skin is ISkinTransformer transformer && isUserSkin(transformer.Skin))
return transformer.Skin;
if (isUserSkin(skin))
@@ -112,7 +112,8 @@ namespace osu.Game.Skinning
// Temporarily used to exclude undesirable ISkin implementations
static bool isUserSkin(ISkin skin)
- => skin.GetType() == typeof(DefaultSkin)
+ => skin.GetType() == typeof(TrianglesSkin)
+ || skin.GetType() == typeof(ArgonSkin)
|| skin.GetType() == typeof(DefaultLegacySkin)
|| skin.GetType() == typeof(LegacySkin);
}
diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/TrianglesSkin.cs
similarity index 95%
rename from osu.Game/Skinning/DefaultSkin.cs
rename to osu.Game/Skinning/TrianglesSkin.cs
index f10e8412b1..2c70963524 100644
--- a/osu.Game/Skinning/DefaultSkin.cs
+++ b/osu.Game/Skinning/TrianglesSkin.cs
@@ -22,26 +22,26 @@ using osuTK.Graphics;
namespace osu.Game.Skinning
{
- public class DefaultSkin : Skin
+ public class TrianglesSkin : Skin
{
public static SkinInfo CreateInfo() => new SkinInfo
{
- ID = osu.Game.Skinning.SkinInfo.DEFAULT_SKIN,
- Name = "osu! (triangles)",
+ ID = osu.Game.Skinning.SkinInfo.TRIANGLES_SKIN,
+ Name = "osu! \"triangles\" (2017)",
Creator = "team osu!",
Protected = true,
- InstantiationInfo = typeof(DefaultSkin).GetInvariantInstantiationInfo()
+ InstantiationInfo = typeof(TrianglesSkin).GetInvariantInstantiationInfo()
};
private readonly IStorageResourceProvider resources;
- public DefaultSkin(IStorageResourceProvider resources)
+ public TrianglesSkin(IStorageResourceProvider resources)
: this(CreateInfo(), resources)
{
}
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)]
- public DefaultSkin(SkinInfo skin, IStorageResourceProvider resources)
+ public TrianglesSkin(SkinInfo skin, IStorageResourceProvider resources)
: base(skin, resources)
{
this.resources = resources;
diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs
index 369a3ee7ba..07e1e86617 100644
--- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs
+++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs
@@ -10,7 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Textures;
using osu.Framework.Utils;
-using osu.Game.Screens.Play;
+using osu.Game.Beatmaps;
using osu.Game.Skinning;
using osuTK;
@@ -91,6 +91,9 @@ namespace osu.Game.Storyboards.Drawables
[Resolved]
private ISkinSource skin { get; set; }
+ [Resolved]
+ private IBeatSyncProvider beatSyncProvider { get; set; }
+
[BackgroundDependencyLoader]
private void load(TextureStore textureStore, Storyboard storyboard)
{
@@ -116,9 +119,6 @@ namespace osu.Game.Storyboards.Drawables
Animation.ApplyTransforms(this);
}
- [Resolved]
- private IGameplayClock gameplayClock { get; set; }
-
protected override void LoadComplete()
{
base.LoadComplete();
@@ -128,7 +128,7 @@ namespace osu.Game.Storyboards.Drawables
//
// In the case of storyboard animations, we want to synchronise with game time perfectly
// so let's get a correct time based on gameplay clock and earliest transform.
- PlaybackPosition = gameplayClock.CurrentTime - Animation.EarliestTransformTime;
+ PlaybackPosition = (beatSyncProvider.Clock?.CurrentTime ?? Clock.CurrentTime) - Animation.EarliestTransformTime;
}
private void skinSourceChanged()
diff --git a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs
index fa7ade2c07..ef4539ba56 100644
--- a/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs
+++ b/osu.Game/Tests/Visual/OnlinePlay/TestRoomRequestsHandler.cs
@@ -135,25 +135,15 @@ namespace osu.Game.Tests.Visual.OnlinePlay
});
return true;
+ case GetBeatmapRequest getBeatmapRequest:
+ {
+ getBeatmapRequest.TriggerSuccess(createResponseBeatmaps(getBeatmapRequest.BeatmapInfo.OnlineID).Single());
+ return true;
+ }
+
case GetBeatmapsRequest getBeatmapsRequest:
{
- var result = new List();
-
- foreach (int id in getBeatmapsRequest.BeatmapIds)
- {
- var baseBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == id);
-
- if (baseBeatmap == null)
- {
- baseBeatmap = new TestBeatmap(new RulesetInfo { OnlineID = 0 }).BeatmapInfo;
- baseBeatmap.OnlineID = id;
- baseBeatmap.BeatmapSet!.OnlineID = id;
- }
-
- result.Add(OsuTestScene.CreateAPIBeatmap(baseBeatmap));
- }
-
- getBeatmapsRequest.TriggerSuccess(new GetBeatmapsResponse { Beatmaps = result });
+ getBeatmapsRequest.TriggerSuccess(new GetBeatmapsResponse { Beatmaps = createResponseBeatmaps(getBeatmapsRequest.BeatmapIds.ToArray()) });
return true;
}
@@ -175,6 +165,27 @@ namespace osu.Game.Tests.Visual.OnlinePlay
}
}
+ List createResponseBeatmaps(params int[] beatmapIds)
+ {
+ var result = new List();
+
+ foreach (int id in beatmapIds)
+ {
+ var baseBeatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == id);
+
+ if (baseBeatmap == null)
+ {
+ baseBeatmap = new TestBeatmap(new RulesetInfo { OnlineID = 0 }).BeatmapInfo;
+ baseBeatmap.OnlineID = id;
+ baseBeatmap.BeatmapSet!.OnlineID = id;
+ }
+
+ result.Add(OsuTestScene.CreateAPIBeatmap(baseBeatmap));
+ }
+
+ return result;
+ }
+
return false;
}
diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs
index a9decbae57..9bad867206 100644
--- a/osu.Game/Tests/Visual/PlayerTestScene.cs
+++ b/osu.Game/Tests/Visual/PlayerTestScene.cs
@@ -7,7 +7,6 @@ using System;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
-using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Rulesets;
@@ -57,7 +56,9 @@ namespace osu.Game.Tests.Visual
protected virtual bool Autoplay => false;
- protected void LoadPlayer()
+ protected void LoadPlayer() => LoadPlayer(Array.Empty());
+
+ protected void LoadPlayer(Mod[] mods)
{
var ruleset = CreatePlayerRuleset();
Ruleset.Value = ruleset.RulesetInfo;
@@ -65,20 +66,21 @@ namespace osu.Game.Tests.Visual
var beatmap = CreateBeatmap(ruleset.RulesetInfo);
Beatmap.Value = CreateWorkingBeatmap(beatmap);
- SelectedMods.Value = Array.Empty();
+
+ SelectedMods.Value = mods;
if (!AllowFail)
{
var noFailMod = ruleset.CreateMod();
if (noFailMod != null)
- SelectedMods.Value = new[] { noFailMod };
+ SelectedMods.Value = SelectedMods.Value.Append(noFailMod).ToArray();
}
if (Autoplay)
{
var mod = ruleset.GetAutoplayMod();
if (mod != null)
- SelectedMods.Value = SelectedMods.Value.Concat(mod.Yield()).ToArray();
+ SelectedMods.Value = SelectedMods.Value.Append(mod).ToArray();
}
Player = CreatePlayer(ruleset);
diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs
index 7278e1e93f..f8f15e2729 100644
--- a/osu.Game/Tests/Visual/SkinnableTestScene.cs
+++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs
@@ -29,8 +29,10 @@ namespace osu.Game.Tests.Visual
{
public abstract class SkinnableTestScene : OsuGridTestScene, IStorageResourceProvider
{
+ private TrianglesSkin trianglesSkin;
private Skin metricsSkin;
- private Skin defaultSkin;
+ private Skin legacySkin;
+ private Skin argonSkin;
private Skin specialSkin;
private Skin oldSkin;
@@ -47,8 +49,10 @@ namespace osu.Game.Tests.Visual
{
var dllStore = new DllResourceStore(GetType().Assembly);
+ argonSkin = new ArgonSkin(this);
+ trianglesSkin = new TrianglesSkin(this);
metricsSkin = new TestLegacySkin(new SkinInfo { Name = "metrics-skin" }, new NamespacedResourceStore(dllStore, "Resources/metrics_skin"), this, true);
- defaultSkin = new DefaultLegacySkin(this);
+ legacySkin = new DefaultLegacySkin(this);
specialSkin = new TestLegacySkin(new SkinInfo { Name = "special-skin" }, new NamespacedResourceStore(dllStore, "Resources/special_skin"), this, true);
oldSkin = new TestLegacySkin(new SkinInfo { Name = "old-skin" }, new NamespacedResourceStore(dllStore, "Resources/old_skin"), this, true);
}
@@ -61,11 +65,12 @@ namespace osu.Game.Tests.Visual
var beatmap = CreateBeatmapForSkinProvider();
- Cell(0).Child = createProvider(null, creationFunction, beatmap);
- Cell(1).Child = createProvider(metricsSkin, creationFunction, beatmap);
- Cell(2).Child = createProvider(defaultSkin, creationFunction, beatmap);
- Cell(3).Child = createProvider(specialSkin, creationFunction, beatmap);
- Cell(4).Child = createProvider(oldSkin, creationFunction, beatmap);
+ Cell(0).Child = createProvider(argonSkin, creationFunction, beatmap);
+ Cell(1).Child = createProvider(trianglesSkin, creationFunction, beatmap);
+ Cell(2).Child = createProvider(metricsSkin, creationFunction, beatmap);
+ Cell(3).Child = createProvider(legacySkin, creationFunction, beatmap);
+ Cell(4).Child = createProvider(specialSkin, creationFunction, beatmap);
+ Cell(5).Child = createProvider(oldSkin, creationFunction, beatmap);
}
protected IEnumerable CreatedDrawables => createdDrawables;
@@ -80,10 +85,7 @@ namespace osu.Game.Tests.Visual
OutlineBox outlineBox;
SkinProvidingContainer skinProvider;
- ISkin provider = skin;
-
- if (provider is LegacySkin legacyProvider)
- provider = Ruleset.Value.CreateInstance().CreateSkinTransformer(legacyProvider, beatmap);
+ ISkin provider = Ruleset.Value.CreateInstance().CreateSkinTransformer(skin, beatmap) ?? skin;
var children = new Container
{
@@ -102,7 +104,7 @@ namespace osu.Game.Tests.Visual
},
new OsuSpriteText
{
- Text = skin?.SkinInfo.Value.Name ?? "none",
+ Text = skin.SkinInfo.Value.Name,
Scale = new Vector2(1.5f),
Padding = new MarginPadding(5),
},
@@ -138,6 +140,7 @@ namespace osu.Game.Tests.Visual
{
c.RelativeSizeAxes = Axes.None;
c.AutoSizeAxes = Axes.None;
+ c.Size = Vector2.Zero;
c.RelativeSizeAxes = !autoSize ? Axes.Both : Axes.None;
c.AutoSizeAxes = autoSize ? Axes.Both : Axes.None;
diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs
index 100464029b..7b540cb564 100644
--- a/osu.Game/Updater/UpdateManager.cs
+++ b/osu.Game/Updater/UpdateManager.cs
@@ -156,6 +156,7 @@ namespace osu.Game.Updater
switch (State)
{
case ProgressNotificationState.Cancelled:
+ case ProgressNotificationState.Completed:
base.Close(runFlingAnimation);
break;
}
diff --git a/osu.Game/Users/Drawables/DrawableAvatar.cs b/osu.Game/Users/Drawables/DrawableAvatar.cs
index 483106d3f4..155f63dc18 100644
--- a/osu.Game/Users/Drawables/DrawableAvatar.cs
+++ b/osu.Game/Users/Drawables/DrawableAvatar.cs
@@ -14,13 +14,13 @@ namespace osu.Game.Users.Drawables
[LongRunningLoad]
public class DrawableAvatar : Sprite
{
- private readonly APIUser user;
+ private readonly IUser user;
///
/// A simple, non-interactable avatar sprite for the specified user.
///
/// The user. A null value will get a placeholder avatar.
- public DrawableAvatar(APIUser user = null)
+ public DrawableAvatar(IUser user = null)
{
this.user = user;
@@ -33,10 +33,10 @@ namespace osu.Game.Users.Drawables
[BackgroundDependencyLoader]
private void load(LargeTextureStore textures)
{
- if (user != null && user.Id > 1)
+ if (user != null && user.OnlineID > 1)
// TODO: The fallback here should not need to exist. Users should be looked up and populated via UserLookupCache or otherwise
// in remaining cases where this is required (chat tabs, local leaderboard), at which point this should be removed.
- Texture = textures.Get(user.AvatarUrl ?? $@"https://a.ppy.sh/{user.Id}");
+ Texture = textures.Get((user as APIUser)?.AvatarUrl ?? $@"https://a.ppy.sh/{user.OnlineID}");
Texture ??= textures.Get(@"Online/avatar-guest");
}
diff --git a/osu.Game/Utils/HumanizerUtils.cs b/osu.Game/Utils/HumanizerUtils.cs
index 5b7c3630d9..0da346ed73 100644
--- a/osu.Game/Utils/HumanizerUtils.cs
+++ b/osu.Game/Utils/HumanizerUtils.cs
@@ -4,6 +4,7 @@
using System;
using System.Globalization;
using Humanizer;
+using Humanizer.Localisation;
namespace osu.Game.Utils
{
@@ -26,5 +27,27 @@ namespace osu.Game.Utils
return input.Humanize(culture: new CultureInfo("en-US"));
}
}
+
+ ///
+ /// Turns the current or provided timespan into a human readable sentence
+ ///
+ /// The date to be humanized
+ /// The maximum number of time units to return. Defaulted is 1 which means the largest unit is returned
+ /// The maximum unit of time to output. The default value is . The time units and will give approximations for time spans bigger 30 days by calculating with 365.2425 days a year and 30.4369 days a month.
+ /// The minimum unit of time to output.
+ /// Uses words instead of numbers if true. E.g. one day.
+ /// distance of time in words
+ public static string Humanize(TimeSpan input, int precision = 1, TimeUnit maxUnit = TimeUnit.Week, TimeUnit minUnit = TimeUnit.Millisecond, bool toWords = false)
+ {
+ // this works around https://github.com/xamarin/xamarin-android/issues/2012 and https://github.com/Humanizr/Humanizer/issues/690#issuecomment-368536282
+ try
+ {
+ return input.Humanize(precision: precision, maxUnit: maxUnit, minUnit: minUnit);
+ }
+ catch (ArgumentException)
+ {
+ return input.Humanize(culture: new CultureInfo("en-US"), precision: precision, maxUnit: maxUnit, minUnit: minUnit);
+ }
+ }
}
}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 29e690a024..df7bfab17a 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -35,7 +35,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 83410b08f6..9e2568bf7e 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -61,7 +61,7 @@
-
+
@@ -82,7 +82,7 @@
-
+