diff --git a/osu.Android.props b/osu.Android.props
index 2c186a52dd..77c29a5d6e 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,7 +52,7 @@
-
+
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/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.Osu.Tests/Mods/TestSceneOsuModAlternate.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs
index 5e46498aca..521c10c10c 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAlternate.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
@@ -125,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
/// Ensures alternation is reset before the first hitobject after a break.
///
[Test]
- public void TestInputSingularWithBreak() => CreateModTest(new ModTestData
+ public void TestInputSingularWithBreak([Values] bool pressBeforeSecondObject) => CreateModTest(new ModTestData
{
Mod = new OsuModAlternate(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 2,
@@ -155,21 +156,26 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
},
}
},
- ReplayFrames = new List
+ ReplayFrames = new ReplayFrame[]
{
// first press to start alternate lock.
- new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
- new OsuReplayFrame(501, new Vector2(100)),
- // press same key after break but before hit object.
- new OsuReplayFrame(2250, new Vector2(300, 100), OsuAction.LeftButton),
- new OsuReplayFrame(2251, new Vector2(300, 100)),
+ new OsuReplayFrame(450, new Vector2(100), OsuAction.LeftButton),
+ new OsuReplayFrame(451, new Vector2(100)),
// press same key at second hitobject and ensure it has been hit.
- new OsuReplayFrame(2500, new Vector2(500, 100), OsuAction.LeftButton),
- new OsuReplayFrame(2501, new Vector2(500, 100)),
+ new OsuReplayFrame(2450, new Vector2(500, 100), OsuAction.LeftButton),
+ new OsuReplayFrame(2451, new Vector2(500, 100)),
// press same key at third hitobject and ensure it has been missed.
- new OsuReplayFrame(3000, new Vector2(500, 100), OsuAction.LeftButton),
- new OsuReplayFrame(3001, new Vector2(500, 100)),
- }
+ new OsuReplayFrame(2950, new Vector2(500, 100), OsuAction.LeftButton),
+ new OsuReplayFrame(2951, new Vector2(500, 100)),
+ }.Concat(!pressBeforeSecondObject
+ ? Enumerable.Empty()
+ : new ReplayFrame[]
+ {
+ // press same key after break but before hit object.
+ new OsuReplayFrame(2250, new Vector2(300, 100), OsuAction.LeftButton),
+ new OsuReplayFrame(2251, new Vector2(300, 100)),
+ }
+ ).ToList()
});
}
}
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/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/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/Mods/InputBlockingMod.cs b/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs
index a7aca8257b..e4e8905722 100644
--- a/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs
+++ b/osu.Game.Rulesets.Osu/Mods/InputBlockingMod.cs
@@ -18,7 +18,7 @@ using osu.Game.Utils;
namespace osu.Game.Rulesets.Osu.Mods
{
- public abstract class InputBlockingMod : Mod, IApplicableToDrawableRuleset
+ public abstract class InputBlockingMod : Mod, IApplicableToDrawableRuleset, IUpdatableByPlayfield
{
public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax), typeof(OsuModCinema) };
@@ -62,15 +62,18 @@ namespace osu.Game.Rulesets.Osu.Mods
gameplayClock = drawableRuleset.FrameStableClock;
}
+ public void Update(Playfield playfield)
+ {
+ if (LastAcceptedAction != null && nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
+ LastAcceptedAction = null;
+ }
+
protected abstract bool CheckValidNewAction(OsuAction action);
private bool checkCorrectAction(OsuAction action)
{
if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
- {
- LastAcceptedAction = null;
return true;
- }
switch (action)
{
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
index 0306c99fd5..1a9d12e860 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
@@ -1,9 +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.Diagnostics;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects;
@@ -21,32 +20,36 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
public const int SPACING = 32;
public const double PREEMPT = 800;
- public DrawablePool Pool;
+ public DrawablePool? Pool { private get; set; }
protected override void OnApply(FollowPointLifetimeEntry entry)
{
base.OnApply(entry);
- entry.Invalidated += onEntryInvalidated;
- refreshPoints();
+ entry.Invalidated += scheduleRefresh;
+
+ // Our clock may not be correct at this point if `LoadComplete` has not run yet.
+ // Without a schedule, animations referencing FollowPoint's clock (see `IAnimationTimeReference`) would be incorrect on first pool usage.
+ scheduleRefresh();
}
protected override void OnFree(FollowPointLifetimeEntry entry)
{
base.OnFree(entry);
- entry.Invalidated -= onEntryInvalidated;
+ entry.Invalidated -= scheduleRefresh;
// Return points to the pool.
ClearInternal(false);
}
- private void onEntryInvalidated() => Scheduler.AddOnce(refreshPoints);
-
- private void refreshPoints()
+ private void scheduleRefresh() => Scheduler.AddOnce(() =>
{
+ Debug.Assert(Pool != null);
+
ClearInternal(false);
var entry = Entry;
+
if (entry?.End == null) return;
OsuHitObject start = entry.Start;
@@ -95,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
}
entry.LifetimeEnd = finalTransformEndTime;
- }
+ });
///
/// Computes the fade time of follow point positioned between two hitobjects.
diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
index c3c10215a5..3cbc205eaf 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(TrianglesSkin), 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(TrianglesSkin), 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/Visual/Editing/TestSceneZoomableScrollContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
index ce418f33f0..1858aee76b 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
@@ -66,6 +67,18 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("Scroll container is loaded", () => scrollContainer.LoadState >= LoadState.Loaded);
}
+ [Test]
+ public void TestInitialZoomOutOfRange()
+ {
+ AddStep("Invalid ZoomableScrollContainer throws ArgumentException", () =>
+ {
+ Assert.Throws(() =>
+ {
+ _ = new ZoomableScrollContainer(1, 60, 0);
+ });
+ });
+ }
+
[Test]
public void TestWidthInitialization()
{
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/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs
index 663e398c01..72656c29b1 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,52 @@ 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)));
}
[Test]
public void TestPlayerScore()
{
+ createLeaderboard();
+ addLocalPlayer();
+
var player2Score = new BindableDouble(1234567);
var player3Score = new BindableDouble(1111111);
@@ -73,6 +90,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 +100,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 +112,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 +129,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/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/Navigation/TestSceneEditDefaultSkin.cs b/osu.Game.Tests/Visual/Navigation/TestSceneEditDefaultSkin.cs
index 4ca55e8744..3757c49f18 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.TRIANGLES_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.TRIANGLES_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/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs
index e978b57ba4..b314d95597 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();
@@ -45,7 +49,7 @@ namespace osu.Game.Tests.Visual.UserInterface
displayedCount = new OsuSpriteText()
};
- notificationOverlay.UnreadCount.ValueChanged += count => { displayedCount.Text = $"displayed count: {count.NewValue}"; };
+ notificationOverlay.UnreadCount.ValueChanged += count => { displayedCount.Text = $"unread count: {count.NewValue}"; };
});
[Test]
@@ -228,6 +232,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()
{
@@ -270,6 +299,8 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("wait completion", () => notification.State == ProgressNotificationState.Completed);
AddAssert("Completion toast shown", () => notificationOverlay.ToastCount == 1);
+ AddUntilStep("wait forwarded", () => notificationOverlay.ToastCount == 0);
+ AddAssert("only one unread", () => notificationOverlay.UnreadCount.Value == 1);
}
[Test]
@@ -297,7 +328,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);
@@ -422,6 +453,14 @@ 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();
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index 5f49557685..54c545e367 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.TRIANGLES_SKIN.ToString());
SetDefault(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Details);
SetDefault(OsuSetting.BeatmapDetailModsFilter, false);
diff --git a/osu.Game/Database/Live.cs b/osu.Game/Database/Live.cs
index 52e1d420f7..3bb11c3a50 100644
--- a/osu.Game/Database/Live.cs
+++ b/osu.Game/Database/Live.cs
@@ -51,7 +51,15 @@ namespace osu.Game.Database
ID = id;
}
- public bool Equals(Live? other) => ID == other?.ID;
+ public bool Equals(Live? other)
+ {
+ if (ReferenceEquals(this, other)) return true;
+ if (other == null) return false;
+
+ return ID == other.ID;
+ }
+
+ public override int GetHashCode() => HashCode.Combine(ID);
public override string ToString() => PerformRead(i => i.ToString());
}
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/Online/API/Requests/Responses/APIUserScoreAggregate.cs b/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs
index 30433ab8cd..c870157fec 100644
--- a/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs
@@ -10,6 +10,9 @@ using osu.Game.Scoring;
namespace osu.Game.Online.API.Requests.Responses
{
+ ///
+ /// Represents an aggregate score for a user based off all beatmaps that have been played in the playlist.
+ ///
public class APIUserScoreAggregate
{
[JsonProperty("attempts")]
diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs
index 2f3ece0e3b..f51f57c031 100644
--- a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs
+++ b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs
@@ -15,6 +15,8 @@ using osu.Framework.Localisation;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
+using osu.Framework.Bindables;
+using osu.Game.Configuration;
namespace osu.Game.Online.Leaderboards
{
@@ -24,6 +26,7 @@ namespace osu.Game.Online.Leaderboards
private FillFlowContainer topScoreStatistics = null!;
private FillFlowContainer bottomScoreStatistics = null!;
private FillFlowContainer modStatistics = null!;
+ private readonly Bindable prefer24HourTime = new Bindable();
public LeaderboardScoreTooltip()
{
@@ -36,8 +39,9 @@ namespace osu.Game.Online.Leaderboards
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load(OsuColour colours, OsuConfigManager configManager)
{
+ configManager.BindWith(OsuSetting.Prefer24HourTime, prefer24HourTime);
InternalChildren = new Drawable[]
{
new Box
@@ -92,6 +96,13 @@ namespace osu.Game.Online.Leaderboards
};
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ prefer24HourTime.BindValueChanged(_ => updateTimestampLabel(), true);
+ }
+
private ScoreInfo? displayedScore;
public void SetContent(ScoreInfo score)
@@ -101,7 +112,7 @@ namespace osu.Game.Online.Leaderboards
displayedScore = score;
- timestampLabel.Text = $"Played on {score.Date.ToLocalTime():d MMMM yyyy HH:mm}";
+ updateTimestampLabel();
modStatistics.Clear();
topScoreStatistics.Clear();
@@ -121,6 +132,16 @@ namespace osu.Game.Online.Leaderboards
}
}
+ private void updateTimestampLabel()
+ {
+ if (displayedScore != null)
+ {
+ timestampLabel.Text = prefer24HourTime.Value
+ ? $"Played on {displayedScore.Date.ToLocalTime():d MMMM yyyy HH:mm}"
+ : $"Played on {displayedScore.Date.ToLocalTime():d MMMM yyyy h:mm tt}";
+ }
+ }
+
protected override void PopIn() => this.FadeIn(20, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(80, Easing.OutQuint);
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/OsuGame.cs b/osu.Game/OsuGame.cs
index 9e2384322a..f1da492c05 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -56,14 +56,12 @@ using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
-using osu.Game.Skinning;
using osu.Game.Skinning.Editor;
using osu.Game.Updater;
using osu.Game.Users;
using osu.Game.Utils;
using osuTK.Graphics;
using Sentry;
-using Logger = osu.Framework.Logging.Logger;
namespace osu.Game
{
@@ -294,25 +292,13 @@ namespace osu.Game
Ruleset.ValueChanged += r => configRuleset.Value = r.NewValue.ShortName;
- // bind config int to database SkinInfo
configSkin = LocalConfig.GetBindable(OsuSetting.Skin);
+
+ // Transfer skin from config to realm instance once on startup.
+ SkinManager.SetSkinFromConfiguration(configSkin.Value);
+
+ // Transfer any runtime changes back to configuration file.
SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID.ToString();
- configSkin.ValueChanged += skinId =>
- {
- Live skinInfo = null;
-
- if (Guid.TryParse(skinId.NewValue, out var guid))
- skinInfo = SkinManager.Query(s => s.ID == guid);
-
- if (skinInfo == null)
- {
- if (guid == SkinInfo.CLASSIC_SKIN)
- skinInfo = DefaultLegacySkin.CreateInfo().ToLiveUnmanaged();
- }
-
- SkinManager.CurrentSkinInfo.Value = skinInfo ?? DefaultSkin.CreateInfo().ToLiveUnmanaged();
- };
- configSkin.TriggerChange();
IsActive.BindValueChanged(active => updateActiveState(active.NewValue), true);
@@ -730,6 +716,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";
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index b30a065371..8b016e8eb0 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -179,7 +179,7 @@ namespace osu.Game
private SpectatorClient spectatorClient;
- private MultiplayerClient multiplayerClient;
+ protected MultiplayerClient MultiplayerClient { get; private set; }
private MetadataClient metadataClient;
@@ -284,7 +284,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));
@@ -329,7 +329,7 @@ namespace osu.Game
AddInternal(apiAccess);
AddInternal(spectatorClient);
- AddInternal(multiplayerClient);
+ AddInternal(MultiplayerClient);
AddInternal(metadataClient);
AddInternal(rulesetConfigCache);
diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs
index 15573f99af..fad8afd371 100644
--- a/osu.Game/Overlays/NotificationOverlay.cs
+++ b/osu.Game/Overlays/NotificationOverlay.cs
@@ -210,14 +210,14 @@ namespace osu.Game.Overlays
mainContent.FadeTo(0, TRANSITION_LENGTH, Easing.OutQuint);
}
- private void notificationClosed()
+ private void notificationClosed() => Schedule(() =>
{
updateCounts();
// this debounce is currently shared between popin/popout sounds, which means one could potentially not play when the user is expecting it.
// popout is constant across all notification types, and should therefore be handled using playback concurrency instead, but seems broken at the moment.
playDebouncedSample("UI/overlay-pop-out");
- }
+ });
private void playDebouncedSample(string sampleName)
{
diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs
index 885bb2fc6c..ea654e1272 100644
--- a/osu.Game/Overlays/Notifications/Notification.cs
+++ b/osu.Game/Overlays/Notifications/Notification.cs
@@ -26,7 +26,8 @@ namespace osu.Game.Overlays.Notifications
public abstract class Notification : Container
{
///
- /// User requested close.
+ /// Notification was closed, either by user or otherwise.
+ /// Importantly, this event may be fired from a non-update thread.
///
public event Action? Closed;
@@ -55,6 +56,8 @@ namespace osu.Game.Overlays.Notifications
protected Container IconContent;
+ public bool WasClosed { get; private set; }
+
private readonly Container content;
protected override Container Content => content;
@@ -226,8 +229,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;
@@ -245,21 +248,23 @@ namespace osu.Game.Overlays.Notifications
initialFlash.FadeOutFromOne(2000, Easing.OutQuart);
}
- public bool WasClosed;
-
public virtual void Close(bool runFlingAnimation)
{
if (WasClosed) return;
WasClosed = true;
- if (runFlingAnimation && dragContainer.FlingLeft())
- this.FadeOut(600, Easing.In);
- else
- this.FadeOut(100);
-
Closed?.Invoke();
- Expire();
+
+ Schedule(() =>
+ {
+ if (runFlingAnimation && dragContainer.FlingLeft())
+ this.FadeOut(600, Easing.In);
+ else
+ this.FadeOut(100);
+
+ Expire();
+ });
}
private class DragContainer : Container
diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs
index c4d402e5b9..61bb22041e 100644
--- a/osu.Game/Overlays/Notifications/ProgressNotification.cs
+++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs
@@ -142,7 +142,6 @@ namespace osu.Game.Overlays.Notifications
case ProgressNotificationState.Completed:
loadingSpinner.Hide();
attemptPostCompletion();
- base.Close(false);
break;
}
}
@@ -166,6 +165,8 @@ namespace osu.Game.Overlays.Notifications
CompletionTarget.Invoke(CreateCompletionNotification());
completionSent = true;
+
+ Close(false);
}
private ProgressNotificationState state;
@@ -239,6 +240,7 @@ namespace osu.Game.Overlays.Notifications
{
switch (State)
{
+ case ProgressNotificationState.Completed:
case ProgressNotificationState.Cancelled:
base.Close(runFlingAnimation);
break;
diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
index d23ef7e3e7..8d491a980a 100644
--- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
@@ -14,7 +14,6 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Framework.Platform;
-using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
@@ -36,9 +35,6 @@ namespace osu.Game.Overlays.Settings.Sections
Icon = FontAwesome.Solid.PaintBrush
};
- private readonly Bindable> dropdownBindable = new Bindable> { Default = DefaultSkin.CreateInfo().ToLiveUnmanaged() };
- private readonly Bindable configBindable = new Bindable();
-
private static readonly Live random_skin_info = new SkinInfo
{
ID = SkinInfo.RANDOM_SKIN,
@@ -56,13 +52,14 @@ namespace osu.Game.Overlays.Settings.Sections
private IDisposable realmSubscription;
[BackgroundDependencyLoader(permitNulls: true)]
- private void load(OsuConfigManager config, [CanBeNull] SkinEditorOverlay skinEditor)
+ private void load([CanBeNull] SkinEditorOverlay skinEditor)
{
Children = new Drawable[]
{
skinDropdown = new SkinSettingsDropdown
{
LabelText = SkinSettingsStrings.CurrentSkin,
+ Current = skins.CurrentSkinInfo,
Keywords = new[] { @"skins" }
},
new SettingsButton
@@ -73,47 +70,27 @@ namespace osu.Game.Overlays.Settings.Sections
new ExportSkinButton(),
new DeleteSkinButton(),
};
-
- config.BindWith(OsuSetting.Skin, configBindable);
}
protected override void LoadComplete()
{
base.LoadComplete();
- skinDropdown.Current = dropdownBindable;
-
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);
- configBindable.BindValueChanged(_ => Scheduler.AddOnce(updateSelectedSkinFromConfig));
-
- dropdownBindable.BindValueChanged(dropdownSelectionChanged);
- }
-
- private void dropdownSelectionChanged(ValueChangedEvent> skin)
- {
- // Only handle cases where it's clear the user has intent to change skins.
- if (skin.OldValue == null) return;
-
- if (skin.NewValue.Equals(random_skin_info))
+ skinDropdown.Current.BindValueChanged(skin =>
{
- var skinBefore = skins.CurrentSkinInfo.Value;
-
- skins.SelectRandomSkin();
-
- if (skinBefore == skins.CurrentSkinInfo.Value)
+ if (skin.NewValue == random_skin_info)
{
- // the random selection didn't change the skin, so we should manually update the dropdown to match.
- dropdownBindable.Value = skins.CurrentSkinInfo.Value;
+ // before selecting random, set the skin back to the previous selection.
+ // this is done because at this point it will be random_skin_info, and would
+ // cause SelectRandomSkin to be unable to skip the previous selection.
+ skins.CurrentSkinInfo.Value = skin.OldValue;
+ skins.SelectRandomSkin();
}
-
- return;
- }
-
- configBindable.Value = skin.NewValue.ID.ToString();
+ });
}
private void skinsChanged(IRealmCollection sender, ChangeSet changes, Exception error)
@@ -123,34 +100,19 @@ 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.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;
-
- updateSelectedSkinFromConfig();
- });
- }
-
- private void updateSelectedSkinFromConfig()
- {
- if (!skinDropdown.Items.Any())
- return;
-
- Live skin = null;
-
- if (Guid.TryParse(configBindable.Value, out var configId))
- skin = skinDropdown.Items.FirstOrDefault(s => s.ID == configId);
-
- dropdownBindable.Value = skin ?? skinDropdown.Items.First();
+ Schedule(() => skinDropdown.Items = dropdownItems);
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs
index 558605efc3..6d7706cde2 100644
--- a/osu.Game/Rulesets/Mods/ModFlashlight.cs
+++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs
@@ -150,9 +150,9 @@ namespace osu.Game.Rulesets.Mods
if (comboBasedSize)
{
- if (combo > 200)
+ if (combo >= 200)
size *= 0.8f;
- else if (combo > 100)
+ else if (combo >= 100)
size *= 0.9f;
}
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..dbc4e2b2e1 100644
--- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
+++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
@@ -130,7 +130,7 @@ 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)
+ if (skin.Value is TrianglesSkin || skin.Value is DefaultLegacySkin)
break;
newBackground = new SkinBackground(skin.Value, getBackgroundTextureName());
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
index 9e96a7386d..721f0c4e3b 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
@@ -196,7 +196,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
defaultTimelineZoom = getZoomLevelForVisibleMilliseconds(6000);
- float initialZoom = (float)(defaultTimelineZoom * editorBeatmap.BeatmapInfo.TimelineZoom);
+ float initialZoom = (float)(defaultTimelineZoom * (editorBeatmap.BeatmapInfo.TimelineZoom == 0 ? 1 : editorBeatmap.BeatmapInfo.TimelineZoom));
float minimumZoom = getZoomLevelForVisibleMilliseconds(10000);
float maximumZoom = getZoomLevelForVisibleMilliseconds(500);
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs
index 7d51284f46..0fb59a8a1f 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs
@@ -87,6 +87,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (minimum > maximum)
throw new ArgumentException($"{nameof(minimum)} ({minimum}) must be less than {nameof(maximum)} ({maximum})");
+ if (initial < minimum || initial > maximum)
+ throw new ArgumentException($"{nameof(initial)} ({initial}) must be between {nameof(minimum)} ({minimum}) and {nameof(maximum)} ({maximum})");
+
minZoom = minimum;
maxZoom = maximum;
CurrentZoom = zoomTarget = initial;
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs
index 1f80c47d13..5a297f18db 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs
@@ -110,6 +110,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (Client != null)
{
Client.RoomUpdated -= invokeOnRoomUpdated;
+ Client.LoadRequested -= invokeOnRoomLoadRequested;
Client.UserLeft -= invokeUserLeft;
Client.UserKicked -= invokeUserKicked;
Client.UserJoined -= invokeUserJoined;
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/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs
index 325253e9d4..64aa8bed03 100644
--- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs
+++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs
@@ -92,6 +92,7 @@ namespace osu.Game.Screens.Play.HUD
int displayCount = Math.Min(Flow.Count, max_panels);
Height = displayCount * (GameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y);
+ Flow.Margin = new MarginPadding { Bottom = Height };
requiresScroll = displayCount != Flow.Count;
return drawable;
@@ -114,7 +115,7 @@ namespace osu.Game.Screens.Play.HUD
if (requiresScroll && trackedScore != null)
{
float scrollTarget = scroll.GetChildPosInContent(trackedScore) + trackedScore.DrawHeight / 2 - scroll.DrawHeight / 2;
- scroll.ScrollTo(scrollTarget, false);
+ scroll.ScrollTo(scrollTarget);
}
const float panel_height = GameplayLeaderboardScore.PANEL_HEIGHT;
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/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/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/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/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
index d5690710bb..4c35681f66 100644
--- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
+++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs
@@ -81,7 +81,7 @@ namespace osu.Game.Skinning
}
}
- int lastDefaultSkinIndex = sources.IndexOf(sources.OfType().LastOrDefault());
+ 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..04d1216ed6 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,7 @@ 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 CLASSIC_SKIN = new Guid("81F02CD3-EEC6-4865-AC23-FAE26A386187");
internal static readonly Guid RANDOM_SKIN = new Guid("D39DFEFB-477C-4372-B1EA-2BCEA5FB8908");
@@ -45,7 +44,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 f677cebe51..a9a01dfebf 100644
--- a/osu.Game/Skinning/SkinManager.cs
+++ b/osu.Game/Skinning/SkinManager.cs
@@ -49,9 +49,9 @@ namespace osu.Game.Skinning
public readonly Bindable CurrentSkin = new Bindable();
- public readonly Bindable> CurrentSkinInfo = new Bindable>(Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged())
+ public readonly Bindable> CurrentSkinInfo = new Bindable>(TrianglesSkin.CreateInfo().ToLiveUnmanaged())
{
- Default = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged()
+ Default = TrianglesSkin.CreateInfo().ToLiveUnmanaged()
};
private readonly SkinImporter skinImporter;
@@ -59,14 +59,14 @@ namespace osu.Game.Skinning
private readonly IResourceStore userFiles;
///
- /// The default skin.
+ /// The default "triangles" skin.
///
- public Skin DefaultSkin { get; }
+ public Skin DefaultSkinTriangles { get; }
///
- /// The default legacy skin.
+ /// The default "classic" skin.
///
- public Skin DefaultLegacySkin { get; }
+ public Skin DefaultClassicSkin { get; }
public SkinManager(Storage storage, RealmAccess realm, GameHost host, IResourceStore resources, AudioManager audio, Scheduler scheduler)
: base(storage, realm)
@@ -85,8 +85,8 @@ namespace osu.Game.Skinning
var defaultSkins = new[]
{
- DefaultLegacySkin = new DefaultLegacySkin(this),
- DefaultSkin = new DefaultSkin(this),
+ DefaultClassicSkin = new DefaultLegacySkin(this),
+ DefaultSkinTriangles = new TrianglesSkin(this),
};
// Ensure the default entries are present.
@@ -104,7 +104,7 @@ namespace osu.Game.Skinning
CurrentSkin.Value = skin.NewValue.PerformRead(GetSkin);
};
- CurrentSkin.Value = DefaultSkin;
+ CurrentSkin.Value = DefaultSkinTriangles;
CurrentSkin.ValueChanged += skin =>
{
if (!skin.NewValue.SkinInfo.Equals(CurrentSkinInfo.Value))
@@ -119,11 +119,13 @@ namespace osu.Game.Skinning
Realm.Run(r =>
{
// choose from only user skins, removing the current selection to ensure a new one is chosen.
- var randomChoices = r.All().Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID).ToArray();
+ var randomChoices = r.All()
+ .Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID)
+ .ToArray();
if (randomChoices.Length == 0)
{
- CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged();
+ CurrentSkinInfo.Value = TrianglesSkin.CreateInfo().ToLiveUnmanaged();
return;
}
@@ -227,11 +229,11 @@ namespace osu.Game.Skinning
{
yield return CurrentSkin.Value;
- if (CurrentSkin.Value is LegacySkin && CurrentSkin.Value != DefaultLegacySkin)
- yield return DefaultLegacySkin;
+ if (CurrentSkin.Value is LegacySkin && CurrentSkin.Value != DefaultClassicSkin)
+ yield return DefaultClassicSkin;
- if (CurrentSkin.Value != DefaultSkin)
- yield return DefaultSkin;
+ if (CurrentSkin.Value != DefaultSkinTriangles)
+ yield return DefaultSkinTriangles;
}
}
@@ -292,10 +294,26 @@ 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 = TrianglesSkin.CreateInfo().ToLiveUnmanaged());
Delete(items.ToList(), silent);
});
}
+
+ public void SetSkinFromConfiguration(string guidString)
+ {
+ Live skinInfo = null;
+
+ if (Guid.TryParse(guidString, out var guid))
+ skinInfo = Query(s => s.ID == guid);
+
+ if (skinInfo == null)
+ {
+ if (guid == SkinInfo.CLASSIC_SKIN)
+ skinInfo = DefaultClassicSkin.SkinInfo;
+ }
+
+ CurrentSkinInfo.Value = skinInfo ?? DefaultSkinTriangles.SkinInfo;
+ }
}
}
diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs
index 57beb6e803..69454de979 100644
--- a/osu.Game/Skinning/SkinnableSprite.cs
+++ b/osu.Game/Skinning/SkinnableSprite.cs
@@ -112,7 +112,7 @@ 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(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/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..75224742a2 100644
--- a/osu.Game/Tests/Visual/SkinnableTestScene.cs
+++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs
@@ -29,8 +29,9 @@ 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 specialSkin;
private Skin oldSkin;
@@ -47,8 +48,9 @@ namespace osu.Game.Tests.Visual
{
var dllStore = new DllResourceStore(GetType().Assembly);
+ 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,9 +63,9 @@ namespace osu.Game.Tests.Visual
var beatmap = CreateBeatmapForSkinProvider();
- Cell(0).Child = createProvider(null, creationFunction, beatmap);
+ Cell(0).Child = createProvider(trianglesSkin, creationFunction, beatmap);
Cell(1).Child = createProvider(metricsSkin, creationFunction, beatmap);
- Cell(2).Child = createProvider(defaultSkin, creationFunction, beatmap);
+ Cell(2).Child = createProvider(legacySkin, creationFunction, beatmap);
Cell(3).Child = createProvider(specialSkin, creationFunction, beatmap);
Cell(4).Child = createProvider(oldSkin, creationFunction, beatmap);
}
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 fed7c27f07..29e690a024 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 496bfbb85c..83410b08f6 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -61,7 +61,7 @@
-
+
@@ -82,7 +82,7 @@
-
+