diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index 874f73da6d7f..66db439c8211 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -27,8 +27,8 @@
-
-
+
+
diff --git a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
index 01c57a6b9a75..51fe0b035df0 100644
--- a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs
@@ -13,7 +13,7 @@ public class CatchDifficultyCalculatorTest : DifficultyCalculatorTest
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
- [TestCase(4.2038001515546597d, "diffcalc-test")]
+ [TestCase(4.2058561036909863d, "diffcalc-test")]
public void Test(double expected, string name)
=> base.Test(expected, name);
diff --git a/osu.Game.Rulesets.Catch.Tests/TestCaseAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestCaseAutoJuiceStream.cs
index 9319fb3dfba6..fbb2db33b0ca 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestCaseAutoJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestCaseAutoJuiceStream.cs
@@ -13,7 +13,7 @@
namespace osu.Game.Rulesets.Catch.Tests
{
- public class TestCaseAutoJuiceStream : TestCasePlayer
+ public class TestCaseAutoJuiceStream : PlayerTestCase
{
public TestCaseAutoJuiceStream()
: base(new CatchRuleset())
diff --git a/osu.Game.Rulesets.Catch.Tests/TestCaseBananaShower.cs b/osu.Game.Rulesets.Catch.Tests/TestCaseBananaShower.cs
index 9e1c44ba40b0..d413b53d1784 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestCaseBananaShower.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestCaseBananaShower.cs
@@ -8,11 +8,12 @@
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawable;
using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestCaseBananaShower : Game.Tests.Visual.TestCasePlayer
+ public class TestCaseBananaShower : PlayerTestCase
{
public override IReadOnlyList RequiredTypes => new[]
{
@@ -20,7 +21,7 @@ public class TestCaseBananaShower : Game.Tests.Visual.TestCasePlayer
typeof(DrawableBananaShower),
typeof(CatchRuleset),
- typeof(CatchRulesetContainer),
+ typeof(DrawableCatchRuleset),
};
public TestCaseBananaShower()
diff --git a/osu.Game.Rulesets.Catch.Tests/TestCaseCatchPlayer.cs b/osu.Game.Rulesets.Catch.Tests/TestCaseCatchPlayer.cs
index 8f9dd73b80f5..5b242d05d7c9 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestCaseCatchPlayer.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestCaseCatchPlayer.cs
@@ -2,11 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
+using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestCaseCatchPlayer : Game.Tests.Visual.TestCasePlayer
+ public class TestCaseCatchPlayer : PlayerTestCase
{
public TestCaseCatchPlayer()
: base(new CatchRuleset())
diff --git a/osu.Game.Rulesets.Catch.Tests/TestCaseCatchStacker.cs b/osu.Game.Rulesets.Catch.Tests/TestCaseCatchStacker.cs
index 1e3d60d9687e..5a16a23a4e6c 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestCaseCatchStacker.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestCaseCatchStacker.cs
@@ -4,11 +4,12 @@
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestCaseCatchStacker : Game.Tests.Visual.TestCasePlayer
+ public class TestCaseCatchStacker : PlayerTestCase
{
public TestCaseCatchStacker()
: base(new CatchRuleset())
diff --git a/osu.Game.Rulesets.Catch.Tests/TestCaseHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestCaseHyperDash.cs
index 7451986a8b64..a7e7f0ab14c6 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestCaseHyperDash.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestCaseHyperDash.cs
@@ -1,22 +1,28 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
-using osu.Game.Screens.Play;
+using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
- public class TestCaseHyperDash : Game.Tests.Visual.TestCasePlayer
+ public class TestCaseHyperDash : PlayerTestCase
{
public TestCaseHyperDash()
: base(new CatchRuleset())
{
}
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash);
+ }
+
protected override IBeatmap CreateBeatmap(Ruleset ruleset)
{
var beatmap = new Beatmap
@@ -28,7 +34,7 @@ protected override IBeatmap CreateBeatmap(Ruleset ruleset)
}
};
- // Should produce a hperdash
+ // Should produce a hyper-dash
beatmap.HitObjects.Add(new Fruit { StartTime = 816, X = 308 / 512f, NewCombo = true });
beatmap.HitObjects.Add(new Fruit { StartTime = 1008, X = 56 / 512f, });
@@ -38,11 +44,5 @@ protected override IBeatmap CreateBeatmap(Ruleset ruleset)
return beatmap;
}
-
- protected override void AddCheckSteps(Func player)
- {
- base.AddCheckSteps(player);
- AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash);
- }
}
}
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index feab3ed81c4b..3f8b3bf0866b 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -2,9 +2,9 @@
-
+
-
+
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index a69070e93ee6..5140135f80b5 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -10,6 +10,7 @@
using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
+using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Beatmaps.Legacy;
@@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Catch
{
public class CatchRuleset : Ruleset
{
- public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap) => new CatchRulesetContainer(this, beatmap);
+ public override DrawableRuleset CreateDrawableRulesetWith(WorkingBeatmap beatmap) => new DrawableCatchRuleset(this, beatmap);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap);
public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new CatchBeatmapProcessor(beatmap);
@@ -99,6 +100,11 @@ public override IEnumerable GetModsFor(ModType type)
new MultiMod(new CatchModAutoplay(), new ModCinema()),
new CatchModRelax(),
};
+ case ModType.Fun:
+ return new Mod[]
+ {
+ new MultiMod(new ModWindUp(), new ModWindDown())
+ };
default:
return new Mod[] { };
}
diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
index 8cfda5d532ff..b4998347f43b 100644
--- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs
@@ -7,6 +7,7 @@
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
using osu.Game.Rulesets.Catch.Difficulty.Skills;
+using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Difficulty;
@@ -22,16 +23,9 @@ public class CatchDifficultyCalculator : DifficultyCalculator
protected override int SectionLength => 750;
- private readonly float halfCatchWidth;
-
public CatchDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
- var catcher = new CatcherArea.Catcher(beatmap.BeatmapInfo.BaseDifficulty);
- halfCatchWidth = catcher.CatchWidth * 0.5f;
-
- // We're only using 80% of the catcher's width to simulate imperfect gameplay.
- halfCatchWidth *= 0.8f;
}
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
@@ -53,6 +47,14 @@ protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beat
protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
+ float halfCatchWidth;
+
+ using (var catcher = new CatcherArea.Catcher(beatmap.BeatmapInfo.BaseDifficulty))
+ {
+ halfCatchWidth = catcher.CatchWidth * 0.5f;
+ halfCatchWidth *= 0.8f; // We're only using 80% of the catcher's width to simulate imperfect gameplay.
+ }
+
CatchHitObject lastObject = null;
foreach (var hitObject in beatmap.HitObjects.OfType())
@@ -88,5 +90,13 @@ protected override IEnumerable CreateDifficultyHitObjects(I
{
new Movement(),
};
+
+ protected override Mod[] DifficultyAdjustmentMods => new Mod[]
+ {
+ new CatchModDoubleTime(),
+ new CatchModHalfTime(),
+ new CatchModHardRock(),
+ new CatchModEasy(),
+ };
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs
index 612df5bde55b..692e63fa69f4 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModAutoplay : ModAutoplay
{
- protected override Score CreateReplayScore(Beatmap beatmap) => new Score
+ public override Score CreateReplayScore(IBeatmap beatmap) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } },
Replay = new CatchAutoGenerator(beatmap).Generate(),
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs
index 82cda7df47cd..71268d899d92 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs
@@ -21,10 +21,10 @@ public class CatchModFlashlight : ModFlashlight
private CatchPlayfield playfield;
- public override void ApplyToRulesetContainer(RulesetContainer rulesetContainer)
+ public override void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
- playfield = (CatchPlayfield)rulesetContainer.Playfield;
- base.ApplyToRulesetContainer(rulesetContainer);
+ playfield = (CatchPlayfield)drawableRuleset.Playfield;
+ base.ApplyToDrawableRuleset(drawableRuleset);
}
private class CatchFlashlight : Flashlight
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs
index 0cfa3e98f7a7..060e51e31d6f 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs
@@ -15,77 +15,107 @@ public class CatchModHardRock : ModHardRock, IApplicableToHitObject
public override double ScoreMultiplier => 1.12;
public override bool Ranked => true;
- private float lastStartX;
- private int lastStartTime;
+ private float? lastPosition;
+ private double lastStartTime;
public void ApplyToHitObject(HitObject hitObject)
{
+ if (hitObject is JuiceStream stream)
+ {
+ lastPosition = stream.EndX;
+ lastStartTime = stream.EndTime;
+ return;
+ }
+
+ if (!(hitObject is Fruit))
+ return;
+
var catchObject = (CatchHitObject)hitObject;
float position = catchObject.X;
- int startTime = (int)hitObject.StartTime;
+ double startTime = hitObject.StartTime;
- if (lastStartX == 0)
+ if (lastPosition == null)
{
- lastStartX = position;
+ lastPosition = position;
lastStartTime = startTime;
+
return;
}
- float diff = lastStartX - position;
- int timeDiff = startTime - lastStartTime;
+ float positionDiff = position - lastPosition.Value;
+ double timeDiff = startTime - lastStartTime;
if (timeDiff > 1000)
{
- lastStartX = position;
+ lastPosition = position;
lastStartTime = startTime;
return;
}
- if (diff == 0)
+ if (positionDiff == 0)
{
- bool right = RNG.NextBool();
+ applyRandomOffset(ref position, timeDiff / 4d);
+ catchObject.X = position;
+ return;
+ }
- float rand = Math.Min(20, (float)RNG.NextDouble(0, timeDiff / 4d)) / CatchPlayfield.BASE_WIDTH;
+ if (Math.Abs(positionDiff * CatchPlayfield.BASE_WIDTH) < timeDiff / 3d)
+ applyOffset(ref position, positionDiff);
- if (right)
- {
- if (position + rand <= 1)
- position += rand;
- else
- position -= rand;
- }
- else
- {
- if (position - rand >= 0)
- position -= rand;
- else
- position += rand;
- }
+ catchObject.X = position;
- catchObject.X = position;
+ lastPosition = position;
+ lastStartTime = startTime;
+ }
- return;
- }
+ ///
+ /// Applies a random offset in a random direction to a position, ensuring that the final position remains within the boundary of the playfield.
+ ///
+ /// The position which the offset should be applied to.
+ /// The maximum offset, cannot exceed 20px.
+ private void applyRandomOffset(ref float position, double maxOffset)
+ {
+ bool right = RNG.NextBool();
+ float rand = Math.Min(20, (float)RNG.NextDouble(0, Math.Max(0, maxOffset))) / CatchPlayfield.BASE_WIDTH;
- if (Math.Abs(diff) < timeDiff / 3d)
+ if (right)
{
- if (diff > 0)
- {
- if (position - diff > 0)
- position -= diff;
- }
+ // Clamp to the right bound
+ if (position + rand <= 1)
+ position += rand;
else
- {
- if (position - diff < 1)
- position -= diff;
- }
+ position -= rand;
}
+ else
+ {
+ // Clamp to the left bound
+ if (position - rand >= 0)
+ position -= rand;
+ else
+ position += rand;
+ }
+ }
- catchObject.X = position;
-
- lastStartX = position;
- lastStartTime = startTime;
+ ///
+ /// Applies an offset to a position, ensuring that the final position remains within the boundary of the playfield.
+ ///
+ /// The position which the offset should be applied to.
+ /// The amount to offset by.
+ private void applyOffset(ref float position, float amount)
+ {
+ if (amount > 0)
+ {
+ // Clamp to the right bound
+ if (position + amount < 1)
+ position += amount;
+ }
+ else
+ {
+ // Clamp to the left bound
+ if (position + amount > 0)
+ position += amount;
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 61bb4335f3e4..2adc156efdca 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Audio;
@@ -25,6 +24,11 @@ public class JuiceStream : CatchHitObject, IHasCurve
public double Velocity;
public double TickDistance;
+ ///
+ /// The length of one span of this .
+ ///
+ public double SpanDuration => Duration / this.SpanCount();
+
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
@@ -41,19 +45,6 @@ protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, B
protected override void CreateNestedHitObjects()
{
base.CreateNestedHitObjects();
- createTicks();
- }
-
- private void createTicks()
- {
- if (TickDistance == 0)
- return;
-
- var length = Path.Distance;
- var tickDistance = Math.Min(TickDistance, length);
- var spanDuration = length / Velocity;
-
- var minDistanceFromEnd = Velocity * 0.01;
var tickSamples = Samples.Select(s => new SampleInfo
{
@@ -62,81 +53,59 @@ private void createTicks()
Volume = s.Volume
}).ToList();
- AddNested(new Fruit
- {
- Samples = Samples,
- StartTime = StartTime,
- X = X
- });
-
- double lastTickTime = StartTime;
+ SliderEventDescriptor? lastEvent = null;
- for (int span = 0; span < this.SpanCount(); span++)
+ foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset))
{
- var spanStartTime = StartTime + span * spanDuration;
- var reversed = span % 2 == 1;
-
- for (double d = tickDistance;; d += tickDistance)
+ // generate tiny droplets since the last point
+ if (lastEvent != null)
{
- bool isLastTick = false;
- if (d + minDistanceFromEnd >= length)
- {
- d = length;
- isLastTick = true;
- }
-
- var timeProgress = d / length;
- var distanceProgress = reversed ? 1 - timeProgress : timeProgress;
+ double sinceLastTick = e.Time - lastEvent.Value.Time;
- double time = spanStartTime + timeProgress * spanDuration;
-
- if (LegacyLastTickOffset != null)
+ if (sinceLastTick > 80)
{
- // If we're the last tick, apply the legacy offset
- if (span == this.SpanCount() - 1 && isLastTick)
- time = Math.Max(StartTime + Duration / 2, time - LegacyLastTickOffset.Value);
- }
+ double timeBetweenTiny = sinceLastTick;
+ while (timeBetweenTiny > 100)
+ timeBetweenTiny /= 2;
- int tinyTickCount = 1;
- double tinyTickInterval = time - lastTickTime;
- while (tinyTickInterval > 100 && tinyTickCount < 10000)
- {
- tinyTickInterval /= 2;
- tinyTickCount *= 2;
+ for (double t = timeBetweenTiny; t < sinceLastTick; t += timeBetweenTiny)
+ {
+ AddNested(new TinyDroplet
+ {
+ Samples = tickSamples,
+ StartTime = t + lastEvent.Value.Time,
+ X = X + Path.PositionAt(
+ lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X / CatchPlayfield.BASE_WIDTH,
+ });
+ }
}
+ }
- for (int tinyTickIndex = 0; tinyTickIndex < tinyTickCount - 1; tinyTickIndex++)
- {
- var t = lastTickTime + (tinyTickIndex + 1) * tinyTickInterval;
- double progress = reversed ? 1 - (t - spanStartTime) / spanDuration : (t - spanStartTime) / spanDuration;
+ // this also includes LegacyLastTick and this is used for TinyDroplet generation above.
+ // this means that the final segment of TinyDroplets are increasingly mistimed where LegacyLastTickOffset is being applied.
+ lastEvent = e;
- AddNested(new TinyDroplet
+ switch (e.Type)
+ {
+ case SliderEventType.Tick:
+ AddNested(new Droplet
{
- StartTime = t,
- X = X + Path.PositionAt(progress).X / CatchPlayfield.BASE_WIDTH,
- Samples = tickSamples
+ Samples = tickSamples,
+ StartTime = e.Time,
+ X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH,
+ });
+ break;
+ case SliderEventType.Head:
+ case SliderEventType.Tail:
+ case SliderEventType.Repeat:
+ AddNested(new Fruit
+ {
+ Samples = Samples,
+ StartTime = e.Time,
+ X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH,
});
- }
-
- lastTickTime = time;
-
- if (isLastTick)
break;
-
- AddNested(new Droplet
- {
- StartTime = time,
- X = X + Path.PositionAt(distanceProgress).X / CatchPlayfield.BASE_WIDTH,
- Samples = tickSamples
- });
}
-
- AddNested(new Fruit
- {
- Samples = Samples,
- StartTime = spanStartTime + spanDuration,
- X = X + Path.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH
- });
}
}
diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
index 2fd05483ef12..daa3f61de319 100644
--- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
+++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs
@@ -6,17 +6,20 @@
using osu.Framework.MathUtils;
using osu.Game.Beatmaps;
using osu.Game.Replays;
+using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Catch.Replays
{
- internal class CatchAutoGenerator : AutoGenerator
+ internal class CatchAutoGenerator : AutoGenerator
{
public const double RELEASE_DELAY = 20;
- public CatchAutoGenerator(Beatmap beatmap)
+ public new CatchBeatmap Beatmap => (CatchBeatmap)base.Beatmap;
+
+ public CatchAutoGenerator(IBeatmap beatmap)
: base(beatmap)
{
Replay = new Replay();
diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
index e1fda1a7b3ee..af614f95d049 100644
--- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
+++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs
@@ -13,8 +13,8 @@ namespace osu.Game.Rulesets.Catch.Scoring
{
public class CatchScoreProcessor : ScoreProcessor
{
- public CatchScoreProcessor(RulesetContainer rulesetContainer)
- : base(rulesetContainer)
+ public CatchScoreProcessor(DrawableRuleset drawableRuleset)
+ : base(drawableRuleset)
{
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index d0f50c6af2af..c6dd0a86a019 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -60,7 +60,7 @@ void runAfterLoaded(Action action)
if (lastPlateableFruit.IsLoaded)
action();
else
- lastPlateableFruit.OnLoadComplete = _ => action();
+ lastPlateableFruit.OnLoadComplete += _ => action();
}
if (result.IsHit && fruit.CanBePlated)
diff --git a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
similarity index 88%
rename from osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs
rename to osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
index f4219694496e..406dc10eead4 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs
+++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
@@ -17,13 +17,13 @@
namespace osu.Game.Rulesets.Catch.UI
{
- public class CatchRulesetContainer : ScrollingRulesetContainer
+ public class DrawableCatchRuleset : DrawableScrollingRuleset
{
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Constant;
protected override bool UserScrollSpeedAdjustment => false;
- public CatchRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
+ public DrawableCatchRuleset(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
Direction.Value = ScrollingDirection.Down;
@@ -36,7 +36,7 @@ public CatchRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty, GetVisualRepresentation);
- public override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo);
+ protected override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo);
public override DrawableHitObject GetVisualRepresentation(CatchHitObject h)
{
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index e26d2433f946..fd17285a381f 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -2,9 +2,9 @@
-
+
-
+
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaEditRulesetContainer.cs b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditRuleset.cs
similarity index 83%
rename from osu.Game.Rulesets.Mania/Edit/ManiaEditRulesetContainer.cs
rename to osu.Game.Rulesets.Mania/Edit/DrawableManiaEditRuleset.cs
index 89e531fd9fde..acafaffee67d 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaEditRulesetContainer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditRuleset.cs
@@ -10,11 +10,11 @@
namespace osu.Game.Rulesets.Mania.Edit
{
- public class ManiaEditRulesetContainer : ManiaRulesetContainer
+ public class DrawableManiaEditRuleset : DrawableManiaRuleset
{
public new IScrollingInfo ScrollingInfo => base.ScrollingInfo;
- public ManiaEditRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
+ public DrawableManiaEditRuleset(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
index 3dbbd132a6aa..56c9471462f0 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mania.Edit
[Cached(Type = typeof(IManiaHitObjectComposer))]
public class ManiaHitObjectComposer : HitObjectComposer, IManiaHitObjectComposer
{
- protected new ManiaEditRulesetContainer RulesetContainer { get; private set; }
+ protected new DrawableManiaEditRuleset DrawableRuleset { get; private set; }
public ManiaHitObjectComposer(Ruleset ruleset)
: base(ruleset)
@@ -32,23 +32,23 @@ public ManiaHitObjectComposer(Ruleset ruleset)
///
/// The screen-space position.
/// The column which intersects with .
- public Column ColumnAt(Vector2 screenSpacePosition) => RulesetContainer.GetColumnByPosition(screenSpacePosition);
+ public Column ColumnAt(Vector2 screenSpacePosition) => DrawableRuleset.GetColumnByPosition(screenSpacePosition);
private DependencyContainer dependencies;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
- public int TotalColumns => ((ManiaPlayfield)RulesetContainer.Playfield).TotalColumns;
+ public int TotalColumns => ((ManiaPlayfield)DrawableRuleset.Playfield).TotalColumns;
- protected override RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
+ protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, WorkingBeatmap beatmap)
{
- RulesetContainer = new ManiaEditRulesetContainer(ruleset, beatmap);
+ DrawableRuleset = new DrawableManiaEditRuleset(ruleset, beatmap);
// This is the earliest we can cache the scrolling info to ourselves, before masks are added to the hierarchy and inject it
- dependencies.CacheAs(RulesetContainer.ScrollingInfo);
+ dependencies.CacheAs(DrawableRuleset.ScrollingInfo);
- return RulesetContainer;
+ return DrawableRuleset;
}
protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[]
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index b40093844b42..a4a10f174291 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -12,6 +12,7 @@
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Game.Graphics;
+using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Beatmaps.Legacy;
@@ -30,7 +31,7 @@ namespace osu.Game.Rulesets.Mania
{
public class ManiaRuleset : Ruleset
{
- public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap) => new ManiaRulesetContainer(this, beatmap);
+ public override DrawableRuleset CreateDrawableRulesetWith(WorkingBeatmap beatmap) => new DrawableManiaRuleset(this, beatmap);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap);
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new ManiaPerformanceCalculator(this, beatmap, score);
@@ -145,6 +146,11 @@ public override IEnumerable GetModsFor(ModType type)
{
new MultiMod(new ManiaModAutoplay(), new ModCinema()),
};
+ case ModType.Fun:
+ return new Mod[]
+ {
+ new MultiMod(new ModWindUp(), new ModWindDown())
+ };
default:
return new Mod[] { };
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs
index 02eb7ac883e0..c05e979e9af2 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModAutoplay : ModAutoplay
{
- protected override Score CreateReplayScore(Beatmap beatmap) => new Score
+ public override Score CreateReplayScore(IBeatmap beatmap) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } },
Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),
diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
index df6afa040e2e..65b7d54cd280 100644
--- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
@@ -5,13 +5,12 @@
using System.Linq;
using osu.Game.Replays;
using osu.Game.Rulesets.Mania.Beatmaps;
-using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Mania.Replays
{
- internal class ManiaAutoGenerator : AutoGenerator
+ internal class ManiaAutoGenerator : AutoGenerator
{
public const double RELEASE_DELAY = 20;
diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
index cf3d0734fb08..5c914d8eacbd 100644
--- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
+++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
@@ -92,8 +92,8 @@ public ManiaScoreProcessor()
{
}
- public ManiaScoreProcessor(RulesetContainer rulesetContainer)
- : base(rulesetContainer)
+ public ManiaScoreProcessor(DrawableRuleset drawableRuleset)
+ : base(drawableRuleset)
{
}
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
index 5874bac7f69e..8797f014df7c 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs
@@ -22,22 +22,15 @@ private void load()
JudgementText.Font = JudgementText.Font.With(size: 25);
}
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- this.FadeInFromZero(50, Easing.OutQuint);
+ protected override double FadeInDuration => 50;
- if (Result.IsHit)
- {
- JudgementBody.ScaleTo(0.8f);
- JudgementBody.ScaleTo(1, 250, Easing.OutElastic);
-
- JudgementBody.Delay(50).ScaleTo(0.75f, 250);
- this.Delay(50).FadeOut(200);
- }
+ protected override void ApplyHitAnimations()
+ {
+ JudgementBody.ScaleTo(0.8f);
+ JudgementBody.ScaleTo(1, 250, Easing.OutElastic);
- Expire();
+ JudgementBody.Delay(FadeInDuration).ScaleTo(0.75f, 250);
+ this.Delay(FadeInDuration).FadeOut(200);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
similarity index 92%
rename from osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs
rename to osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
index d8b7dc038117..a019401d5bde 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
@@ -28,8 +28,10 @@
namespace osu.Game.Rulesets.Mania.UI
{
- public class ManiaRulesetContainer : ScrollingRulesetContainer
+ public class DrawableManiaRuleset : DrawableScrollingRuleset
{
+ protected new ManiaPlayfield Playfield => (ManiaPlayfield)base.Playfield;
+
public new ManiaBeatmap Beatmap => (ManiaBeatmap)base.Beatmap;
public IEnumerable BarLines;
@@ -38,7 +40,7 @@ public class ManiaRulesetContainer : ScrollingRulesetContainer configDirection = new Bindable();
- public ManiaRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
+ public DrawableManiaRuleset(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
// Generate the bar lines
@@ -97,7 +99,7 @@ private void load()
public override int Variant => (int)(Beatmap.Stages.Count == 1 ? PlayfieldType.Single : PlayfieldType.Dual) + Beatmap.TotalColumns;
- public override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo, Variant);
+ protected override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo, Variant);
public override DrawableHitObject GetVisualRepresentation(ManiaHitObject h)
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseGameplayCursor.cs
index 3d553c334f73..5c1e775c0168 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestCaseGameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestCaseGameplayCursor.cs
@@ -16,18 +16,18 @@ namespace osu.Game.Rulesets.Osu.Tests
[TestFixture]
public class TestCaseGameplayCursor : OsuTestCase, IProvideCursor
{
- private GameplayCursor cursor;
+ private GameplayCursorContainer cursorContainer;
public override IReadOnlyList RequiredTypes => new[] { typeof(CursorTrail) };
- public CursorContainer Cursor => cursor;
+ public CursorContainer Cursor => cursorContainer;
public bool ProvidingUserCursor => true;
[BackgroundDependencyLoader]
private void load()
{
- Add(cursor = new GameplayCursor { RelativeSizeAxes = Axes.Both });
+ Add(cursorContainer = new GameplayCursorContainer { RelativeSizeAxes = Axes.Both });
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseHitCircleLongCombo.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseHitCircleLongCombo.cs
index f5fe36b56a57..8d097ff1c1e7 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestCaseHitCircleLongCombo.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestCaseHitCircleLongCombo.cs
@@ -4,12 +4,13 @@
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
- public class TestCaseHitCircleLongCombo : Game.Tests.Visual.TestCasePlayer
+ public class TestCaseHitCircleLongCombo : PlayerTestCase
{
public TestCaseHitCircleLongCombo()
: base(new OsuRuleset())
diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseOsuPlayer.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseOsuPlayer.cs
new file mode 100644
index 000000000000..720c3c66fe2f
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestCaseOsuPlayer.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.
+
+using NUnit.Framework;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ [TestFixture]
+ public class TestCaseOsuPlayer : PlayerTestCase
+ {
+ public TestCaseOsuPlayer()
+ : base(new OsuRuleset())
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseSliderInput.cs
index 57effe01f1d8..2f33982d41f4 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestCaseSliderInput.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestCaseSliderInput.cs
@@ -354,9 +354,9 @@ private void performTest(List frames)
judgementResults = new List();
});
- AddUntilStep(() => Beatmap.Value.Track.CurrentTime == 0, "Beatmap at 0");
- AddUntilStep(() => currentPlayer.IsCurrentScreen(), "Wait until player is loaded");
- AddUntilStep(() => allJudgedFired, "Wait for all judged");
+ AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
+ AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
+ AddUntilStep("Wait for all judged", () => allJudgedFired);
}
private class ScoreAccessibleReplayPlayer : ReplayPlayer
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index 273d29c3de82..8c31db9a7dae 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -2,9 +2,9 @@
-
+
-
+
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
index a164b7e6bbbd..e257369ad9c5 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
@@ -38,7 +38,7 @@ public PathControlPointPiece(Slider slider, int index)
path = new SmoothPath
{
Anchor = Anchor.Centre,
- PathWidth = 1
+ PathRadius = 1
},
marker = new CircularContainer
{
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
index 179fa1bc72ea..957550a0514f 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
@@ -24,7 +24,7 @@ public SliderBodyPiece(Slider slider)
InternalChild = body = new ManualSliderBody
{
AccentColour = Color4.Transparent,
- PathWidth = slider.Scale * 64
+ PathRadius = slider.Scale * 64
};
}
@@ -34,7 +34,7 @@ private void load(OsuColour colours)
body.BorderColour = colours.Yellow;
PositionBindable.BindValueChanged(_ => updatePosition(), true);
- ScaleBindable.BindValueChanged(scale => body.PathWidth = scale.NewValue * 64, true);
+ ScaleBindable.BindValueChanged(scale => body.PathRadius = scale.NewValue * 64, true);
}
private void updatePosition() => Position = slider.StackedPosition;
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs
similarity index 58%
rename from osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs
rename to osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs
index c293a5a4f8c1..1a6e78d9189f 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs
@@ -9,15 +9,18 @@
namespace osu.Game.Rulesets.Osu.Edit
{
- public class OsuEditRulesetContainer : OsuRulesetContainer
+ public class DrawableOsuEditRuleset : DrawableOsuRuleset
{
- public OsuEditRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
+ public DrawableOsuEditRuleset(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
}
- protected override CursorContainer CreateCursor() => null;
+ protected override Playfield CreatePlayfield() => new OsuPlayfieldNoCursor { Size = Vector2.One };
- protected override Playfield CreatePlayfield() => new OsuPlayfield { Size = Vector2.One };
+ private class OsuPlayfieldNoCursor : OsuPlayfield
+ {
+ protected override CursorContainer CreateCursor() => null;
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index 174321b8b924..dd3925e04fe3 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -26,8 +26,8 @@ public OsuHitObjectComposer(Ruleset ruleset)
{
}
- protected override RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
- => new OsuEditRulesetContainer(ruleset, beatmap);
+ protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, WorkingBeatmap beatmap)
+ => new DrawableOsuEditRuleset(ruleset, beatmap);
protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[]
{
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
index d7ba0d4da97c..bea2bbcb32f9 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs
@@ -16,7 +16,7 @@ public class OsuModAutoplay : ModAutoplay
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray();
- protected override Score CreateReplayScore(Beatmap beatmap) => new Score
+ public override Score CreateReplayScore(IBeatmap beatmap) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } },
Replay = new OsuAutoGenerator(beatmap).Generate()
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
index a203e23687e4..a1f4dfe1da18 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs
@@ -18,7 +18,7 @@
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModBlinds : Mod, IApplicableToRulesetContainer, IApplicableToScoreProcessor
+ public class OsuModBlinds : Mod, IApplicableToDrawableRuleset, IApplicableToScoreProcessor
{
public override string Name => "Blinds";
public override string Description => "Play with blinds on your screen.";
@@ -32,9 +32,9 @@ public class OsuModBlinds : Mod, IApplicableToRulesetContainer, IA
public override double ScoreMultiplier => 1.12;
private DrawableOsuBlinds blinds;
- public void ApplyToRulesetContainer(RulesetContainer rulesetContainer)
+ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
- rulesetContainer.Overlays.Add(blinds = new DrawableOsuBlinds(rulesetContainer.Playfield.HitObjectContainer, rulesetContainer.Beatmap));
+ drawableRuleset.Overlays.Add(blinds = new DrawableOsuBlinds(drawableRuleset.Playfield.HitObjectContainer, drawableRuleset.Beatmap));
}
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
index efcab2831081..ec23570f54ba 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
@@ -13,7 +13,7 @@
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModRelax : ModRelax, IApplicableFailOverride, IUpdatableByPlayfield, IApplicableToRulesetContainer
+ public class OsuModRelax : ModRelax, IApplicableFailOverride, IUpdatableByPlayfield, IApplicableToDrawableRuleset
{
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
@@ -79,10 +79,10 @@ private void addAction(bool hitting)
state.Apply(osuInputManager.CurrentState, osuInputManager);
}
- public void ApplyToRulesetContainer(RulesetContainer rulesetContainer)
+ public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
// grab the input manager for future use.
- osuInputManager = (OsuInputManager)rulesetContainer.KeyBindingInputManager;
+ osuInputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager;
osuInputManager.AllowUserPresses = false;
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
index 2512e74da752..938a2293baa6 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs
@@ -5,7 +5,6 @@
using osuTK;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@@ -16,12 +15,10 @@ public DrawableOsuJudgement(JudgementResult result, DrawableHitObject judgedObje
{
}
- protected override void LoadComplete()
+ protected override void ApplyHitAnimations()
{
- if (Result.Type != HitResult.Miss)
- JudgementText?.TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint);
-
- base.LoadComplete();
+ JudgementText?.TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint);
+ base.ApplyHitAnimations();
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index ff749545522b..57ea0abdd87f 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -50,7 +50,7 @@ public DrawableSlider(Slider s)
{
Body = new SnakingSliderBody(s)
{
- PathWidth = s.Scale * 64,
+ PathRadius = s.Scale * 64,
},
ticks = new Container { RelativeSizeAxes = Axes.Both },
repeatPoints = new Container { RelativeSizeAxes = Axes.Both },
@@ -105,7 +105,7 @@ private void load()
positionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
scaleBindable.BindValueChanged(scale =>
{
- Body.PathWidth = scale.NewValue * 64;
+ Body.PathRadius = scale.NewValue * 64;
Ball.Scale = new Vector2(scale.NewValue);
});
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
index 1b2e2c1f47d4..e41c568403eb 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs
@@ -132,6 +132,12 @@ public override void ClearTransformsAfter(double time, bool propagateChildren =
base.ClearTransformsAfter(time, false, targetMember);
}
+ public override void ApplyTransformsAt(double time, bool propagateChildren = false)
+ {
+ // For the same reasons as above w.r.t rewinding, we shouldn't propagate to children here either.
+ base.ApplyTransformsAt(time, false);
+ }
+
private bool tracking;
public bool Tracking
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs
index b088f1914b50..2f5c326bda7b 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs
@@ -19,10 +19,10 @@ public abstract class SliderBody : CompositeDrawable
private readonly BufferedContainer container;
- public float PathWidth
+ public float PathRadius
{
- get => path.PathWidth;
- set => path.PathWidth = value;
+ get => path.PathRadius;
+ set => path.PathRadius = value;
}
///
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index 345f599b9d66..1afbacc01e83 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using osuTK;
using osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic;
@@ -155,116 +154,76 @@ protected override void CreateNestedHitObjects()
{
base.CreateNestedHitObjects();
- createSliderEnds();
- createTicks();
- createRepeatPoints();
-
- if (LegacyLastTickOffset != null)
- TailCircle.StartTime = Math.Max(StartTime + Duration / 2, TailCircle.StartTime - LegacyLastTickOffset.Value);
- }
-
- private void createSliderEnds()
- {
- HeadCircle = new SliderCircle
- {
- StartTime = StartTime,
- Position = Position,
- Samples = getNodeSamples(0),
- SampleControlPoint = SampleControlPoint,
- IndexInCurrentCombo = IndexInCurrentCombo,
- ComboIndex = ComboIndex,
- };
-
- TailCircle = new SliderTailCircle(this)
+ foreach (var e in
+ SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset))
{
- StartTime = EndTime,
- Position = EndPosition,
- IndexInCurrentCombo = IndexInCurrentCombo,
- ComboIndex = ComboIndex,
- };
-
- AddNested(HeadCircle);
- AddNested(TailCircle);
- }
-
- private void createTicks()
- {
- // A very lenient maximum length of a slider for ticks to be generated.
- // This exists for edge cases such as /b/1573664 where the beatmap has been edited by the user, and should never be reached in normal usage.
- const double max_length = 100000;
-
- var length = Math.Min(max_length, Path.Distance);
- var tickDistance = MathHelper.Clamp(TickDistance, 0, length);
-
- if (tickDistance == 0) return;
+ var firstSample = Samples.Find(s => s.Name == SampleInfo.HIT_NORMAL)
+ ?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933)
+ var sampleList = new List();
- var minDistanceFromEnd = Velocity * 10;
-
- var spanCount = this.SpanCount();
-
- for (var span = 0; span < spanCount; span++)
- {
- var spanStartTime = StartTime + span * SpanDuration;
- var reversed = span % 2 == 1;
+ if (firstSample != null)
+ sampleList.Add(new SampleInfo
+ {
+ Bank = firstSample.Bank,
+ Volume = firstSample.Volume,
+ Name = @"slidertick",
+ });
- for (var d = tickDistance; d <= length; d += tickDistance)
+ switch (e.Type)
{
- if (d > length - minDistanceFromEnd)
+ case SliderEventType.Tick:
+ AddNested(new SliderTick
+ {
+ SpanIndex = e.SpanIndex,
+ SpanStartTime = e.SpanStartTime,
+ StartTime = e.Time,
+ Position = Position + Path.PositionAt(e.PathProgress),
+ StackHeight = StackHeight,
+ Scale = Scale,
+ Samples = sampleList
+ });
break;
-
- var distanceProgress = d / length;
- var timeProgress = reversed ? 1 - distanceProgress : distanceProgress;
-
- var firstSample = Samples.Find(s => s.Name == SampleInfo.HIT_NORMAL)
- ?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933)
- var sampleList = new List();
-
- if (firstSample != null)
- sampleList.Add(new SampleInfo
+ case SliderEventType.Head:
+ AddNested(HeadCircle = new SliderCircle
{
- Bank = firstSample.Bank,
- Volume = firstSample.Volume,
- Name = @"slidertick",
+ StartTime = e.Time,
+ Position = Position,
+ Samples = getNodeSamples(0),
+ SampleControlPoint = SampleControlPoint,
+ IndexInCurrentCombo = IndexInCurrentCombo,
+ ComboIndex = ComboIndex,
});
-
- AddNested(new SliderTick
- {
- SpanIndex = span,
- SpanStartTime = spanStartTime,
- StartTime = spanStartTime + timeProgress * SpanDuration,
- Position = Position + Path.PositionAt(distanceProgress),
- StackHeight = StackHeight,
- Scale = Scale,
- Samples = sampleList
- });
+ break;
+ case SliderEventType.LegacyLastTick:
+ // we need to use the LegacyLastTick here for compatibility reasons (difficulty).
+ // it is *okay* to use this because the TailCircle is not used for any meaningful purpose in gameplay.
+ // if this is to change, we should revisit this.
+ AddNested(TailCircle = new SliderTailCircle(this)
+ {
+ StartTime = e.Time,
+ Position = EndPosition,
+ IndexInCurrentCombo = IndexInCurrentCombo,
+ ComboIndex = ComboIndex,
+ });
+ break;
+ case SliderEventType.Repeat:
+ AddNested(new RepeatPoint
+ {
+ RepeatIndex = e.SpanIndex,
+ SpanDuration = SpanDuration,
+ StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
+ Position = Position + Path.PositionAt(e.PathProgress),
+ StackHeight = StackHeight,
+ Scale = Scale,
+ Samples = getNodeSamples(e.SpanIndex + 1)
+ });
+ break;
}
}
}
- private void createRepeatPoints()
- {
- for (int repeatIndex = 0, repeat = 1; repeatIndex < RepeatCount; repeatIndex++, repeat++)
- {
- AddNested(new RepeatPoint
- {
- RepeatIndex = repeatIndex,
- SpanDuration = SpanDuration,
- StartTime = StartTime + repeat * SpanDuration,
- Position = Position + Path.PositionAt(repeat % 2),
- StackHeight = StackHeight,
- Scale = Scale,
- Samples = getNodeSamples(1 + repeatIndex)
- });
- }
- }
-
- private List getNodeSamples(int nodeIndex)
- {
- if (nodeIndex < NodeSamples.Count)
- return NodeSamples[nodeIndex];
-
- return Samples;
- }
+ private List getNodeSamples(int nodeIndex) =>
+ nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples;
public override Judgement CreateJudgement() => new OsuJudgement();
}
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
index 43a2ae0fbb09..4f2af6416133 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
@@ -8,6 +8,10 @@
namespace osu.Game.Rulesets.Osu.Objects
{
+ ///
+ /// Note that this should not be used for timing correctness.
+ /// See usage in for more information.
+ ///
public class SliderTailCircle : SliderCircle
{
private readonly IBindable pathBindable = new Bindable();
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 20752517d56e..7d3aff7801a1 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -13,6 +13,7 @@
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Beatmaps.Legacy;
@@ -28,7 +29,7 @@ namespace osu.Game.Rulesets.Osu
{
public class OsuRuleset : Ruleset
{
- public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap) => new OsuRulesetContainer(this, beatmap);
+ public override DrawableRuleset CreateDrawableRulesetWith(WorkingBeatmap beatmap) => new DrawableOsuRuleset(this, beatmap);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap);
public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new OsuBeatmapProcessor(beatmap);
@@ -128,7 +129,8 @@ public override IEnumerable GetModsFor(ModType type)
{
new OsuModTransform(),
new OsuModWiggle(),
- new OsuModGrow()
+ new OsuModGrow(),
+ new MultiMod(new ModWindUp(), new ModWindDown()),
};
default:
return new Mod[] { };
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
index b0fb85d7ed4b..c1aaa7767e5a 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs
@@ -10,12 +10,15 @@
using osu.Framework.Graphics;
using osu.Game.Replays;
using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Replays
{
public class OsuAutoGenerator : OsuAutoGeneratorBase
{
+ public new OsuBeatmap Beatmap => (OsuBeatmap)base.Beatmap;
+
#region Parameters
///
@@ -42,7 +45,7 @@ public class OsuAutoGenerator : OsuAutoGeneratorBase
#region Construction / Initialisation
- public OsuAutoGenerator(Beatmap beatmap)
+ public OsuAutoGenerator(IBeatmap beatmap)
: base(beatmap)
{
// Already superhuman, but still somewhat realistic
diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
index 9a60f0cafc88..9ab358ee12ad 100644
--- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
+++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs
@@ -3,7 +3,6 @@
using osuTK;
using osu.Game.Beatmaps;
-using osu.Game.Rulesets.Osu.Objects;
using System;
using System.Collections.Generic;
using osu.Game.Replays;
@@ -12,7 +11,7 @@
namespace osu.Game.Rulesets.Osu.Replays
{
- public abstract class OsuAutoGeneratorBase : AutoGenerator
+ public abstract class OsuAutoGeneratorBase : AutoGenerator
{
#region Constants
@@ -35,7 +34,7 @@ public abstract class OsuAutoGeneratorBase : AutoGenerator
protected Replay Replay;
protected List Frames => Replay.Frames;
- protected OsuAutoGeneratorBase(Beatmap beatmap)
+ protected OsuAutoGeneratorBase(IBeatmap beatmap)
: base(beatmap)
{
Replay = new Replay();
diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
index 4f97cc0da546..2c8bf110165a 100644
--- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
+++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
@@ -15,8 +15,8 @@ namespace osu.Game.Rulesets.Osu.Scoring
{
internal class OsuScoreProcessor : ScoreProcessor
{
- public OsuScoreProcessor(RulesetContainer rulesetContainer)
- : base(rulesetContainer)
+ public OsuScoreProcessor(DrawableRuleset drawableRuleset)
+ : base(drawableRuleset)
{
}
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
index 0f8a0ce1ae67..03dbf7ac632d 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
@@ -24,7 +24,7 @@ internal class CursorTrail : Drawable, IRequireHighFrequencyMousePosition
{
private int currentIndex;
- private Shader shader;
+ private IShader shader;
private Texture texture;
private Vector2 size => texture.Size * Scale;
@@ -35,7 +35,6 @@ internal class CursorTrail : Drawable, IRequireHighFrequencyMousePosition
public override bool IsPresent => true;
- private readonly TrailDrawNodeSharedData trailDrawNodeSharedData = new TrailDrawNodeSharedData();
private const int max_sprites = 2048;
private readonly TrailPart[] parts = new TrailPart[max_sprites];
@@ -55,7 +54,6 @@ protected override void ApplyDrawNode(DrawNode node)
tNode.Texture = texture;
tNode.Size = size;
tNode.Time = time;
- tNode.Shared = trailDrawNodeSharedData;
for (int i = 0; i < parts.Length; ++i)
if (parts[i].InvalidationID > tNode.Parts[i].InvalidationID)
@@ -81,7 +79,7 @@ public CursorTrail()
[BackgroundDependencyLoader]
private void load(ShaderManager shaders, TextureStore textures)
{
- shader = shaders?.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE);
+ shader = shaders.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE);
texture = textures.Get(@"Cursor/cursortrail");
Scale = new Vector2(1 / texture.ScaleAdjust);
}
@@ -167,22 +165,18 @@ private struct TrailPart
public bool WasUpdated;
}
- private class TrailDrawNodeSharedData
- {
- public VertexBuffer VertexBuffer;
- }
-
private class TrailDrawNode : DrawNode
{
- public Shader Shader;
+ public IShader Shader;
public Texture Texture;
public float Time;
- public TrailDrawNodeSharedData Shared;
public readonly TrailPart[] Parts = new TrailPart[max_sprites];
public Vector2 Size;
+ private readonly VertexBuffer vertexBuffer = new QuadVertexBuffer(max_sprites, BufferUsageHint.DynamicDraw);
+
public TrailDrawNode()
{
for (int i = 0; i < max_sprites; i++)
@@ -194,9 +188,6 @@ public TrailDrawNode()
public override void Draw(Action vertexAction)
{
- if (Shared.VertexBuffer == null)
- Shared.VertexBuffer = new QuadVertexBuffer(max_sprites, BufferUsageHint.DynamicDraw);
-
Shader.GetUniform("g_FadeClock").UpdateValue(ref Time);
int updateStart = -1, updateEnd = 0;
@@ -218,7 +209,7 @@ public override void Draw(Action vertexAction)
new Quad(pos.X - Size.X / 2, pos.Y - Size.Y / 2, Size.X, Size.Y),
DrawColourInfo.Colour,
null,
- v => Shared.VertexBuffer.Vertices[end++] = new TexturedTrailVertex
+ v => vertexBuffer.Vertices[end++] = new TexturedTrailVertex
{
Position = v.Position,
TexturePosition = v.TexturePosition,
@@ -230,24 +221,31 @@ public override void Draw(Action vertexAction)
}
else if (updateStart != -1)
{
- Shared.VertexBuffer.UpdateRange(updateStart * 4, updateEnd * 4);
+ vertexBuffer.UpdateRange(updateStart * 4, updateEnd * 4);
updateStart = -1;
}
}
// Update all remaining vertices that have been changed.
if (updateStart != -1)
- Shared.VertexBuffer.UpdateRange(updateStart * 4, updateEnd * 4);
+ vertexBuffer.UpdateRange(updateStart * 4, updateEnd * 4);
base.Draw(vertexAction);
Shader.Bind();
Texture.TextureGL.Bind();
- Shared.VertexBuffer.Draw();
+ vertexBuffer.Draw();
Shader.Unbind();
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ vertexBuffer.Dispose();
+ }
}
[StructLayout(LayoutKind.Sequential)]
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursorContainer.cs
similarity index 97%
rename from osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursor.cs
rename to osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursorContainer.cs
index ef126cdf7dbd..8c6723f5be36 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursorContainer.cs
@@ -17,7 +17,7 @@
namespace osu.Game.Rulesets.Osu.UI.Cursor
{
- public class GameplayCursor : CursorContainer, IKeyBindingHandler
+ public class GameplayCursorContainer : CursorContainer, IKeyBindingHandler
{
protected override Drawable CreateCursor() => new OsuCursor();
@@ -25,7 +25,7 @@ public class GameplayCursor : CursorContainer, IKeyBindingHandler
private readonly Container fadeContainer;
- public GameplayCursor()
+ public GameplayCursorContainer()
{
InternalChild = fadeContainer = new Container
{
@@ -103,7 +103,7 @@ public class OsuCursor : SkinReloadableDrawable
public OsuCursor()
{
Origin = Anchor.Centre;
- Size = new Vector2(42);
+ Size = new Vector2(28);
}
protected override void SkinChanged(ISkinSource skin, bool allowFallback)
diff --git a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs
similarity index 80%
rename from osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs
rename to osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs
index 85b72cbb5b85..b632e0fb056e 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs
+++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
-using osu.Framework.Graphics.Cursor;
using osu.Framework.Input;
using osu.Game.Beatmaps;
using osu.Game.Input.Handlers;
@@ -13,17 +12,16 @@
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.Scoring;
-using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.UI
{
- public class OsuRulesetContainer : RulesetContainer
+ public class DrawableOsuRuleset : DrawableRuleset
{
protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config;
- public OsuRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
+ public DrawableOsuRuleset(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
}
@@ -32,7 +30,7 @@ public OsuRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
protected override Playfield CreatePlayfield() => new OsuPlayfield();
- public override PassThroughInputManager CreateInputManager() => new OsuInputManager(Ruleset.RulesetInfo);
+ protected override PassThroughInputManager CreateInputManager() => new OsuInputManager(Ruleset.RulesetInfo);
public override DrawableHitObject GetVisualRepresentation(OsuHitObject h)
{
@@ -59,7 +57,5 @@ public override double GameplayStartTime
return first.StartTime - first.TimePreempt;
}
}
-
- protected override CursorContainer CreateCursor() => new GameplayCursor();
}
}
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index 2db2b455409d..51733c3c01ad 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -10,7 +10,9 @@
using osu.Game.Rulesets.Osu.Objects.Drawables.Connections;
using osu.Game.Rulesets.UI;
using System.Linq;
+using osu.Framework.Graphics.Cursor;
using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Osu.UI.Cursor;
namespace osu.Game.Rulesets.Osu.UI
{
@@ -22,6 +24,12 @@ public class OsuPlayfield : Playfield
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
+ private readonly PlayfieldAdjustmentContainer adjustmentContainer;
+
+ protected override Container CursorTargetContainer => adjustmentContainer;
+
+ protected override CursorContainer CreateCursor() => new GameplayCursorContainer();
+
public OsuPlayfield()
{
Anchor = Anchor.Centre;
@@ -29,7 +37,7 @@ public OsuPlayfield()
Size = new Vector2(0.75f);
- InternalChild = new PlayfieldAdjustmentContainer
+ InternalChild = adjustmentContainer = new PlayfieldAdjustmentContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestCaseTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/TestCaseTaikoPlayfield.cs
index 00e1b649d9bd..369cdd49d29f 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestCaseTaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestCaseTaikoPlayfield.cs
@@ -33,7 +33,7 @@ public class TestCaseTaikoPlayfield : OsuTestCase
protected override double TimePerAction => default_duration * 2;
private readonly Random rng = new Random(1337);
- private TaikoRulesetContainer rulesetContainer;
+ private DrawableTaikoRuleset drawableRuleset;
private Container playfieldContainer;
[BackgroundDependencyLoader]
@@ -86,7 +86,7 @@ private void load()
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = 768,
- Children = new[] { rulesetContainer = new TaikoRulesetContainer(new TaikoRuleset(), beatmap) }
+ Children = new[] { drawableRuleset = new DrawableTaikoRuleset(new TaikoRuleset(), beatmap) }
});
}
@@ -139,7 +139,7 @@ private void addHitJudgement(bool kiai)
var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) };
- ((TaikoPlayfield)rulesetContainer.Playfield).OnNewResult(h, new JudgementResult(new TaikoJudgement()) { Type = hitResult });
+ ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new TaikoJudgement()) { Type = hitResult });
}
private void addStrongHitJudgement(bool kiai)
@@ -154,33 +154,33 @@ private void addStrongHitJudgement(bool kiai)
var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) };
- ((TaikoPlayfield)rulesetContainer.Playfield).OnNewResult(h, new JudgementResult(new TaikoJudgement()) { Type = hitResult });
- ((TaikoPlayfield)rulesetContainer.Playfield).OnNewResult(new TestStrongNestedHit(h), new JudgementResult(new TaikoStrongJudgement()) { Type = HitResult.Great });
+ ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new TaikoJudgement()) { Type = hitResult });
+ ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(new TestStrongNestedHit(h), new JudgementResult(new TaikoStrongJudgement()) { Type = HitResult.Great });
}
private void addMissJudgement()
{
- ((TaikoPlayfield)rulesetContainer.Playfield).OnNewResult(new DrawableTestHit(new Hit()), new JudgementResult(new TaikoJudgement()) { Type = HitResult.Miss });
+ ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(new DrawableTestHit(new Hit()), new JudgementResult(new TaikoJudgement()) { Type = HitResult.Miss });
}
private void addBarLine(bool major, double delay = scroll_time)
{
- BarLine bl = new BarLine { StartTime = rulesetContainer.Playfield.Time.Current + delay };
+ BarLine bl = new BarLine { StartTime = drawableRuleset.Playfield.Time.Current + delay };
- rulesetContainer.Playfield.Add(major ? new DrawableBarLineMajor(bl) : new DrawableBarLine(bl));
+ drawableRuleset.Playfield.Add(major ? new DrawableBarLineMajor(bl) : new DrawableBarLine(bl));
}
private void addSwell(double duration = default_duration)
{
var swell = new Swell
{
- StartTime = rulesetContainer.Playfield.Time.Current + scroll_time,
+ StartTime = drawableRuleset.Playfield.Time.Current + scroll_time,
Duration = duration,
};
swell.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
- rulesetContainer.Playfield.Add(new DrawableSwell(swell));
+ drawableRuleset.Playfield.Add(new DrawableSwell(swell));
}
private void addDrumRoll(bool strong, double duration = default_duration)
@@ -190,40 +190,40 @@ private void addDrumRoll(bool strong, double duration = default_duration)
var d = new DrumRoll
{
- StartTime = rulesetContainer.Playfield.Time.Current + scroll_time,
+ StartTime = drawableRuleset.Playfield.Time.Current + scroll_time,
IsStrong = strong,
Duration = duration,
};
d.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
- rulesetContainer.Playfield.Add(new DrawableDrumRoll(d));
+ drawableRuleset.Playfield.Add(new DrawableDrumRoll(d));
}
private void addCentreHit(bool strong)
{
Hit h = new Hit
{
- StartTime = rulesetContainer.Playfield.Time.Current + scroll_time,
+ StartTime = drawableRuleset.Playfield.Time.Current + scroll_time,
IsStrong = strong
};
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
- rulesetContainer.Playfield.Add(new DrawableCentreHit(h));
+ drawableRuleset.Playfield.Add(new DrawableCentreHit(h));
}
private void addRimHit(bool strong)
{
Hit h = new Hit
{
- StartTime = rulesetContainer.Playfield.Time.Current + scroll_time,
+ StartTime = drawableRuleset.Playfield.Time.Current + scroll_time,
IsStrong = strong
};
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
- rulesetContainer.Playfield.Add(new DrawableRimHit(h));
+ drawableRuleset.Playfield.Add(new DrawableRimHit(h));
}
private class TestStrongNestedHit : DrawableStrongNestedHit
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index fade0543822b..72ce6c947bce 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -2,9 +2,9 @@
-
+
-
+
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs
index 4e5491da9c52..5b890b3d038a 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModAutoplay : ModAutoplay
{
- protected override Score CreateReplayScore(Beatmap beatmap) => new Score
+ public override Score CreateReplayScore(IBeatmap beatmap) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "mekkadosu!" } },
Replay = new TaikoAutoGenerator(beatmap).Generate(),
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
index b99ec5716601..b7db3307adcb 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
@@ -22,10 +22,10 @@ public class TaikoModFlashlight : ModFlashlight
private TaikoPlayfield playfield;
- public override void ApplyToRulesetContainer(RulesetContainer rulesetContainer)
+ public override void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
- playfield = (TaikoPlayfield)rulesetContainer.Playfield;
- base.ApplyToRulesetContainer(rulesetContainer);
+ playfield = (TaikoPlayfield)drawableRuleset.Playfield;
+ base.ApplyToDrawableRuleset(drawableRuleset);
}
private class TaikoFlashlight : Flashlight
diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs
index 14a6f9848020..01ba53e07b44 100644
--- a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs
+++ b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs
@@ -9,14 +9,17 @@
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Replays;
+using osu.Game.Rulesets.Taiko.Beatmaps;
namespace osu.Game.Rulesets.Taiko.Replays
{
- public class TaikoAutoGenerator : AutoGenerator
+ public class TaikoAutoGenerator : AutoGenerator
{
+ public new TaikoBeatmap Beatmap => (TaikoBeatmap)base.Beatmap;
+
private const double swell_hit_speed = 50;
- public TaikoAutoGenerator(Beatmap beatmap)
+ public TaikoAutoGenerator(IBeatmap beatmap)
: base(beatmap)
{
Replay = new Replay();
diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs
index 73cd9ba82190..442cca49f86a 100644
--- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs
+++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs
@@ -32,8 +32,8 @@ internal class TaikoScoreProcessor : ScoreProcessor
///
private double hpMissMultiplier;
- public TaikoScoreProcessor(RulesetContainer rulesetContainer)
- : base(rulesetContainer)
+ public TaikoScoreProcessor(DrawableRuleset drawableRuleset)
+ : base(drawableRuleset)
{
}
diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
index 7851a2f91999..3e94775eb654 100644
--- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
@@ -11,6 +11,7 @@
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Replays.Types;
+using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Replays;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Difficulty;
@@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko
{
public class TaikoRuleset : Ruleset
{
- public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap) => new TaikoRulesetContainer(this, beatmap);
+ public override DrawableRuleset CreateDrawableRulesetWith(WorkingBeatmap beatmap) => new DrawableTaikoRuleset(this, beatmap);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap);
public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[]
@@ -99,6 +100,11 @@ public override IEnumerable GetModsFor(ModType type)
new MultiMod(new TaikoModAutoplay(), new ModCinema()),
new TaikoModRelax(),
};
+ case ModType.Fun:
+ return new Mod[]
+ {
+ new MultiMod(new ModWindUp(), new ModWindDown())
+ };
default:
return new Mod[] { };
}
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
index 90841f11f599..943adaed4b5e 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
@@ -39,12 +39,10 @@ private void load(OsuColour colours)
}
}
- protected override void LoadComplete()
+ protected override void ApplyHitAnimations()
{
- if (Result.IsHit)
- this.MoveToY(-100, 500);
-
- base.LoadComplete();
+ this.MoveToY(-100, 500);
+ base.ApplyHitAnimations();
}
}
}
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
similarity index 92%
rename from osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs
rename to osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
index 7a73f4bd2ae9..899b91863e3e 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
@@ -20,13 +20,13 @@
namespace osu.Game.Rulesets.Taiko.UI
{
- public class TaikoRulesetContainer : ScrollingRulesetContainer
+ public class DrawableTaikoRuleset : DrawableScrollingRuleset
{
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
protected override bool UserScrollSpeedAdjustment => false;
- public TaikoRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
+ public DrawableTaikoRuleset(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
Direction.Value = ScrollingDirection.Left;
@@ -81,7 +81,7 @@ private void loadBarLines()
public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(this);
- public override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);
+ protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);
protected override Playfield CreatePlayfield() => new TaikoPlayfield(Beatmap.ControlPointInfo);
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
index cb527adb98e0..35b941b52bda 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Taiko.UI
public class TaikoPlayfield : ScrollingPlayfield
{
///
- /// Default height of a when inside a .
+ /// Default height of a when inside a .
///
public const float DEFAULT_HEIGHT = 178;
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index 171ba91ada57..02dff6993d81 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -333,6 +333,7 @@ public void TestDecodeHitObjectFileSamples()
Assert.AreEqual("hit_2.wav", getTestableSampleInfo(hitObjects[1]).LookupNames.First());
Assert.AreEqual("normal-hitnormal2", getTestableSampleInfo(hitObjects[2]).LookupNames.First());
Assert.AreEqual("hit_1.wav", getTestableSampleInfo(hitObjects[3]).LookupNames.First());
+ Assert.AreEqual(70, getTestableSampleInfo(hitObjects[3]).Volume);
}
SampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]);
diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs
index d5ab0e43d1e7..a867ddebaefb 100644
--- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs
@@ -113,6 +113,7 @@ public void TestDecodeHitObjects()
[TestCase(normal)]
[TestCase(marathon)]
+ [Ignore("temporarily disabled pending DeepEqual fix (https://github.com/jamesfoster/DeepEqual/pull/35)")]
// Currently fails:
// [TestCase(with_sb)]
public void TestParity(string beatmap)
diff --git a/osu.Game.Tests/Beatmaps/Formats/ParsingTest.cs b/osu.Game.Tests/Beatmaps/Formats/ParsingTest.cs
new file mode 100644
index 000000000000..b3863bcf4497
--- /dev/null
+++ b/osu.Game.Tests/Beatmaps/Formats/ParsingTest.cs
@@ -0,0 +1,72 @@
+// 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 NUnit.Framework;
+using osu.Game.Beatmaps.Formats;
+
+namespace osu.Game.Tests.Beatmaps.Formats
+{
+ [TestFixture]
+ public class ParsingTest
+ {
+ [Test]
+ public void TestNaNHandling() => allThrow("NaN");
+
+ [Test]
+ public void TestBadStringHandling() => allThrow("Random string 123");
+
+ [TestCase(Parsing.MAX_PARSE_VALUE)]
+ [TestCase(-1)]
+ [TestCase(0)]
+ [TestCase(1)]
+ [TestCase(-Parsing.MAX_PARSE_VALUE)]
+ [TestCase(10, 10)]
+ [TestCase(-10, 10)]
+ public void TestValidRanges(double input, double limit = Parsing.MAX_PARSE_VALUE)
+ {
+ Assert.AreEqual(Parsing.ParseInt((input).ToString(CultureInfo.InvariantCulture), (int)limit), (int)input);
+ Assert.AreEqual(Parsing.ParseFloat((input).ToString(CultureInfo.InvariantCulture), (float)limit), (float)input);
+ Assert.AreEqual(Parsing.ParseDouble((input).ToString(CultureInfo.InvariantCulture), limit), input);
+ }
+
+ [TestCase(double.PositiveInfinity)]
+ [TestCase(double.NegativeInfinity)]
+ [TestCase(999999999999)]
+ [TestCase(Parsing.MAX_PARSE_VALUE * 1.1)]
+ [TestCase(-Parsing.MAX_PARSE_VALUE * 1.1)]
+ [TestCase(11, 10)]
+ [TestCase(-11, 10)]
+ public void TestOutOfRangeHandling(double input, double limit = Parsing.MAX_PARSE_VALUE)
+ => allThrow(input.ToString(CultureInfo.InvariantCulture), limit);
+
+ private void allThrow(string input, double limit = Parsing.MAX_PARSE_VALUE)
+ where T : Exception
+ {
+ Assert.Throws(getIntParseException(input) ?? typeof(T), () => Parsing.ParseInt(input, (int)limit));
+ Assert.Throws(() => Parsing.ParseFloat(input, (float)limit));
+ Assert.Throws(() => Parsing.ParseDouble(input, limit));
+ }
+
+ ///
+ /// may not be able to parse some inputs.
+ /// In this case we expect to receive the raw parsing exception.
+ ///
+ /// The input attempting to be parsed.
+ /// The type of exception thrown by . Null if no exception is thrown.
+ private Type getIntParseException(string input)
+ {
+ try
+ {
+ var _ = int.Parse(input);
+ }
+ catch (Exception e)
+ {
+ return e.GetType();
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
index 5b8bdd8a51ba..f020c2a805a1 100644
--- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
+++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs
@@ -207,6 +207,96 @@ public void TestImportThenDeleteThenImport()
}
}
+ [TestCase(true)]
+ [TestCase(false)]
+ public void TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set)
+ {
+ //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"TestImportThenDeleteThenImport-{set}"))
+ {
+ try
+ {
+ var osu = loadOsu(host);
+
+ var imported = LoadOszIntoOsu(osu);
+
+ if (set)
+ imported.OnlineBeatmapSetID = 1234;
+ else
+ imported.Beatmaps.First().OnlineBeatmapID = 1234;
+
+ osu.Dependencies.Get().Update(imported);
+
+ deleteBeatmapSet(imported, osu);
+
+ var importedSecondTime = LoadOszIntoOsu(osu);
+
+ // check the newly "imported" beatmap has been reimported due to mismatch (even though hashes matched)
+ Assert.IsTrue(imported.ID != importedSecondTime.ID);
+ Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID);
+ }
+ finally
+ {
+ host.Exit();
+ }
+ }
+ }
+
+ [Test]
+ public void TestImportWithDuplicateBeatmapIDs()
+ {
+ //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWithDuplicateBeatmapID"))
+ {
+ try
+ {
+ var osu = loadOsu(host);
+
+ var metadata = new BeatmapMetadata
+ {
+ Artist = "SomeArtist",
+ AuthorString = "SomeAuthor"
+ };
+
+ var difficulty = new BeatmapDifficulty();
+
+ var toImport = new BeatmapSetInfo
+ {
+ OnlineBeatmapSetID = 1,
+ Metadata = metadata,
+ Beatmaps = new List
+ {
+ new BeatmapInfo
+ {
+ OnlineBeatmapID = 2,
+ Metadata = metadata,
+ BaseDifficulty = difficulty
+ },
+ new BeatmapInfo
+ {
+ OnlineBeatmapID = 2,
+ Metadata = metadata,
+ Status = BeatmapSetOnlineStatus.Loved,
+ BaseDifficulty = difficulty
+ }
+ }
+ };
+
+ var manager = osu.Dependencies.Get();
+
+ var imported = manager.Import(toImport);
+
+ Assert.NotNull(imported);
+ Assert.AreEqual(null, imported.Beatmaps[0].OnlineBeatmapID);
+ Assert.AreEqual(null, imported.Beatmaps[1].OnlineBeatmapID);
+ }
+ finally
+ {
+ host.Exit();
+ }
+ }
+ }
+
[Test]
[NonParallelizable]
[Ignore("Binding IPC on Appveyor isn't working (port in use). Need to figure out why")]
diff --git a/osu.Game.Tests/Resources/hitobject-file-samples.osu b/osu.Game.Tests/Resources/hitobject-file-samples.osu
index 588672e2d928..7c69f259b8c5 100644
--- a/osu.Game.Tests/Resources/hitobject-file-samples.osu
+++ b/osu.Game.Tests/Resources/hitobject-file-samples.osu
@@ -13,4 +13,4 @@ SampleSet: Normal
255,193,2170,1,0,0:0:0:0:hit_1.wav
256,191,2638,5,0,0:0:0:0:hit_2.wav
255,193,3107,1,0,0:0:0:0:
-256,191,3576,1,0,0:0:0:0:hit_1.wav
+256,191,3576,1,0,0:0:0:70:hit_1.wav
diff --git a/osu.Game.Tests/Visual/TestCaseAccountCreationOverlay.cs b/osu.Game.Tests/Visual/TestCaseAccountCreationOverlay.cs
index 543a43b43905..24380645d1a9 100644
--- a/osu.Game.Tests/Visual/TestCaseAccountCreationOverlay.cs
+++ b/osu.Game.Tests/Visual/TestCaseAccountCreationOverlay.cs
@@ -3,9 +3,13 @@
using System;
using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Overlays.AccountCreation;
+using osu.Game.Users;
namespace osu.Game.Tests.Visual
{
@@ -21,12 +25,32 @@ public class TestCaseAccountCreationOverlay : OsuTestCase
typeof(AccountCreationScreen),
};
+ [Cached(typeof(IAPIProvider))]
+ private DummyAPIAccess api = new DummyAPIAccess();
+
public TestCaseAccountCreationOverlay()
{
- var accountCreation = new AccountCreationOverlay();
- Child = accountCreation;
+ Container userPanelArea;
+ AccountCreationOverlay accountCreation;
+
+ Children = new Drawable[]
+ {
+ api,
+ accountCreation = new AccountCreationOverlay(),
+ userPanelArea = new Container
+ {
+ Padding = new MarginPadding(10),
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ },
+ };
+
+ api.Logout();
+ api.LocalUser.BindValueChanged(user => { userPanelArea.Child = new UserPanel(user.NewValue) { Width = 200 }; }, true);
- accountCreation.State = Visibility.Visible;
+ AddStep("show", () => accountCreation.State = Visibility.Visible);
+ AddStep("logout", () => api.Logout());
}
}
}
diff --git a/osu.Game.Tests/Visual/TestCaseAllPlayers.cs b/osu.Game.Tests/Visual/TestCaseAllPlayers.cs
deleted file mode 100644
index a5decaa9fb24..000000000000
--- a/osu.Game.Tests/Visual/TestCaseAllPlayers.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using NUnit.Framework;
-
-namespace osu.Game.Tests.Visual
-{
- [TestFixture]
- public class TestCaseAllPlayers : TestCasePlayer
- {
- }
-}
diff --git a/osu.Game.Tests/Visual/TestCaseAutoplay.cs b/osu.Game.Tests/Visual/TestCaseAutoplay.cs
index 61339a6af8d2..4d6a0ae7b806 100644
--- a/osu.Game.Tests/Visual/TestCaseAutoplay.cs
+++ b/osu.Game.Tests/Visual/TestCaseAutoplay.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using System.ComponentModel;
using System.Linq;
using osu.Game.Rulesets;
@@ -11,7 +10,7 @@
namespace osu.Game.Tests.Visual
{
[Description("Player instantiated with an autoplay mod.")]
- public class TestCaseAutoplay : TestCasePlayer
+ public class TestCaseAutoplay : AllPlayersTestCase
{
protected override Player CreatePlayer(Ruleset ruleset)
{
@@ -24,11 +23,10 @@ protected override Player CreatePlayer(Ruleset ruleset)
};
}
- protected override void AddCheckSteps(Func player)
+ protected override void AddCheckSteps()
{
- base.AddCheckSteps(player);
- AddUntilStep(() => ((ScoreAccessiblePlayer)player()).ScoreProcessor.TotalScore.Value > 0, "score above zero");
- AddUntilStep(() => ((ScoreAccessiblePlayer)player()).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 0), "key counter counted keys");
+ AddUntilStep("score above zero", () => ((ScoreAccessiblePlayer)Player).ScoreProcessor.TotalScore.Value > 0);
+ AddUntilStep("key counter counted keys", () => ((ScoreAccessiblePlayer)Player).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 0));
}
private class ScoreAccessiblePlayer : Player
diff --git a/osu.Game.Tests/Visual/TestCaseBackgroundScreenBeatmap.cs b/osu.Game.Tests/Visual/TestCaseBackgroundScreenBeatmap.cs
index d850c58f09b3..ba3a02a843e2 100644
--- a/osu.Game.Tests/Visual/TestCaseBackgroundScreenBeatmap.cs
+++ b/osu.Game.Tests/Visual/TestCaseBackgroundScreenBeatmap.cs
@@ -48,8 +48,8 @@ public class TestCaseBackgroundScreenBeatmap : ManualInputManagerTestCase
};
private DummySongSelect songSelect;
- private DimAccessiblePlayerLoader playerLoader;
- private DimAccessiblePlayer player;
+ private TestPlayerLoader playerLoader;
+ private TestPlayer player;
private DatabaseContextFactory factory;
private BeatmapManager manager;
private RulesetStore rulesets;
@@ -74,31 +74,27 @@ private void load(GameHost host)
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, factory, rulesets, null, null, host, Beatmap.Default));
Dependencies.Cache(new OsuConfigManager(LocalStorage));
+ manager.Import(TestResources.GetTestBeatmapForImport());
+
Beatmap.SetDefault();
}
[SetUp]
- public virtual void SetUp()
+ public virtual void SetUp() => Schedule(() =>
{
- Schedule(() =>
- {
- manager.Delete(manager.GetAllUsableBeatmapSets());
- var temp = TestResources.GetTestBeatmapForImport();
- manager.Import(temp);
- Child = screenStackContainer = new ScreenStackCacheContainer { RelativeSizeAxes = Axes.Both };
- screenStackContainer.ScreenStack.Push(songSelect = new DummySongSelect());
- });
- }
+ Child = screenStackContainer = new ScreenStackCacheContainer { RelativeSizeAxes = Axes.Both };
+ screenStackContainer.ScreenStack.Push(songSelect = new DummySongSelect());
+ });
///
- /// Check if properly triggers background dim previews when a user hovers over the visual settings panel.
+ /// Check if properly triggers the visual settings preview when a user hovers over the visual settings panel.
///
[Test]
public void PlayerLoaderSettingsHoverTest()
{
setupUserSettings();
- AddStep("Start player loader", () => songSelect.Push(playerLoader = new DimAccessiblePlayerLoader(player = new DimAccessiblePlayer())));
- AddUntilStep(() => playerLoader?.IsLoaded ?? false, "Wait for Player Loader to load");
+ AddStep("Start player loader", () => songSelect.Push(playerLoader = new TestPlayerLoader(player = new TestPlayer())));
+ AddUntilStep("Wait for Player Loader to load", () => playerLoader?.IsLoaded ?? false);
AddAssert("Background retained from song select", () => songSelect.IsBackgroundCurrent());
AddStep("Trigger background preview", () =>
{
@@ -106,16 +102,16 @@ public void PlayerLoaderSettingsHoverTest()
InputManager.MoveMouseTo(playerLoader.VisualSettingsPos);
});
waitForDim();
- AddAssert("Screen is dimmed", () => songSelect.IsBackgroundDimmed());
+ AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddStep("Stop background preview", () => InputManager.MoveMouseTo(playerLoader.ScreenPos));
waitForDim();
- AddAssert("Screen is undimmed", () => songSelect.IsBackgroundUndimmed());
+ AddAssert("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && playerLoader.IsBlurCorrect());
}
///
/// In the case of a user triggering the dim preview the instant player gets loaded, then moving the cursor off of the visual settings:
- /// The OnHover of PlayerLoader will trigger, which could potentially trigger an undim unless checked for in PlayerLoader.
- /// We need to check that in this scenario, the dim is still properly applied after entering player.
+ /// The OnHover of PlayerLoader will trigger, which could potentially cause visual settings to be unapplied unless checked for in PlayerLoader.
+ /// We need to check that in this scenario, the dim and blur is still properly applied after entering player.
///
[Test]
public void PlayerLoaderTransitionTest()
@@ -124,7 +120,7 @@ public void PlayerLoaderTransitionTest()
AddStep("Trigger hover event", () => playerLoader.TriggerOnHover());
AddAssert("Background retained from song select", () => songSelect.IsBackgroundCurrent());
waitForDim();
- AddAssert("Screen is dimmed and unblurred", () => songSelect.IsBackgroundDimmed() && songSelect.IsBackgroundUnblurred());
+ AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
}
///
@@ -165,51 +161,54 @@ public void StoryboardTransitionTest()
}
///
- /// Check if the is properly accepting user dim changes at all.
+ /// Check if the is properly accepting user-defined visual changes at all.
///
[Test]
public void DisableUserDimTest()
{
performFullSetup();
waitForDim();
- AddAssert("Screen is dimmed", () => songSelect.IsBackgroundDimmed());
+ AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddStep("EnableUserDim disabled", () => songSelect.DimEnabled.Value = false);
waitForDim();
- AddAssert("Screen is undimmed", () => songSelect.IsBackgroundUndimmed());
+ AddAssert("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsUserBlurDisabled());
AddStep("EnableUserDim enabled", () => songSelect.DimEnabled.Value = true);
waitForDim();
- AddAssert("Screen is dimmed", () => songSelect.IsBackgroundDimmed());
+ AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
}
///
- /// Check if the fade container retains dim when pausing
+ /// Check if the visual settings container retains dim and blur when pausing
///
[Test]
public void PauseTest()
{
performFullSetup(true);
- AddStep("Pause", () => player.CurrentPauseContainer.Pause());
+ AddStep("Pause", () => player.Pause());
waitForDim();
- AddAssert("Screen is dimmed", () => songSelect.IsBackgroundDimmed());
- AddStep("Unpause", () => player.CurrentPauseContainer.Resume());
+ AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
+ AddStep("Unpause", () => player.Resume());
waitForDim();
- AddAssert("Screen is dimmed", () => songSelect.IsBackgroundDimmed());
+ AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
}
///
- /// Check if the fade container removes user dim when suspending for
+ /// Check if the visual settings container removes user dim when suspending for
///
[Test]
public void TransitionTest()
{
performFullSetup();
- AddStep("Transition to Results", () => player.Push(new FadeAccessibleResults(new ScoreInfo { User = new User { Username = "osu!" } })));
+ var results = new FadeAccessibleResults(new ScoreInfo { User = new User { Username = "osu!" } });
+ AddStep("Transition to Results", () => player.Push(results));
+ AddUntilStep("Wait for results is current", results.IsCurrentScreen);
waitForDim();
- AddAssert("Screen is undimmed and is original background", () => songSelect.IsBackgroundUndimmed() && songSelect.IsBackgroundCurrent());
+ AddAssert("Screen is undimmed, original background retained", () =>
+ songSelect.IsBackgroundUndimmed() && songSelect.IsBackgroundCurrent() && results.IsBlurCorrect());
}
///
- /// Check if background gets undimmed when leaving for
+ /// Check if background gets undimmed and unblurred when leaving for
///
[Test]
public void TransitionOutTest()
@@ -217,10 +216,26 @@ public void TransitionOutTest()
performFullSetup();
AddStep("Exit to song select", () => player.Exit());
waitForDim();
- AddAssert("Screen is undimmed", () => songSelect.IsBackgroundUndimmed());
+ AddAssert("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsBlurCorrect());
}
- private void waitForDim() => AddWaitStep(5, "Wait for dim");
+ ///
+ /// Check if hovering on the visual settings dialogue after resuming from player still previews the background dim.
+ ///
+ [Test]
+ public void ResumeFromPlayerTest()
+ {
+ performFullSetup();
+ AddStep("Move mouse to Visual Settings", () => InputManager.MoveMouseTo(playerLoader.VisualSettingsPos));
+ AddStep("Resume PlayerLoader", () => player.Restart());
+ waitForDim();
+ AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
+ AddStep("Move mouse to center of screen", () => InputManager.MoveMouseTo(playerLoader.ScreenPos));
+ waitForDim();
+ AddAssert("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && playerLoader.IsBlurCorrect());
+ }
+
+ private void waitForDim() => AddWaitStep("Wait for dim", 5);
private void createFakeStoryboard() => AddStep("Create storyboard", () =>
{
@@ -243,25 +258,25 @@ private void performFullSetup(bool allowPause = false)
AddStep("Start player loader", () =>
{
- songSelect.Push(playerLoader = new DimAccessiblePlayerLoader(player = new DimAccessiblePlayer
+ songSelect.Push(playerLoader = new TestPlayerLoader(player = new TestPlayer
{
AllowPause = allowPause,
Ready = true,
}));
});
- AddUntilStep(() => playerLoader.IsLoaded, "Wait for Player Loader to load");
+ AddUntilStep("Wait for Player Loader to load", () => playerLoader.IsLoaded);
AddStep("Move mouse to center of screen", () => InputManager.MoveMouseTo(playerLoader.ScreenPos));
- AddUntilStep(() => player.IsLoaded, "Wait for player to load");
+ AddUntilStep("Wait for player to load", () => player.IsLoaded);
}
private void setupUserSettings()
{
- AddUntilStep(() => songSelect.Carousel.SelectedBeatmap != null, "Song select has selection");
+ AddUntilStep("Song select has selection", () => songSelect.Carousel.SelectedBeatmap != null);
AddStep("Set default user settings", () =>
{
Beatmap.Value.Mods.Value = Beatmap.Value.Mods.Value.Concat(new[] { new OsuModNoFail() });
songSelect.DimLevel.Value = 0.7f;
- songSelect.BlurLevel.Value = 0.0f;
+ songSelect.BlurLevel.Value = 0.4f;
});
}
@@ -289,14 +304,18 @@ private void load(OsuConfigManager config)
public bool IsBackgroundDimmed() => ((FadeAccessibleBackground)Background).CurrentColour == OsuColour.Gray(1 - (float)DimLevel.Value);
- public bool IsBackgroundUnblurred() => ((FadeAccessibleBackground)Background).CurrentBlur == new Vector2(0);
-
public bool IsBackgroundUndimmed() => ((FadeAccessibleBackground)Background).CurrentColour == Color4.White;
+ public bool IsUserBlurApplied() => ((FadeAccessibleBackground)Background).CurrentBlur == new Vector2((float)BlurLevel.Value * 25);
+
+ public bool IsUserBlurDisabled() => ((FadeAccessibleBackground)Background).CurrentBlur == new Vector2(0);
+
public bool IsBackgroundInvisible() => ((FadeAccessibleBackground)Background).CurrentAlpha == 0;
public bool IsBackgroundVisible() => ((FadeAccessibleBackground)Background).CurrentAlpha == 1;
+ public bool IsBlurCorrect() => ((FadeAccessibleBackground)Background).CurrentBlur == new Vector2(BACKGROUND_BLUR);
+
///
/// Make sure every time a screen gets pushed, the background doesn't get replaced
///
@@ -312,9 +331,11 @@ public FadeAccessibleResults(ScoreInfo score)
}
protected override BackgroundScreen CreateBackground() => new FadeAccessibleBackground(Beatmap.Value);
+
+ public bool IsBlurCorrect() => ((FadeAccessibleBackground)Background).CurrentBlur == new Vector2(BACKGROUND_BLUR);
}
- private class DimAccessiblePlayer : Player
+ private class TestPlayer : Player
{
protected override BackgroundScreen CreateBackground() => new FadeAccessibleBackground(Beatmap.Value);
@@ -328,8 +349,6 @@ protected override UserDimContainer CreateStoryboardContainer()
};
}
- public PauseContainer CurrentPauseContainer => PauseContainer;
-
public UserDimContainer CurrentStoryboardContainer => StoryboardContainer;
// Whether or not the player should be allowed to load.
@@ -350,7 +369,7 @@ private void load(OsuConfigManager config)
Thread.Sleep(1);
StoryboardEnabled = config.GetBindable(OsuSetting.ShowStoryboard);
ReplacesBackground.BindTo(Background.StoryboardReplacesBackground);
- RulesetContainer.IsPaused.BindTo(IsPaused);
+ DrawableRuleset.IsPaused.BindTo(IsPaused);
}
}
@@ -368,18 +387,20 @@ public ScreenStackCacheContainer()
}
}
- private class DimAccessiblePlayerLoader : PlayerLoader
+ private class TestPlayerLoader : PlayerLoader
{
public VisualSettings VisualSettingsPos => VisualSettings;
public BackgroundScreen ScreenPos => Background;
- public DimAccessiblePlayerLoader(Player player)
+ public TestPlayerLoader(Player player)
: base(() => player)
{
}
public void TriggerOnHover() => OnHover(new HoverEvent(new InputState()));
+ public bool IsBlurCorrect() => ((FadeAccessibleBackground)Background).CurrentBlur == new Vector2(BACKGROUND_BLUR);
+
protected override BackgroundScreen CreateBackground() => new FadeAccessibleBackground(Beatmap.Value);
}
@@ -388,6 +409,7 @@ private class FadeAccessibleBackground : BackgroundScreenBeatmap
protected override UserDimContainer CreateFadeContainer() => fadeContainer = new TestUserDimContainer { RelativeSizeAxes = Axes.Both };
public Color4 CurrentColour => fadeContainer.CurrentColour;
+
public float CurrentAlpha => fadeContainer.CurrentAlpha;
public Vector2 CurrentBlur => Background.BlurSigma;
diff --git a/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs b/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs
index 618d8376c071..956d84618c4f 100644
--- a/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs
+++ b/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs
@@ -87,7 +87,7 @@ private void loadBeatmaps(List beatmapSets)
carousel.BeatmapSetsChanged = () => changed = true;
carousel.BeatmapSets = beatmapSets;
});
- AddUntilStep(() => changed, "Wait for load");
+ AddUntilStep("Wait for load", () => changed);
}
private void ensureRandomFetchSuccess() =>
@@ -214,7 +214,7 @@ private void testFiltering()
checkSelected(3, 2);
AddStep("Un-filter (debounce)", () => carousel.Filter(new FilterCriteria()));
- AddUntilStep(() => !carousel.PendingFilterTask, "Wait for debounce");
+ AddUntilStep("Wait for debounce", () => !carousel.PendingFilterTask);
checkVisibleItemCount(diff: false, count: set_count);
checkVisibleItemCount(diff: true, count: 3);
@@ -327,13 +327,13 @@ private void testRemoveAll()
AddStep("Remove first", () => carousel.RemoveBeatmapSet(carousel.BeatmapSets.First()));
checkSelected(1);
- AddUntilStep(() =>
+ AddUntilStep("Remove all", () =>
{
if (!carousel.BeatmapSets.Any()) return true;
carousel.RemoveBeatmapSet(carousel.BeatmapSets.Last());
return false;
- }, "Remove all");
+ });
checkNoSelection();
}
diff --git a/osu.Game.Tests/Visual/TestCaseBeatmapDetailArea.cs b/osu.Game.Tests/Visual/TestCaseBeatmapDetailArea.cs
index c23075a12781..6cc3982f9c4f 100644
--- a/osu.Game.Tests/Visual/TestCaseBeatmapDetailArea.cs
+++ b/osu.Game.Tests/Visual/TestCaseBeatmapDetailArea.cs
@@ -1,8 +1,12 @@
// 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.Graphics;
+using osu.Game.Beatmaps;
using osu.Game.Screens.Select;
using osuTK;
@@ -12,14 +16,145 @@ namespace osu.Game.Tests.Visual
[System.ComponentModel.Description("PlaySongSelect leaderboard/details area")]
public class TestCaseBeatmapDetailArea : OsuTestCase
{
+ public override IReadOnlyList RequiredTypes => new[] { typeof(BeatmapDetails) };
+
public TestCaseBeatmapDetailArea()
{
- Add(new BeatmapDetailArea
+ BeatmapDetailArea detailsArea;
+ Add(detailsArea = new BeatmapDetailArea
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(550f, 450f),
});
+
+ AddStep("all metrics", () => detailsArea.Beatmap = new DummyWorkingBeatmap
+ {
+ BeatmapInfo =
+ {
+ Version = "All Metrics",
+ Metadata = new BeatmapMetadata
+ {
+ Source = "osu!lazer",
+ Tags = "this beatmap has all the metrics",
+ },
+ BaseDifficulty = new BeatmapDifficulty
+ {
+ CircleSize = 7,
+ DrainRate = 1,
+ OverallDifficulty = 5.7f,
+ ApproachRate = 3.5f,
+ },
+ StarDifficulty = 5.3f,
+ Metrics = new BeatmapMetrics
+ {
+ Ratings = Enumerable.Range(0, 11),
+ Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6),
+ Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6),
+ },
+ }
+ }
+ );
+
+ AddStep("all except source", () => detailsArea.Beatmap = new DummyWorkingBeatmap
+ {
+ BeatmapInfo =
+ {
+ Version = "All Metrics",
+ Metadata = new BeatmapMetadata
+ {
+ Tags = "this beatmap has all the metrics",
+ },
+ BaseDifficulty = new BeatmapDifficulty
+ {
+ CircleSize = 7,
+ DrainRate = 1,
+ OverallDifficulty = 5.7f,
+ ApproachRate = 3.5f,
+ },
+ StarDifficulty = 5.3f,
+ Metrics = new BeatmapMetrics
+ {
+ Ratings = Enumerable.Range(0, 11),
+ Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6),
+ Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6),
+ },
+ }
+ });
+
+ AddStep("ratings", () => detailsArea.Beatmap = new DummyWorkingBeatmap
+ {
+ BeatmapInfo =
+ {
+ Version = "Only Ratings",
+ Metadata = new BeatmapMetadata
+ {
+ Source = "osu!lazer",
+ Tags = "this beatmap has ratings metrics but not retries or fails",
+ },
+ BaseDifficulty = new BeatmapDifficulty
+ {
+ CircleSize = 6,
+ DrainRate = 9,
+ OverallDifficulty = 6,
+ ApproachRate = 6,
+ },
+ StarDifficulty = 4.8f,
+ Metrics = new BeatmapMetrics
+ {
+ Ratings = Enumerable.Range(0, 11),
+ },
+ }
+ });
+
+ AddStep("fails+retries", () => detailsArea.Beatmap = new DummyWorkingBeatmap
+ {
+ BeatmapInfo =
+ {
+ Version = "Only Retries and Fails",
+ Metadata = new BeatmapMetadata
+ {
+ Source = "osu!lazer",
+ Tags = "this beatmap has retries and fails but no ratings",
+ },
+ BaseDifficulty = new BeatmapDifficulty
+ {
+ CircleSize = 3.7f,
+ DrainRate = 6,
+ OverallDifficulty = 6,
+ ApproachRate = 7,
+ },
+ StarDifficulty = 2.91f,
+ Metrics = new BeatmapMetrics
+ {
+ Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6),
+ Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6),
+ },
+ }
+ });
+
+ AddStep("null metrics", () => detailsArea.Beatmap = new DummyWorkingBeatmap
+ {
+ BeatmapInfo =
+ {
+ Version = "No Metrics",
+ Metadata = new BeatmapMetadata
+ {
+ Source = "osu!lazer",
+ Tags = "this beatmap has no metrics",
+ },
+ BaseDifficulty = new BeatmapDifficulty
+ {
+ CircleSize = 5,
+ DrainRate = 5,
+ OverallDifficulty = 5.5f,
+ ApproachRate = 6.5f,
+ },
+ StarDifficulty = 1.97f,
+ }
+ });
+
+ AddStep("null beatmap", () => detailsArea.Beatmap = null);
}
}
}
diff --git a/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs
index 31bb8b64a35d..0d77ac666b51 100644
--- a/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs
+++ b/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs
@@ -56,11 +56,11 @@ protected override void LoadComplete()
// select part is redundant, but wait for load isn't
selectBeatmap(Beatmap.Value.Beatmap);
- AddWaitStep(3);
+ AddWaitStep("wait for select", 3);
AddStep("hide", () => { infoWedge.State = Visibility.Hidden; });
- AddWaitStep(3);
+ AddWaitStep("wait for hide", 3);
AddStep("show", () => { infoWedge.State = Visibility.Visible; });
@@ -135,7 +135,7 @@ private void selectBeatmap([CanBeNull] IBeatmap b)
infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : new TestWorkingBeatmap(b);
});
- AddUntilStep(() => infoWedge.Info != infoBefore, "wait for async load");
+ AddUntilStep("wait for async load", () => infoWedge.Info != infoBefore);
}
private IBeatmap createTestBeatmap(RulesetInfo ruleset)
diff --git a/osu.Game.Tests/Visual/TestCaseChannelTabControl.cs b/osu.Game.Tests/Visual/TestCaseChannelTabControl.cs
index 749303b1bb45..e90b5f53728f 100644
--- a/osu.Game.Tests/Visual/TestCaseChannelTabControl.cs
+++ b/osu.Game.Tests/Visual/TestCaseChannelTabControl.cs
@@ -90,7 +90,7 @@ public TestCaseChannelTabControl()
AddStep("set second channel", () => channelTabControl.Current.Value = channelTabControl.Items.Skip(1).First());
AddAssert("selector tab is inactive", () => !channelTabControl.ChannelSelectorActive.Value);
- AddUntilStep(() =>
+ AddUntilStep("remove all channels", () =>
{
var first = channelTabControl.Items.First();
if (first.Name == "+")
@@ -98,7 +98,7 @@ public TestCaseChannelTabControl()
channelTabControl.RemoveChannel(first);
return false;
- }, "remove all channels");
+ });
AddAssert("selector tab is active", () => channelTabControl.ChannelSelectorActive.Value);
}
diff --git a/osu.Game.Tests/Visual/TestCaseChatLink.cs b/osu.Game.Tests/Visual/TestCaseChatLink.cs
index b2ec2c9b4732..ecab64ccf3ba 100644
--- a/osu.Game.Tests/Visual/TestCaseChatLink.cs
+++ b/osu.Game.Tests/Visual/TestCaseChatLink.cs
@@ -159,7 +159,7 @@ void addEchoWithWait(string text, string completeText = null, double delay = 250
Scheduler.AddDelayed(() => newLine.Message = new DummyMessage(completeText ?? text), delay);
});
- AddUntilStep(() => textContainer.All(line => line.Message is DummyMessage), $"wait for msg #{echoCounter}");
+ AddUntilStep($"wait for msg #{echoCounter}", () => textContainer.All(line => line.Message is DummyMessage));
}
}
diff --git a/osu.Game.Tests/Visual/TestCaseDirectPanel.cs b/osu.Game.Tests/Visual/TestCaseDirectPanel.cs
new file mode 100644
index 000000000000..beb88ac56cb5
--- /dev/null
+++ b/osu.Game.Tests/Visual/TestCaseDirectPanel.cs
@@ -0,0 +1,47 @@
+// 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 osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Overlays.Direct;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Tests.Beatmaps;
+using osuTK;
+
+namespace osu.Game.Tests.Visual
+{
+ public class TestCaseDirectPanel : OsuTestCase
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(DirectGridPanel),
+ typeof(DirectListPanel),
+ typeof(IconPill)
+ };
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ var beatmap = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo, null);
+ beatmap.BeatmapSetInfo.OnlineInfo.HasVideo = true;
+ beatmap.BeatmapSetInfo.OnlineInfo.HasStoryboard = true;
+
+ Child = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Padding = new MarginPadding(20),
+ Spacing = new Vector2(0, 20),
+ Children = new Drawable[]
+ {
+ new DirectGridPanel(beatmap.BeatmapSetInfo),
+ new DirectListPanel(beatmap.BeatmapSetInfo)
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/TestCaseDisclaimer.cs b/osu.Game.Tests/Visual/TestCaseDisclaimer.cs
index 3ceb3eb4bd08..8bba16e4b406 100644
--- a/osu.Game.Tests/Visual/TestCaseDisclaimer.cs
+++ b/osu.Game.Tests/Visual/TestCaseDisclaimer.cs
@@ -2,16 +2,33 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Game.Online.API;
using osu.Game.Screens.Menu;
+using osu.Game.Users;
namespace osu.Game.Tests.Visual
{
public class TestCaseDisclaimer : ScreenTestCase
{
+ [Cached(typeof(IAPIProvider))]
+ private readonly DummyAPIAccess api = new DummyAPIAccess();
+
[BackgroundDependencyLoader]
private void load()
{
- LoadScreen(new Disclaimer());
+ Add(api);
+
+ AddStep("load disclaimer", () => LoadScreen(new Disclaimer()));
+
+ AddStep("toggle support", () =>
+ {
+ api.LocalUser.Value = new User
+ {
+ Username = api.LocalUser.Value.Username,
+ Id = api.LocalUser.Value.Id,
+ IsSupporter = !api.LocalUser.Value.IsSupporter,
+ };
+ });
}
}
}
diff --git a/osu.Game.Tests/Visual/TestCaseGameplayMenuOverlay.cs b/osu.Game.Tests/Visual/TestCaseGameplayMenuOverlay.cs
index a21573236a2f..0a240186d26b 100644
--- a/osu.Game.Tests/Visual/TestCaseGameplayMenuOverlay.cs
+++ b/osu.Game.Tests/Visual/TestCaseGameplayMenuOverlay.cs
@@ -7,8 +7,10 @@
using System.Linq;
using osuTK.Input;
using osu.Framework.Allocation;
+using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
+using osu.Game.Input.Bindings;
using osu.Game.Screens.Play;
using osuTK;
@@ -17,26 +19,34 @@ namespace osu.Game.Tests.Visual
[Description("player pause/fail screens")]
public class TestCaseGameplayMenuOverlay : ManualInputManagerTestCase
{
- public override IReadOnlyList RequiredTypes => new[] { typeof(FailOverlay), typeof(PauseContainer) };
+ public override IReadOnlyList RequiredTypes => new[] { typeof(FailOverlay), typeof(PauseOverlay) };
private FailOverlay failOverlay;
- private PauseContainer.PauseOverlay pauseOverlay;
+ private PauseOverlay pauseOverlay;
+
+ private GlobalActionContainer globalActionContainer;
[BackgroundDependencyLoader]
- private void load()
+ private void load(OsuGameBase game)
{
- Add(pauseOverlay = new PauseContainer.PauseOverlay
- {
- OnResume = () => Logger.Log(@"Resume"),
- OnRetry = () => Logger.Log(@"Retry"),
- OnQuit = () => Logger.Log(@"Quit"),
- });
-
- Add(failOverlay = new FailOverlay
+ Child = globalActionContainer = new GlobalActionContainer(game)
{
- OnRetry = () => Logger.Log(@"Retry"),
- OnQuit = () => Logger.Log(@"Quit"),
- });
+ Children = new Drawable[]
+ {
+ pauseOverlay = new PauseOverlay
+ {
+ OnResume = () => Logger.Log(@"Resume"),
+ OnRetry = () => Logger.Log(@"Retry"),
+ OnQuit = () => Logger.Log(@"Quit"),
+ },
+ failOverlay = new FailOverlay
+
+ {
+ OnRetry = () => Logger.Log(@"Retry"),
+ OnQuit = () => Logger.Log(@"Quit"),
+ }
+ }
+ };
var retryCount = 0;
@@ -79,12 +89,6 @@ private void testHideResets()
AddAssert("Overlay state is reset", () => !failOverlay.Buttons.Any(b => b.Selected.Value));
}
- private void press(Key key)
- {
- InputManager.PressKey(key);
- InputManager.ReleaseKey(key);
- }
-
///
/// Tests that pressing enter after an overlay shows doesn't trigger an event because a selection hasn't occurred.
///
@@ -92,7 +96,7 @@ private void testEnterWithoutSelection()
{
AddStep("Show overlay", () => pauseOverlay.Show());
- AddStep("Press enter", () => press(Key.Enter));
+ AddStep("Press select", () => press(GlobalAction.Select));
AddAssert("Overlay still open", () => pauseOverlay.State == Visibility.Visible);
AddStep("Hide overlay", () => pauseOverlay.Hide());
@@ -270,5 +274,17 @@ private void testEnterKeySelection()
});
AddAssert("Overlay is closed", () => pauseOverlay.State == Visibility.Hidden);
}
+
+ private void press(Key key)
+ {
+ InputManager.PressKey(key);
+ InputManager.ReleaseKey(key);
+ }
+
+ private void press(GlobalAction action)
+ {
+ globalActionContainer.TriggerPressed(action);
+ globalActionContainer.TriggerReleased(action);
+ }
}
}
diff --git a/osu.Game.Tests/Visual/TestCaseHoldForMenuButton.cs b/osu.Game.Tests/Visual/TestCaseHoldForMenuButton.cs
index 5ee1340044a3..a4fadbd3db31 100644
--- a/osu.Game.Tests/Visual/TestCaseHoldForMenuButton.cs
+++ b/osu.Game.Tests/Visual/TestCaseHoldForMenuButton.cs
@@ -32,9 +32,9 @@ private void load()
var text = holdForMenuButton.Children.OfType().First();
AddStep("Trigger text fade in", () => InputManager.MoveMouseTo(holdForMenuButton));
- AddUntilStep(() => text.IsPresent && !exitAction, "Text visible");
+ AddUntilStep("Text visible", () => text.IsPresent && !exitAction);
AddStep("Trigger text fade out", () => InputManager.MoveMouseTo(Vector2.One));
- AddUntilStep(() => !text.IsPresent && !exitAction, "Text is not visible");
+ AddUntilStep("Text is not visible", () => !text.IsPresent && !exitAction);
AddStep("Trigger exit action", () =>
{
@@ -47,7 +47,7 @@ private void load()
AddAssert("action not triggered", () => !exitAction);
AddStep("Trigger exit action", () => InputManager.PressButton(MouseButton.Left));
- AddUntilStep(() => exitAction, $"{nameof(holdForMenuButton.Action)} was triggered");
+ AddUntilStep($"{nameof(holdForMenuButton.Action)} was triggered", () => exitAction);
}
}
}
diff --git a/osu.Game.Tests/Visual/TestCaseHoldToConfirmOverlay.cs b/osu.Game.Tests/Visual/TestCaseHoldToConfirmOverlay.cs
index f66bf34875f3..c9a7e9c39f95 100644
--- a/osu.Game.Tests/Visual/TestCaseHoldToConfirmOverlay.cs
+++ b/osu.Game.Tests/Visual/TestCaseHoldToConfirmOverlay.cs
@@ -49,7 +49,7 @@ public TestCaseHoldToConfirmOverlay()
AddStep("start confirming", () => overlay.Begin());
- AddUntilStep(() => fired, "wait until confirmed");
+ AddUntilStep("wait until confirmed", () => fired);
}
private class TestHoldToConfirmOverlay : ExitConfirmOverlay
diff --git a/osu.Game.Tests/Visual/TestCaseIdleTracker.cs b/osu.Game.Tests/Visual/TestCaseIdleTracker.cs
index 8e8c4e38aecc..a7a1831ba706 100644
--- a/osu.Game.Tests/Visual/TestCaseIdleTracker.cs
+++ b/osu.Game.Tests/Visual/TestCaseIdleTracker.cs
@@ -59,7 +59,7 @@ public void TestNudge()
{
AddStep("move mouse to top left", () => InputManager.MoveMouseTo(box1.ScreenSpaceDrawQuad.Centre));
- AddUntilStep(() => box1.IsIdle && box2.IsIdle && box3.IsIdle && box4.IsIdle, "Wait for all idle");
+ AddUntilStep("Wait for all idle", () => box1.IsIdle && box2.IsIdle && box3.IsIdle && box4.IsIdle);
AddStep("nudge mouse", () => InputManager.MoveMouseTo(box1.ScreenSpaceDrawQuad.Centre + new Vector2(1)));
@@ -87,7 +87,7 @@ public void TestMovement()
AddAssert("check idle", () => !box3.IsIdle);
AddAssert("check idle", () => !box4.IsIdle);
- AddUntilStep(() => box1.IsIdle && box2.IsIdle && box3.IsIdle && box4.IsIdle, "Wait for all idle");
+ AddUntilStep("Wait for all idle", () => box1.IsIdle && box2.IsIdle && box3.IsIdle && box4.IsIdle);
}
[Test]
@@ -96,13 +96,13 @@ public void TestTimings()
AddStep("move mouse", () => InputManager.MoveMouseTo(ScreenSpaceDrawQuad.Centre));
AddAssert("check not idle", () => !box1.IsIdle && !box2.IsIdle && !box3.IsIdle && !box4.IsIdle);
- AddUntilStep(() => box1.IsIdle, "Wait for idle");
+ AddUntilStep("Wait for idle", () => box1.IsIdle);
AddAssert("check not idle", () => !box2.IsIdle && !box3.IsIdle && !box4.IsIdle);
- AddUntilStep(() => box2.IsIdle, "Wait for idle");
+ AddUntilStep("Wait for idle", () => box2.IsIdle);
AddAssert("check not idle", () => !box3.IsIdle && !box4.IsIdle);
- AddUntilStep(() => box3.IsIdle, "Wait for idle");
+ AddUntilStep("Wait for idle", () => box3.IsIdle);
- AddUntilStep(() => box1.IsIdle && box2.IsIdle && box3.IsIdle && box4.IsIdle, "Wait for all idle");
+ AddUntilStep("Wait for all idle", () => box1.IsIdle && box2.IsIdle && box3.IsIdle && box4.IsIdle);
}
private class IdleTrackingBox : CompositeDrawable
diff --git a/osu.Game.Tests/Visual/TestCaseLoaderAnimation.cs b/osu.Game.Tests/Visual/TestCaseLoaderAnimation.cs
index 2088f97580d3..3803764194d9 100644
--- a/osu.Game.Tests/Visual/TestCaseLoaderAnimation.cs
+++ b/osu.Game.Tests/Visual/TestCaseLoaderAnimation.cs
@@ -25,30 +25,30 @@ protected override void LoadComplete()
bool logoVisible = false;
AddStep("almost instant display", () => Child = loader = new TestLoader(250));
- AddUntilStep(() =>
+ AddUntilStep("loaded", () =>
{
logoVisible = loader.Logo?.Alpha > 0;
return loader.Logo != null && loader.ScreenLoaded;
- }, "loaded");
+ });
AddAssert("logo not visible", () => !logoVisible);
AddStep("short load", () => Child = loader = new TestLoader(800));
- AddUntilStep(() =>
+ AddUntilStep("loaded", () =>
{
logoVisible = loader.Logo?.Alpha > 0;
return loader.Logo != null && loader.ScreenLoaded;
- }, "loaded");
+ });
AddAssert("logo visible", () => logoVisible);
- AddUntilStep(() => loader.Logo?.Alpha == 0, "logo gone");
+ AddUntilStep("logo gone", () => loader.Logo?.Alpha == 0);
AddStep("longer load", () => Child = loader = new TestLoader(1400));
- AddUntilStep(() =>
+ AddUntilStep("loaded", () =>
{
logoVisible = loader.Logo?.Alpha > 0;
return loader.Logo != null && loader.ScreenLoaded;
- }, "loaded");
+ });
AddAssert("logo visible", () => logoVisible);
- AddUntilStep(() => loader.Logo?.Alpha == 0, "logo gone");
+ AddUntilStep("logo gone", () => loader.Logo?.Alpha == 0);
}
private class TestLoader : Loader
diff --git a/osu.Game.Tests/Visual/TestCaseMatchLeaderboard.cs b/osu.Game.Tests/Visual/TestCaseMatchLeaderboard.cs
index 42a886a5a34e..484a212a38ef 100644
--- a/osu.Game.Tests/Visual/TestCaseMatchLeaderboard.cs
+++ b/osu.Game.Tests/Visual/TestCaseMatchLeaderboard.cs
@@ -28,7 +28,7 @@ public TestCaseMatchLeaderboard()
}
[Resolved]
- private APIAccess api { get; set; }
+ private IAPIProvider api { get; set; }
[BackgroundDependencyLoader]
private void load()
diff --git a/osu.Game.Tests/Visual/TestCaseMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/TestCaseMatchSettingsOverlay.cs
index a320fc88fa01..11c7d3ef7024 100644
--- a/osu.Game.Tests/Visual/TestCaseMatchSettingsOverlay.cs
+++ b/osu.Game.Tests/Visual/TestCaseMatchSettingsOverlay.cs
@@ -111,7 +111,7 @@ public void TestCreationFailureDisplaysError()
settings.ApplyButton.Action.Invoke();
});
- AddUntilStep(() => !settings.ErrorText.IsPresent, "error not displayed");
+ AddUntilStep("error not displayed", () => !settings.ErrorText.IsPresent);
}
private class TestRoomSettings : MatchSettingsOverlay
diff --git a/osu.Game.Tests/Visual/TestCaseMods.cs b/osu.Game.Tests/Visual/TestCaseMods.cs
index 99bc10d8cc18..cb7e783bee22 100644
--- a/osu.Game.Tests/Visual/TestCaseMods.cs
+++ b/osu.Game.Tests/Visual/TestCaseMods.cs
@@ -208,22 +208,22 @@ private void testMultiplierTextColour(Mod mod, Color4 colour)
{
checkLabelColor(Color4.White);
selectNext(mod);
- AddWaitStep(1, "wait for changing colour");
+ AddWaitStep("wait for changing colour", 1);
checkLabelColor(colour);
selectPrevious(mod);
- AddWaitStep(1, "wait for changing colour");
+ AddWaitStep("wait for changing colour", 1);
checkLabelColor(Color4.White);
}
private void testRankedText(Mod mod)
{
- AddWaitStep(1, "wait for fade");
+ AddWaitStep("wait for fade", 1);
AddAssert("check for ranked", () => modSelect.UnrankedLabel.Alpha == 0);
selectNext(mod);
- AddWaitStep(1, "wait for fade");
+ AddWaitStep("wait for fade", 1);
AddAssert("check for unranked", () => modSelect.UnrankedLabel.Alpha != 0);
selectPrevious(mod);
- AddWaitStep(1, "wait for fade");
+ AddWaitStep("wait for fade", 1);
AddAssert("check for ranked", () => modSelect.UnrankedLabel.Alpha == 0);
}
diff --git a/osu.Game.Tests/Visual/TestCaseNotificationOverlay.cs b/osu.Game.Tests/Visual/TestCaseNotificationOverlay.cs
index d6a3361cf2cb..9e70df91b690 100644
--- a/osu.Game.Tests/Visual/TestCaseNotificationOverlay.cs
+++ b/osu.Game.Tests/Visual/TestCaseNotificationOverlay.cs
@@ -60,7 +60,7 @@ public TestCaseNotificationOverlay()
setState(Visibility.Hidden);
AddRepeatStep(@"add many simple", sendManyNotifications, 3);
- AddWaitStep(5);
+ AddWaitStep("wait some", 5);
checkProgressingCount(0);
@@ -70,7 +70,7 @@ public TestCaseNotificationOverlay()
AddAssert("Displayed count is 33", () => manager.UnreadCount.Value == 33);
- AddWaitStep(10);
+ AddWaitStep("wait some", 10);
checkProgressingCount(0);
diff --git a/osu.Game.Tests/Visual/TestCasePause.cs b/osu.Game.Tests/Visual/TestCasePause.cs
new file mode 100644
index 000000000000..d5d2cebbab41
--- /dev/null
+++ b/osu.Game.Tests/Visual/TestCasePause.cs
@@ -0,0 +1,152 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Screens;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Screens.Play;
+
+namespace osu.Game.Tests.Visual
+{
+ public class TestCasePause : PlayerTestCase
+ {
+ protected new PausePlayer Player => (PausePlayer)base.Player;
+
+ public TestCasePause()
+ : base(new OsuRuleset())
+ {
+ }
+
+ [Test]
+ public void TestPauseResume()
+ {
+ pauseAndConfirm();
+ resumeAndConfirm();
+ }
+
+ [Test]
+ public void TestPauseTooSoon()
+ {
+ pauseAndConfirm();
+ resumeAndConfirm();
+
+ pause();
+
+ confirmClockRunning(true);
+ confirmPauseOverlayShown(false);
+ }
+
+ [Test]
+ public void TestExitTooSoon()
+ {
+ pauseAndConfirm();
+
+ resume();
+
+ AddStep("exit too soon", () => Player.Exit());
+
+ confirmClockRunning(true);
+ confirmPauseOverlayShown(false);
+
+ AddAssert("not exited", () => Player.IsCurrentScreen());
+ }
+
+ [Test]
+ public void TestPauseAfterFail()
+ {
+ AddUntilStep("wait for fail", () => Player.HasFailed);
+ AddAssert("fail overlay shown", () => Player.FailOverlayVisible);
+
+ confirmClockRunning(false);
+
+ pause();
+
+ confirmClockRunning(false);
+ confirmPauseOverlayShown(false);
+
+ AddAssert("fail overlay still shown", () => Player.FailOverlayVisible);
+
+ exitAndConfirm();
+ }
+
+ [Test]
+ public void TestExitFromGameplay()
+ {
+ AddStep("exit", () => Player.Exit());
+
+ confirmPaused();
+
+ exitAndConfirm();
+ }
+
+ [Test]
+ public void TestExitFromPause()
+ {
+ pauseAndConfirm();
+ exitAndConfirm();
+ }
+
+ private void pauseAndConfirm()
+ {
+ pause();
+ confirmPaused();
+ }
+
+ private void resumeAndConfirm()
+ {
+ resume();
+ confirmResumed();
+ }
+
+ private void exitAndConfirm()
+ {
+ AddUntilStep("player not exited", () => Player.IsCurrentScreen());
+ AddStep("exit", () => Player.Exit());
+ confirmExited();
+ }
+
+ private void confirmPaused()
+ {
+ confirmClockRunning(false);
+ AddAssert("pause overlay shown", () => Player.PauseOverlayVisible);
+ }
+
+ private void confirmResumed()
+ {
+ confirmClockRunning(true);
+ confirmPauseOverlayShown(false);
+ }
+
+ private void confirmExited()
+ {
+ AddUntilStep("player exited", () => !Player.IsCurrentScreen());
+ }
+
+ private void pause() => AddStep("pause", () => Player.Pause());
+ private void resume() => AddStep("resume", () => Player.Resume());
+
+ private void confirmPauseOverlayShown(bool isShown) =>
+ AddAssert("pause overlay " + (isShown ? "shown" : "hidden"), () => Player.PauseOverlayVisible == isShown);
+
+ private void confirmClockRunning(bool isRunning) =>
+ AddAssert("clock " + (isRunning ? "running" : "stopped"), () => Player.GameplayClockContainer.GameplayClock.IsRunning == isRunning);
+
+ protected override bool AllowFail => true;
+
+ protected override Player CreatePlayer(Ruleset ruleset) => new PausePlayer();
+
+ protected class PausePlayer : Player
+ {
+ public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer;
+
+ public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
+
+ public bool FailOverlayVisible => FailOverlay.State == Visibility.Visible;
+
+ public bool PauseOverlayVisible => PauseOverlay.State == Visibility.Visible;
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs
index 5fa818472cf1..4a2cf24c6d7f 100644
--- a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs
@@ -112,10 +112,10 @@ public void TestDummy()
createSongSelect();
AddAssert("dummy selected", () => songSelect.CurrentBeatmap == defaultBeatmap);
- AddUntilStep(() => songSelect.CurrentBeatmapDetailsBeatmap == defaultBeatmap, "dummy shown on wedge");
+ AddUntilStep("dummy shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap == defaultBeatmap);
addManyTestMaps();
- AddWaitStep(3);
+ AddWaitStep("wait for select", 3);
AddAssert("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap);
}
@@ -125,7 +125,7 @@ public void TestSorting()
{
createSongSelect();
addManyTestMaps();
- AddWaitStep(3);
+ AddWaitStep("wait for add", 3);
AddAssert("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap);
@@ -142,7 +142,7 @@ public void TestImportUnderDifferentRuleset()
createSongSelect();
changeRuleset(2);
importForRuleset(0);
- AddUntilStep(() => songSelect.Carousel.SelectedBeatmap == null, "no selection");
+ AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmap == null);
}
[Test]
@@ -152,13 +152,13 @@ public void TestImportUnderCurrentRuleset()
changeRuleset(2);
importForRuleset(2);
importForRuleset(1);
- AddUntilStep(() => songSelect.Carousel.SelectedBeatmap.RulesetID == 2, "has selection");
+ AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2);
changeRuleset(1);
- AddUntilStep(() => songSelect.Carousel.SelectedBeatmap.RulesetID == 1, "has selection");
+ AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 1);
changeRuleset(0);
- AddUntilStep(() => songSelect.Carousel.SelectedBeatmap == null, "no selection");
+ AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmap == null);
}
[Test]
@@ -196,7 +196,7 @@ public void TestStartAfterUnMatchingFilterDoesNotStart()
{
createSongSelect();
addManyTestMaps();
- AddUntilStep(() => songSelect.Carousel.SelectedBeatmap != null, "has selection");
+ AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null);
bool startRequested = false;
@@ -225,7 +225,7 @@ public void TestStartAfterUnMatchingFilterDoesNotStart()
private void createSongSelect()
{
AddStep("create song select", () => LoadScreen(songSelect = new TestSongSelect()));
- AddUntilStep(() => songSelect.IsCurrentScreen(), "wait for present");
+ AddUntilStep("wait for present", () => songSelect.IsCurrentScreen());
}
private void addManyTestMaps()
diff --git a/osu.Game.Tests/Visual/TestCasePlayerLoader.cs b/osu.Game.Tests/Visual/TestCasePlayerLoader.cs
index 244f553e97f7..2bc416f7f430 100644
--- a/osu.Game.Tests/Visual/TestCasePlayerLoader.cs
+++ b/osu.Game.Tests/Visual/TestCasePlayerLoader.cs
@@ -37,15 +37,15 @@ private void load(OsuGameBase game)
AllowResults = false,
})));
- AddUntilStep(() => loader.IsCurrentScreen(), "wait for current");
+ AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre));
- AddUntilStep(() => !loader.IsCurrentScreen(), "wait for no longer current");
+ AddUntilStep("wait for no longer current", () => !loader.IsCurrentScreen());
AddStep("exit loader", () => loader.Exit());
- AddUntilStep(() => !loader.IsAlive, "wait for no longer alive");
+ AddUntilStep("wait for no longer alive", () => !loader.IsAlive);
AddStep("load slow dummy beatmap", () =>
{
@@ -61,7 +61,7 @@ private void load(OsuGameBase game)
Scheduler.AddDelayed(() => slow.Ready = true, 5000);
});
- AddUntilStep(() => !loader.IsCurrentScreen(), "wait for no longer current");
+ AddUntilStep("wait for no longer current", () => !loader.IsCurrentScreen());
}
protected class SlowLoadPlayer : Player
diff --git a/osu.Game.Tests/Visual/TestCasePlayerReferenceLeaking.cs b/osu.Game.Tests/Visual/TestCasePlayerReferenceLeaking.cs
new file mode 100644
index 000000000000..3e009ae080a3
--- /dev/null
+++ b/osu.Game.Tests/Visual/TestCasePlayerReferenceLeaking.cs
@@ -0,0 +1,56 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Lists;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Screens.Play;
+
+namespace osu.Game.Tests.Visual
+{
+ public class TestCasePlayerReferenceLeaking : AllPlayersTestCase
+ {
+ private readonly WeakList workingWeakReferences = new WeakList();
+
+ private readonly WeakList playerWeakReferences = new WeakList();
+
+ protected override void AddCheckSteps()
+ {
+ AddUntilStep("no leaked beatmaps", () =>
+ {
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ int count = 0;
+
+ workingWeakReferences.ForEachAlive(_ => count++);
+ return count == 1;
+ });
+
+ AddUntilStep("no leaked players", () =>
+ {
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ int count = 0;
+
+ playerWeakReferences.ForEachAlive(_ => count++);
+ return count == 1;
+ });
+ }
+
+ protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, IFrameBasedClock clock)
+ {
+ var working = base.CreateWorkingBeatmap(beatmap, clock);
+ workingWeakReferences.Add(working);
+ return working;
+ }
+
+ protected override Player CreatePlayer(Ruleset ruleset)
+ {
+ var player = base.CreatePlayer(ruleset);
+ playerWeakReferences.Add(player);
+ return player;
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/TestCaseReplay.cs b/osu.Game.Tests/Visual/TestCaseReplay.cs
index c12015a019f5..3a7e2352f8e7 100644
--- a/osu.Game.Tests/Visual/TestCaseReplay.cs
+++ b/osu.Game.Tests/Visual/TestCaseReplay.cs
@@ -4,25 +4,37 @@
using System.ComponentModel;
using System.Linq;
using osu.Game.Rulesets;
-using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
using osu.Game.Screens.Play;
namespace osu.Game.Tests.Visual
{
[Description("Player instantiated with a replay.")]
- public class TestCaseReplay : TestCasePlayer
+ public class TestCaseReplay : AllPlayersTestCase
{
protected override Player CreatePlayer(Ruleset ruleset)
{
- // We create a dummy RulesetContainer just to get the replay - we don't want to use mods here
- // to simulate setting a replay rather than having the replay already set for us
- Beatmap.Value.Mods.Value = Beatmap.Value.Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() });
- var dummyRulesetContainer = ruleset.CreateRulesetContainerWith(Beatmap.Value);
+ var beatmap = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo);
- // Reset the mods
- Beatmap.Value.Mods.Value = Beatmap.Value.Mods.Value.Where(m => !(m is ModAutoplay));
+ return new ScoreAccessibleReplayPlayer(ruleset.GetAutoplayMod().CreateReplayScore(beatmap));
+ }
+
+ protected override void AddCheckSteps()
+ {
+ AddUntilStep("score above zero", () => ((ScoreAccessibleReplayPlayer)Player).ScoreProcessor.TotalScore.Value > 0);
+ AddUntilStep("key counter counted keys", () => ((ScoreAccessibleReplayPlayer)Player).HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 0));
+ }
+
+ private class ScoreAccessibleReplayPlayer : ReplayPlayer
+ {
+ public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
+ public new HUDOverlay HUDOverlay => base.HUDOverlay;
- return new ReplayPlayer(dummyRulesetContainer.ReplayScore);
+ public ScoreAccessibleReplayPlayer(Score score)
+ : base(score)
+ {
+ }
}
}
}
diff --git a/osu.Game.Tests/Visual/TestCaseScreenBreadcrumbControl.cs b/osu.Game.Tests/Visual/TestCaseScreenBreadcrumbControl.cs
index 204f4a493de1..dad684689ed5 100644
--- a/osu.Game.Tests/Visual/TestCaseScreenBreadcrumbControl.cs
+++ b/osu.Game.Tests/Visual/TestCaseScreenBreadcrumbControl.cs
@@ -74,7 +74,7 @@ private void load(OsuColour colours)
}
private void pushNext() => AddStep(@"push next screen", () => ((TestScreen)screenStack.CurrentScreen).PushNext());
- private void waitForCurrent() => AddUntilStep(() => screenStack.CurrentScreen.IsCurrentScreen(), "current screen");
+ private void waitForCurrent() => AddUntilStep("current screen", () => screenStack.CurrentScreen.IsCurrentScreen());
private abstract class TestScreen : OsuScreen
{
diff --git a/osu.Game.Tests/Visual/TestCaseSongProgress.cs b/osu.Game.Tests/Visual/TestCaseSongProgress.cs
index 9845df7461f6..511272a5ae29 100644
--- a/osu.Game.Tests/Visual/TestCaseSongProgress.cs
+++ b/osu.Game.Tests/Visual/TestCaseSongProgress.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.MathUtils;
using osu.Framework.Timing;
@@ -19,14 +20,20 @@ public class TestCaseSongProgress : OsuTestCase
private readonly StopwatchClock clock;
+ [Cached]
+ private readonly GameplayClock gameplayClock;
+
+ private readonly FramedClock framedClock;
+
public TestCaseSongProgress()
{
clock = new StopwatchClock(true);
+ gameplayClock = new GameplayClock(framedClock = new FramedClock(clock));
+
Add(progress = new SongProgress
{
RelativeSizeAxes = Axes.X,
- AudioClock = new StopwatchClock(true),
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
});
@@ -39,23 +46,23 @@ public TestCaseSongProgress()
Origin = Anchor.TopLeft,
});
- AddWaitStep(5);
+ AddWaitStep("wait some", 5);
AddAssert("ensure not created", () => graph.CreationCount == 0);
AddStep("display values", displayNewValues);
- AddWaitStep(5);
- AddUntilStep(() => graph.CreationCount == 1, "wait for creation count");
+ AddWaitStep("wait some", 5);
+ AddUntilStep("wait for creation count", () => graph.CreationCount == 1);
AddStep("Toggle Bar", () => progress.AllowSeeking = !progress.AllowSeeking);
- AddWaitStep(5);
- AddUntilStep(() => graph.CreationCount == 1, "wait for creation count");
+ AddWaitStep("wait some", 5);
+ AddUntilStep("wait for creation count", () => graph.CreationCount == 1);
AddStep("Toggle Bar", () => progress.AllowSeeking = !progress.AllowSeeking);
- AddWaitStep(5);
- AddUntilStep(() => graph.CreationCount == 1, "wait for creation count");
+ AddWaitStep("wait some", 5);
+ AddUntilStep("wait for creation count", () => graph.CreationCount == 1);
AddRepeatStep("New Values", displayNewValues, 5);
- AddWaitStep(5);
+ AddWaitStep("wait some", 5);
AddAssert("ensure debounced", () => graph.CreationCount == 2);
}
@@ -68,8 +75,13 @@ private void displayNewValues()
progress.Objects = objects;
graph.Objects = objects;
- progress.AudioClock = clock;
- progress.OnSeek = pos => clock.Seek(pos);
+ progress.RequestSeek = pos => clock.Seek(pos);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+ framedClock.ProcessFrame();
}
private class TestSongProgressGraph : SongProgressGraph
diff --git a/osu.Game.Tests/Visual/TestCaseToolbar.cs b/osu.Game.Tests/Visual/TestCaseToolbar.cs
index be1b75823a44..cb5f33911b86 100644
--- a/osu.Game.Tests/Visual/TestCaseToolbar.cs
+++ b/osu.Game.Tests/Visual/TestCaseToolbar.cs
@@ -24,10 +24,13 @@ public class TestCaseToolbar : OsuTestCase
public TestCaseToolbar()
{
var toolbar = new Toolbar { State = Visibility.Visible };
+ ToolbarNotificationButton notificationButton = null;
- Add(toolbar);
-
- var notificationButton = toolbar.Children.OfType().Last().Children.OfType().First();
+ AddStep("create toolbar", () =>
+ {
+ Add(toolbar);
+ notificationButton = toolbar.Children.OfType().Last().Children.OfType().First();
+ });
void setNotifications(int count) => AddStep($"set notification count to {count}", () => notificationButton.NotificationCount.Value = count);
diff --git a/osu.Game.Tests/Visual/TestCaseUpdateableBeatmapBackgroundSprite.cs b/osu.Game.Tests/Visual/TestCaseUpdateableBeatmapBackgroundSprite.cs
index 3ee617e0928c..0981b482a10d 100644
--- a/osu.Game.Tests/Visual/TestCaseUpdateableBeatmapBackgroundSprite.cs
+++ b/osu.Game.Tests/Visual/TestCaseUpdateableBeatmapBackgroundSprite.cs
@@ -16,42 +16,48 @@ namespace osu.Game.Tests.Visual
{
public class TestCaseUpdateableBeatmapBackgroundSprite : OsuTestCase
{
- private UpdateableBeatmapBackgroundSprite backgroundSprite;
+ private TestUpdateableBeatmapBackgroundSprite backgroundSprite;
[Resolved]
private BeatmapManager beatmaps { get; set; }
[BackgroundDependencyLoader]
- private void load(OsuGameBase osu, APIAccess api, RulesetStore rulesets)
+ private void load(OsuGameBase osu, IAPIProvider api, RulesetStore rulesets)
{
Bindable beatmapBindable = new Bindable();
var imported = ImportBeatmapTest.LoadOszIntoOsu(osu);
- Child = backgroundSprite = new UpdateableBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both };
+ Child = backgroundSprite = new TestUpdateableBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both };
backgroundSprite.Beatmap.BindTo(beatmapBindable);
var req = new GetBeatmapSetRequest(1);
api.Queue(req);
- AddStep("null", () => beatmapBindable.Value = null);
-
- AddStep("imported", () => beatmapBindable.Value = imported.Beatmaps.First());
+ AddStep("load null beatmap", () => beatmapBindable.Value = null);
+ AddUntilStep("wait for cleanup...", () => backgroundSprite.ChildCount == 1);
+ AddStep("load imported beatmap", () => beatmapBindable.Value = imported.Beatmaps.First());
+ AddUntilStep("wait for cleanup...", () => backgroundSprite.ChildCount == 1);
if (api.IsLoggedIn)
{
- AddUntilStep(() => req.Result != null, "wait for api response");
-
- AddStep("online", () => beatmapBindable.Value = new BeatmapInfo
+ AddUntilStep("wait for api response", () => req.Result != null);
+ AddStep("load online beatmap", () => beatmapBindable.Value = new BeatmapInfo
{
BeatmapSet = req.Result?.ToBeatmapSet(rulesets)
});
+ AddUntilStep("wait for cleanup...", () => backgroundSprite.ChildCount == 1);
}
else
{
AddStep("online (login first)", () => { });
}
}
+
+ private class TestUpdateableBeatmapBackgroundSprite : UpdateableBeatmapBackgroundSprite
+ {
+ public int ChildCount => InternalChildren.Count;
+ }
}
}
diff --git a/osu.Game.Tests/Visual/TestCaseUserProfile.cs b/osu.Game.Tests/Visual/TestCaseUserProfile.cs
index 726134294eb8..aa0bd37449cc 100644
--- a/osu.Game.Tests/Visual/TestCaseUserProfile.cs
+++ b/osu.Game.Tests/Visual/TestCaseUserProfile.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual
public class TestCaseUserProfile : OsuTestCase
{
private readonly TestUserProfileOverlay profile;
- private APIAccess api;
+ private IAPIProvider api;
public override IReadOnlyList RequiredTypes => new[]
{
@@ -36,7 +36,7 @@ public TestCaseUserProfile()
}
[BackgroundDependencyLoader]
- private void load(APIAccess api)
+ private void load(IAPIProvider api)
{
this.api = api;
}
@@ -108,7 +108,7 @@ protected override void LoadComplete()
private void checkSupporterTag(bool isSupporter)
{
- AddUntilStep(() => profile.Header.User != null, "wait for load");
+ AddUntilStep("wait for load", () => profile.Header.User != null);
if (isSupporter)
AddAssert("is supporter", () => profile.Header.SupporterTag.Alpha == 1);
else
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index b22c1aed99cc..938e1ae0f8b4 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -3,9 +3,9 @@
-
+
-
+
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 88f5e777e3e3..9caa64ec96fd 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -64,7 +64,7 @@ public partial class BeatmapManager : ArchiveModelManager currentDownloads = new List();
- public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, APIAccess api, AudioManager audioManager, GameHost host = null,
+ public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, AudioManager audioManager, GameHost host = null,
WorkingBeatmap defaultBeatmap = null)
: base(storage, contextFactory, new BeatmapStore(contextFactory), host)
{
@@ -102,10 +102,16 @@ protected override void Populate(BeatmapSetInfo beatmapSet, ArchiveReader archiv
b.BeatmapSet = beatmapSet;
}
- validateOnlineIds(beatmapSet.Beatmaps);
+ validateOnlineIds(beatmapSet);
foreach (BeatmapInfo b in beatmapSet.Beatmaps)
- fetchAndPopulateOnlineValues(b, beatmapSet.Beatmaps);
+ fetchAndPopulateOnlineValues(b);
+ }
+
+ protected override void PreImport(BeatmapSetInfo beatmapSet)
+ {
+ if (beatmapSet.Beatmaps.Any(b => b.BaseDifficulty == null))
+ throw new InvalidOperationException($"Cannot import {nameof(BeatmapInfo)} with null {nameof(BeatmapInfo.BaseDifficulty)}.");
// check if a set already exists with the same online id, delete if it does.
if (beatmapSet.OnlineBeatmapSetID != null)
@@ -120,14 +126,30 @@ protected override void Populate(BeatmapSetInfo beatmapSet, ArchiveReader archiv
}
}
- private void validateOnlineIds(List beatmaps)
+ private void validateOnlineIds(BeatmapSetInfo beatmapSet)
{
- var beatmapIds = beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList();
+ var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList();
+
+ // ensure all IDs are unique
+ if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1))
+ {
+ resetIds();
+ return;
+ }
+
+ // find any existing beatmaps in the database that have matching online ids
+ var existingBeatmaps = QueryBeatmaps(b => beatmapIds.Contains(b.OnlineBeatmapID)).ToList();
+
+ if (existingBeatmaps.Count > 0)
+ {
+ // reset the import ids (to force a re-fetch) *unless* they match the candidate CheckForExisting set.
+ // we can ignore the case where the new ids are contained by the CheckForExisting set as it will either be used (import skipped) or deleted.
+ var existing = CheckForExisting(beatmapSet);
+ if (existing == null || existingBeatmaps.Any(b => !existing.Beatmaps.Contains(b)))
+ resetIds();
+ }
- // ensure all IDs are unique in this set and none match existing IDs in the local beatmap store.
- if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1) || QueryBeatmaps(b => beatmapIds.Contains(b.OnlineBeatmapID)).Any())
- // remove all online IDs if any problems were found.
- beatmaps.ForEach(b => b.OnlineBeatmapID = null);
+ void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineBeatmapID = null);
}
///
@@ -254,6 +276,18 @@ public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo, WorkingBeatmap
/// The first result for the provided query, or null if no results were found.
public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query);
+ protected override bool CanUndelete(BeatmapSetInfo existing, BeatmapSetInfo import)
+ {
+ if (!base.CanUndelete(existing, import))
+ return false;
+
+ var existingIds = existing.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i);
+ var importIds = import.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i);
+
+ // force re-import if we are not in a sane state.
+ return existing.OnlineBeatmapSetID == import.OnlineBeatmapSetID && existingIds.SequenceEqual(importIds);
+ }
+
///
/// Returns a list of all usable s.
///
@@ -351,7 +385,7 @@ private List createBeatmapDifficulties(ArchiveReader reader)
/// The other beatmaps contained within this set.
/// Whether to re-query if the provided beatmap already has populated values.
/// True if population was successful.
- private bool fetchAndPopulateOnlineValues(BeatmapInfo beatmap, IEnumerable otherBeatmaps, bool force = false)
+ private bool fetchAndPopulateOnlineValues(BeatmapInfo beatmap, bool force = false)
{
if (api?.State != APIState.Online)
return false;
@@ -374,13 +408,6 @@ private bool fetchAndPopulateOnlineValues(BeatmapInfo beatmap, IEnumerable b.OnlineBeatmapID == res.OnlineBeatmapID))
- {
- Logger.Log("Another beatmap in the same set already mapped to this ID. We'll skip adding it this time.", LoggingTarget.Database);
- return false;
- }
-
beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID;
beatmap.OnlineBeatmapID = res.OnlineBeatmapID;
diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs
index 6fa7d476833a..ce7811fc0d1e 100644
--- a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs
+++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs
@@ -9,7 +9,7 @@
namespace osu.Game.Beatmaps.Drawables
{
///
- /// Display a baetmap background from a local source, but fallback to online source if not available.
+ /// Display a beatmap background from a local source, but fallback to online source if not available.
///
public class UpdateableBeatmapBackgroundSprite : ModelBackedDrawable
{
@@ -18,34 +18,53 @@ public class UpdateableBeatmapBackgroundSprite : ModelBackedDrawable Model = b.NewValue);
+ this.beatmapSetCoverType = beatmapSetCoverType;
}
- protected override Drawable CreateDrawable(BeatmapInfo model)
+ private BeatmapInfo lastModel;
+
+ protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Drawable content, double timeBeforeLoad)
{
return new DelayedLoadUnloadWrapper(() =>
{
- Drawable drawable;
+ // If DelayedLoadUnloadWrapper is attempting to RELOAD the same content (Beatmap), that means that it was
+ // previously UNLOADED and thus its children have been disposed of, so we need to recreate them here.
+ if (lastModel == Beatmap.Value && Beatmap.Value != null)
+ return CreateDrawable(Beatmap.Value);
- var localBeatmap = beatmaps.GetWorkingBeatmap(model);
+ // If the model has changed since the previous unload (or if there was no load), then we can safely use the given content
+ lastModel = Beatmap.Value;
+ return content;
+ }, timeBeforeLoad, 10000);
+ }
- if (localBeatmap.BeatmapInfo.ID == 0 && model?.BeatmapSet?.OnlineInfo != null)
- drawable = new BeatmapSetCover(model.BeatmapSet);
- else
- drawable = new BeatmapBackgroundSprite(localBeatmap);
+ protected override Drawable CreateDrawable(BeatmapInfo model)
+ {
+ Drawable drawable = getDrawableForModel(model);
- drawable.RelativeSizeAxes = Axes.Both;
- drawable.Anchor = Anchor.Centre;
- drawable.Origin = Anchor.Centre;
- drawable.FillMode = FillMode.Fill;
- drawable.OnLoadComplete = d => d.FadeInFromZero(400);
+ drawable.RelativeSizeAxes = Axes.Both;
+ drawable.Anchor = Anchor.Centre;
+ drawable.Origin = Anchor.Centre;
+ drawable.FillMode = FillMode.Fill;
+ drawable.OnLoadComplete += d => d.FadeInFromZero(400);
- return drawable;
- }, 500, 10000);
+ return drawable;
}
- protected override double FadeDuration => 0;
+ private Drawable getDrawableForModel(BeatmapInfo model)
+ {
+ // prefer online cover where available.
+ if (model?.BeatmapSet?.OnlineInfo != null)
+ return new BeatmapSetCover(model.BeatmapSet, beatmapSetCoverType);
+
+ return model?.ID > 0
+ ? new BeatmapBackgroundSprite(beatmaps.GetWorkingBeatmap(model))
+ : new BeatmapBackgroundSprite(beatmaps.DefaultBeatmap);
+ }
}
}
diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs
index 367b63d6d188..c7c4c1fb1e40 100644
--- a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs
+++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs
@@ -67,16 +67,19 @@ private void updateCover()
if (beatmapSet != null)
{
+ BeatmapSetCover cover;
+
Add(displayedCover = new DelayedLoadWrapper(
- new BeatmapSetCover(beatmapSet, coverType)
+ cover = new BeatmapSetCover(beatmapSet, coverType)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill,
- OnLoadComplete = d => d.FadeInFromZero(400, Easing.Out),
})
);
+
+ cover.OnLoadComplete += d => d.FadeInFromZero(400, Easing.Out);
}
}
}
diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
index 0aa1697bf85d..73aa12a3db9e 100644
--- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
@@ -26,7 +26,12 @@ public DummyWorkingBeatmap(OsuGameBase game = null)
Title = "no beatmaps available!"
},
BeatmapSet = new BeatmapSetInfo(),
- BaseDifficulty = new BeatmapDifficulty(),
+ BaseDifficulty = new BeatmapDifficulty
+ {
+ DrainRate = 0,
+ CircleSize = 0,
+ OverallDifficulty = 0,
+ },
Ruleset = new DummyRulesetInfo()
})
{
@@ -47,7 +52,7 @@ private class DummyRuleset : Ruleset
{
public override IEnumerable GetModsFor(ModType type) => new Mod[] { };
- public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap)
+ public override DrawableRuleset CreateDrawableRulesetWith(WorkingBeatmap beatmap)
{
throw new NotImplementedException();
}
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index 4f19fbd32385..a27126ad9ceb 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -2,10 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Globalization;
using System.IO;
using System.Linq;
using osu.Framework.IO.File;
+using osu.Framework.Logging;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Beatmaps.ControlPoints;
@@ -25,7 +25,7 @@ public class LegacyBeatmapDecoder : LegacyDecoder
public static void Register()
{
- AddDecoder(@"osu file format v", m => new LegacyBeatmapDecoder(int.Parse(m.Split('v').Last())));
+ AddDecoder(@"osu file format v", m => new LegacyBeatmapDecoder(Parsing.ParseInt(m.Split('v').Last())));
}
///
@@ -104,25 +104,25 @@ private void handleGeneral(string line)
metadata.AudioFile = FileSafety.PathStandardise(pair.Value);
break;
case @"AudioLeadIn":
- beatmap.BeatmapInfo.AudioLeadIn = int.Parse(pair.Value);
+ beatmap.BeatmapInfo.AudioLeadIn = Parsing.ParseInt(pair.Value);
break;
case @"PreviewTime":
- metadata.PreviewTime = getOffsetTime(int.Parse(pair.Value));
+ metadata.PreviewTime = getOffsetTime(Parsing.ParseInt(pair.Value));
break;
case @"Countdown":
- beatmap.BeatmapInfo.Countdown = int.Parse(pair.Value) == 1;
+ beatmap.BeatmapInfo.Countdown = Parsing.ParseInt(pair.Value) == 1;
break;
case @"SampleSet":
defaultSampleBank = (LegacySampleBank)Enum.Parse(typeof(LegacySampleBank), pair.Value);
break;
case @"SampleVolume":
- defaultSampleVolume = int.Parse(pair.Value);
+ defaultSampleVolume = Parsing.ParseInt(pair.Value);
break;
case @"StackLeniency":
- beatmap.BeatmapInfo.StackLeniency = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo);
+ beatmap.BeatmapInfo.StackLeniency = Parsing.ParseFloat(pair.Value);
break;
case @"Mode":
- beatmap.BeatmapInfo.RulesetID = int.Parse(pair.Value);
+ beatmap.BeatmapInfo.RulesetID = Parsing.ParseInt(pair.Value);
switch (beatmap.BeatmapInfo.RulesetID)
{
@@ -142,13 +142,13 @@ private void handleGeneral(string line)
break;
case @"LetterboxInBreaks":
- beatmap.BeatmapInfo.LetterboxInBreaks = int.Parse(pair.Value) == 1;
+ beatmap.BeatmapInfo.LetterboxInBreaks = Parsing.ParseInt(pair.Value) == 1;
break;
case @"SpecialStyle":
- beatmap.BeatmapInfo.SpecialStyle = int.Parse(pair.Value) == 1;
+ beatmap.BeatmapInfo.SpecialStyle = Parsing.ParseInt(pair.Value) == 1;
break;
case @"WidescreenStoryboard":
- beatmap.BeatmapInfo.WidescreenStoryboard = int.Parse(pair.Value) == 1;
+ beatmap.BeatmapInfo.WidescreenStoryboard = Parsing.ParseInt(pair.Value) == 1;
break;
}
}
@@ -163,16 +163,16 @@ private void handleEditor(string line)
beatmap.BeatmapInfo.StoredBookmarks = pair.Value;
break;
case @"DistanceSpacing":
- beatmap.BeatmapInfo.DistanceSpacing = double.Parse(pair.Value, NumberFormatInfo.InvariantInfo);
+ beatmap.BeatmapInfo.DistanceSpacing = Math.Max(0, Parsing.ParseDouble(pair.Value));
break;
case @"BeatDivisor":
- beatmap.BeatmapInfo.BeatDivisor = int.Parse(pair.Value);
+ beatmap.BeatmapInfo.BeatDivisor = Parsing.ParseInt(pair.Value);
break;
case @"GridSize":
- beatmap.BeatmapInfo.GridSize = int.Parse(pair.Value);
+ beatmap.BeatmapInfo.GridSize = Parsing.ParseInt(pair.Value);
break;
case @"TimelineZoom":
- beatmap.BeatmapInfo.TimelineZoom = double.Parse(pair.Value, NumberFormatInfo.InvariantInfo);
+ beatmap.BeatmapInfo.TimelineZoom = Math.Max(0, Parsing.ParseDouble(pair.Value));
break;
}
}
@@ -209,10 +209,10 @@ private void handleMetadata(string line)
beatmap.BeatmapInfo.Metadata.Tags = pair.Value;
break;
case @"BeatmapID":
- beatmap.BeatmapInfo.OnlineBeatmapID = int.Parse(pair.Value);
+ beatmap.BeatmapInfo.OnlineBeatmapID = Parsing.ParseInt(pair.Value);
break;
case @"BeatmapSetID":
- beatmap.BeatmapInfo.BeatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = int.Parse(pair.Value) };
+ beatmap.BeatmapInfo.BeatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = Parsing.ParseInt(pair.Value) };
break;
}
}
@@ -225,22 +225,22 @@ private void handleDifficulty(string line)
switch (pair.Key)
{
case @"HPDrainRate":
- difficulty.DrainRate = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo);
+ difficulty.DrainRate = Parsing.ParseFloat(pair.Value);
break;
case @"CircleSize":
- difficulty.CircleSize = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo);
+ difficulty.CircleSize = Parsing.ParseFloat(pair.Value);
break;
case @"OverallDifficulty":
- difficulty.OverallDifficulty = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo);
+ difficulty.OverallDifficulty = Parsing.ParseFloat(pair.Value);
break;
case @"ApproachRate":
- difficulty.ApproachRate = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo);
+ difficulty.ApproachRate = Parsing.ParseFloat(pair.Value);
break;
case @"SliderMultiplier":
- difficulty.SliderMultiplier = double.Parse(pair.Value, NumberFormatInfo.InvariantInfo);
+ difficulty.SliderMultiplier = Parsing.ParseDouble(pair.Value);
break;
case @"SliderTickRate":
- difficulty.SliderTickRate = double.Parse(pair.Value, NumberFormatInfo.InvariantInfo);
+ difficulty.SliderTickRate = Parsing.ParseDouble(pair.Value);
break;
}
}
@@ -260,10 +260,12 @@ private void handleEvent(string line)
beatmap.BeatmapInfo.Metadata.BackgroundFile = FileSafety.PathStandardise(filename);
break;
case EventType.Break:
+ double start = getOffsetTime(Parsing.ParseDouble(split[1]));
+
var breakEvent = new BreakPeriod
{
- StartTime = getOffsetTime(double.Parse(split[1], NumberFormatInfo.InvariantInfo)),
- EndTime = getOffsetTime(double.Parse(split[2], NumberFormatInfo.InvariantInfo))
+ StartTime = start,
+ EndTime = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2])))
};
if (!breakEvent.HasEffect)
@@ -280,25 +282,25 @@ private void handleTimingPoint(string line)
{
string[] split = line.Split(',');
- double time = getOffsetTime(double.Parse(split[0].Trim(), NumberFormatInfo.InvariantInfo));
- double beatLength = double.Parse(split[1].Trim(), NumberFormatInfo.InvariantInfo);
+ double time = getOffsetTime(Parsing.ParseDouble(split[0].Trim()));
+ double beatLength = Parsing.ParseDouble(split[1].Trim());
double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1;
TimeSignatures timeSignature = TimeSignatures.SimpleQuadruple;
if (split.Length >= 3)
- timeSignature = split[2][0] == '0' ? TimeSignatures.SimpleQuadruple : (TimeSignatures)int.Parse(split[2]);
+ timeSignature = split[2][0] == '0' ? TimeSignatures.SimpleQuadruple : (TimeSignatures)Parsing.ParseInt(split[2]);
LegacySampleBank sampleSet = defaultSampleBank;
if (split.Length >= 4)
- sampleSet = (LegacySampleBank)int.Parse(split[3]);
+ sampleSet = (LegacySampleBank)Parsing.ParseInt(split[3]);
int customSampleBank = 0;
if (split.Length >= 5)
- customSampleBank = int.Parse(split[4]);
+ customSampleBank = Parsing.ParseInt(split[4]);
int sampleVolume = defaultSampleVolume;
if (split.Length >= 6)
- sampleVolume = int.Parse(split[5]);
+ sampleVolume = Parsing.ParseInt(split[5]);
bool timingChange = true;
if (split.Length >= 7)
@@ -308,7 +310,7 @@ private void handleTimingPoint(string line)
bool omitFirstBarSignature = false;
if (split.Length >= 8)
{
- EffectFlags effectFlags = (EffectFlags)int.Parse(split[7]);
+ EffectFlags effectFlags = (EffectFlags)Parsing.ParseInt(split[7]);
kiaiMode = effectFlags.HasFlag(EffectFlags.Kiai);
omitFirstBarSignature = effectFlags.HasFlag(EffectFlags.OmitFirstBarLine);
}
@@ -348,8 +350,13 @@ private void handleTimingPoint(string line)
CustomSampleBank = customSampleBank
});
}
- catch (FormatException e)
+ catch (FormatException)
+ {
+ Logger.Log("A timing point could not be parsed correctly and will be ignored", LoggingTarget.Runtime, LogLevel.Important);
+ }
+ catch (OverflowException)
{
+ Logger.Log("A timing point could not be parsed correctly and will be ignored", LoggingTarget.Runtime, LogLevel.Important);
}
}
diff --git a/osu.Game/Beatmaps/Formats/Parsing.cs b/osu.Game/Beatmaps/Formats/Parsing.cs
new file mode 100644
index 000000000000..c3efb8c760ad
--- /dev/null
+++ b/osu.Game/Beatmaps/Formats/Parsing.cs
@@ -0,0 +1,52 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Globalization;
+
+namespace osu.Game.Beatmaps.Formats
+{
+ ///
+ /// Helper methods to parse from string to number and perform very basic validation.
+ ///
+ public static class Parsing
+ {
+ public const int MAX_COORDINATE_VALUE = 65536;
+
+ public const double MAX_PARSE_VALUE = int.MaxValue;
+
+ public static float ParseFloat(string input, float parseLimit = (float)MAX_PARSE_VALUE)
+ {
+ var output = float.Parse(input, CultureInfo.InvariantCulture);
+
+ if (output < -parseLimit) throw new OverflowException("Value is too low");
+ if (output > parseLimit) throw new OverflowException("Value is too high");
+
+ if (float.IsNaN(output)) throw new FormatException("Not a number");
+
+ return output;
+ }
+
+ public static double ParseDouble(string input, double parseLimit = MAX_PARSE_VALUE)
+ {
+ var output = double.Parse(input, CultureInfo.InvariantCulture);
+
+ if (output < -parseLimit) throw new OverflowException("Value is too low");
+ if (output > parseLimit) throw new OverflowException("Value is too high");
+
+ if (double.IsNaN(output)) throw new FormatException("Not a number");
+
+ return output;
+ }
+
+ public static int ParseInt(string input, int parseLimit = (int)MAX_PARSE_VALUE)
+ {
+ var output = int.Parse(input, CultureInfo.InvariantCulture);
+
+ if (output < -parseLimit) throw new OverflowException("Value is too low");
+ if (output > parseLimit) throw new OverflowException("Value is too high");
+
+ return output;
+ }
+ }
+}
diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs
index 9ec184abd78a..3805921ac2d8 100644
--- a/osu.Game/Database/ArchiveModelManager.cs
+++ b/osu.Game/Database/ArchiveModelManager.cs
@@ -300,20 +300,30 @@ public TModel Import(TModel item, ArchiveReader archive = null)
{
if (!write.IsTransactionLeader) throw new InvalidOperationException($"Ensure there is no parent transaction so errors can correctly be handled by {this}");
+ if (archive != null)
+ item.Files = createFileInfos(archive, Files);
+
+ Populate(item, archive);
+
var existing = CheckForExisting(item);
if (existing != null)
{
- Undelete(existing);
- Logger.Log($"Found existing {typeof(TModel)} for {item} (ID {existing.ID}). Skipping import.", LoggingTarget.Database);
- handleEvent(() => ItemAdded?.Invoke(existing, true));
- return existing;
+ if (CanUndelete(existing, item))
+ {
+ Undelete(existing);
+ Logger.Log($"Found existing {typeof(TModel)} for {item} (ID {existing.ID}). Skipping import.", LoggingTarget.Database);
+ handleEvent(() => ItemAdded?.Invoke(existing, true));
+ return existing;
+ }
+ else
+ {
+ Delete(existing);
+ ModelStore.PurgeDeletable(s => s.ID == existing.ID);
+ }
}
- if (archive != null)
- item.Files = createFileInfos(archive, Files);
-
- Populate(item, archive);
+ PreImport(item);
// import to store
ModelStore.Add(item);
@@ -542,12 +552,29 @@ protected virtual void Populate(TModel model, [CanBeNull] ArchiveReader archive)
{
}
+ ///
+ /// Perform any final actions before the import to database executes.
+ ///
+ /// The model prepared for import.
+ protected virtual void PreImport(TModel model)
+ {
+ }
+
///
/// Check whether an existing model already exists for a new import item.
///
- /// The new model proposed for import. Note that has not yet been run on this model.
+ /// The new model proposed for import.
/// An existing model which matches the criteria to skip importing, else null.
- protected virtual TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash);
+ protected TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash);
+
+ ///
+ /// After an existing is found during an import process, the default behaviour is to restore the existing
+ /// item and skip the import. This method allows changing that behaviour.
+ ///
+ /// The existing model.
+ /// The newly imported model.
+ /// Whether the existing model should be restored and used. Returning false will delete the existing a force a re-import.
+ protected virtual bool CanUndelete(TModel existing, TModel import) => true;
private DbSet queryModel() => ContextFactory.Get().Set();
diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs
index ebd9db786f18..c67d779c37d3 100644
--- a/osu.Game/Graphics/Backgrounds/Triangles.cs
+++ b/osu.Game/Graphics/Backgrounds/Triangles.cs
@@ -64,7 +64,7 @@ public class Triangles : Drawable
private readonly SortedList parts = new SortedList(Comparer.Default);
- private Shader shader;
+ private IShader shader;
private readonly Texture texture;
public Triangles()
@@ -75,7 +75,7 @@ public Triangles()
[BackgroundDependencyLoader]
private void load(ShaderManager shaders)
{
- shader = shaders?.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED);
+ shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED);
}
protected override void LoadComplete()
@@ -180,8 +180,6 @@ protected virtual TriangleParticle CreateTriangle()
protected override DrawNode CreateDrawNode() => new TrianglesDrawNode();
- private readonly TrianglesDrawNodeSharedData sharedData = new TrianglesDrawNodeSharedData();
-
protected override void ApplyDrawNode(DrawNode node)
{
base.ApplyDrawNode(node);
@@ -191,27 +189,21 @@ protected override void ApplyDrawNode(DrawNode node)
trianglesNode.Shader = shader;
trianglesNode.Texture = texture;
trianglesNode.Size = DrawSize;
- trianglesNode.Shared = sharedData;
trianglesNode.Parts.Clear();
trianglesNode.Parts.AddRange(parts);
}
- private class TrianglesDrawNodeSharedData
- {
- public readonly LinearBatch VertexBatch = new LinearBatch(100 * 3, 10, PrimitiveType.Triangles);
- }
-
private class TrianglesDrawNode : DrawNode
{
- public Shader Shader;
+ public IShader Shader;
public Texture Texture;
- public TrianglesDrawNodeSharedData Shared;
-
public readonly List Parts = new List();
public Vector2 Size;
+ private readonly LinearBatch vertexBatch = new LinearBatch(100 * 3, 10, PrimitiveType.Triangles);
+
public override void Draw(Action vertexAction)
{
base.Draw(vertexAction);
@@ -239,12 +231,19 @@ public override void Draw(Action vertexAction)
triangle,
colourInfo,
null,
- Shared.VertexBatch.AddAction,
+ vertexBatch.AddAction,
Vector2.Divide(localInflationAmount, size));
}
Shader.Unbind();
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ vertexBatch.Dispose();
+ }
}
protected struct TriangleParticle : IComparable
diff --git a/osu.Game/Graphics/Containers/UserDimContainer.cs b/osu.Game/Graphics/Containers/UserDimContainer.cs
index 4c8928e12266..b078f40420dd 100644
--- a/osu.Game/Graphics/Containers/UserDimContainer.cs
+++ b/osu.Game/Graphics/Containers/UserDimContainer.cs
@@ -1,27 +1,26 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
+using osu.Game.Graphics.Backgrounds;
+using osuTK;
using osuTK.Graphics;
namespace osu.Game.Graphics.Containers
{
///
- /// A container that applies user-configured dim levels to its contents.
+ /// A container that applies user-configured visual settings to its contents.
/// This container specifies behavior that applies to both Storyboards and Backgrounds.
///
public class UserDimContainer : Container
{
private const float background_fade_duration = 800;
- private Bindable dimLevel { get; set; }
-
- private Bindable showStoryboard { get; set; }
-
///
/// Whether or not user-configured dim levels should be applied to the container.
///
@@ -32,19 +31,40 @@ public class UserDimContainer : Container
///
public readonly Bindable StoryboardReplacesBackground = new Bindable();
+ ///
+ /// The amount of blur to be applied to the background in addition to user-specified blur.
+ ///
+ ///
+ /// Used in contexts where there can potentially be both user and screen-specified blurring occuring at the same time, such as in
+ ///
+ public readonly Bindable BlurAmount = new Bindable();
+
+ private Bindable userDimLevel { get; set; }
+
+ private Bindable userBlurLevel { get; set; }
+
+ private Bindable showStoryboard { get; set; }
+
protected Container DimContainer { get; }
protected override Container Content => DimContainer;
private readonly bool isStoryboard;
+ ///
+ /// As an optimisation, we add the two blur portions to be applied rather than actually applying two separate blurs.
+ ///
+ private Vector2 blurTarget => EnableUserDim.Value
+ ? new Vector2(BlurAmount.Value + (float)userBlurLevel.Value * 25)
+ : new Vector2(BlurAmount.Value);
+
///
/// Creates a new .
///
- /// Whether or not this instance of UserDimContainer contains a storyboard.
+ /// Whether or not this instance contains a storyboard.
///
/// While both backgrounds and storyboards allow user dim levels to be applied, storyboards can be toggled via
- /// and can cause backgrounds to become hidden via .
+ /// and can cause backgrounds to become hidden via . Storyboards are also currently unable to be blurred.
///
///
public UserDimContainer(bool isStoryboard = false)
@@ -53,36 +73,62 @@ public UserDimContainer(bool isStoryboard = false)
AddInternal(DimContainer = new Container { RelativeSizeAxes = Axes.Both });
}
+ private Background background;
+
+ public Background Background
+ {
+ get => background;
+ set
+ {
+ base.Add(background = value);
+ background.BlurTo(blurTarget, 0, Easing.OutQuint);
+ }
+ }
+
+ public override void Add(Drawable drawable)
+ {
+ if (drawable is Background)
+ throw new InvalidOperationException($"Use {nameof(Background)} to set a background.");
+
+ base.Add(drawable);
+ }
+
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
- dimLevel = config.GetBindable(OsuSetting.DimLevel);
+ userDimLevel = config.GetBindable(OsuSetting.DimLevel);
+ userBlurLevel = config.GetBindable(OsuSetting.BlurLevel);
showStoryboard = config.GetBindable(OsuSetting.ShowStoryboard);
- EnableUserDim.ValueChanged += _ => updateBackgroundDim();
- dimLevel.ValueChanged += _ => updateBackgroundDim();
- showStoryboard.ValueChanged += _ => updateBackgroundDim();
- StoryboardReplacesBackground.ValueChanged += _ => updateBackgroundDim();
+
+ EnableUserDim.ValueChanged += _ => updateVisuals();
+ userDimLevel.ValueChanged += _ => updateVisuals();
+ userBlurLevel.ValueChanged += _ => updateVisuals();
+ showStoryboard.ValueChanged += _ => updateVisuals();
+ StoryboardReplacesBackground.ValueChanged += _ => updateVisuals();
+ BlurAmount.ValueChanged += _ => updateVisuals();
}
protected override void LoadComplete()
{
base.LoadComplete();
- updateBackgroundDim();
+ updateVisuals();
}
- private void updateBackgroundDim()
+ private void updateVisuals()
{
if (isStoryboard)
{
- DimContainer.FadeTo(!showStoryboard.Value || dimLevel.Value == 1 ? 0 : 1, background_fade_duration, Easing.OutQuint);
+ DimContainer.FadeTo(!showStoryboard.Value || userDimLevel.Value == 1 ? 0 : 1, background_fade_duration, Easing.OutQuint);
}
else
{
// The background needs to be hidden in the case of it being replaced by the storyboard
DimContainer.FadeTo(showStoryboard.Value && StoryboardReplacesBackground.Value ? 0 : 1, background_fade_duration, Easing.OutQuint);
+
+ Background?.BlurTo(blurTarget, background_fade_duration, Easing.OutQuint);
}
- DimContainer.FadeColour(EnableUserDim.Value ? OsuColour.Gray(1 - (float)dimLevel.Value) : Color4.White, background_fade_duration, Easing.OutQuint);
+ DimContainer.FadeColour(EnableUserDim.Value ? OsuColour.Gray(1 - (float)userDimLevel.Value) : Color4.White, background_fade_duration, Easing.OutQuint);
}
}
}
diff --git a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs
index 73c9c0dd0e06..f873db0dcb99 100644
--- a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs
+++ b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs
@@ -16,9 +16,6 @@ namespace osu.Game.Graphics.UserInterface
///
public class FocusedTextBox : OsuTextBox
{
- protected override Color4 BackgroundUnfocused => new Color4(10, 10, 10, 255);
- protected override Color4 BackgroundFocused => new Color4(10, 10, 10, 255);
-
public Action Exit;
private bool focus;
@@ -47,6 +44,9 @@ public bool HoldFocus
private void load(GameHost host)
{
this.host = host;
+
+ BackgroundUnfocused = new Color4(10, 10, 10, 255);
+ BackgroundFocused = new Color4(10, 10, 10, 255);
}
// We may not be focused yet, but we need to handle keyboard input to be able to request focus
diff --git a/osu.Game/Graphics/UserInterface/LineGraph.cs b/osu.Game/Graphics/UserInterface/LineGraph.cs
index 7a20dd3d1638..74025b71ff1f 100644
--- a/osu.Game/Graphics/UserInterface/LineGraph.cs
+++ b/osu.Game/Graphics/UserInterface/LineGraph.cs
@@ -69,7 +69,7 @@ public LineGraph()
{
Masking = true,
RelativeSizeAxes = Axes.Both,
- Child = path = new SmoothPath { RelativeSizeAxes = Axes.Both, PathWidth = 1 }
+ Child = path = new SmoothPath { RelativeSizeAxes = Axes.Both, PathRadius = 1 }
});
}
@@ -102,9 +102,10 @@ private void applyPath()
for (int i = 0; i < values.Length; i++)
{
- float x = (i + count - values.Length) / (float)(count - 1) * DrawWidth - 1;
- float y = GetYPosition(values[i]) * DrawHeight - 1;
- // the -1 is for inner offset in path (actually -PathWidth)
+ // Make sure that we are accounting for path width when calculating vertex positions
+ // We need to apply 2x the path radius to account for it because the full diameter of the line accounts into height
+ float x = (i + count - values.Length) / (float)(count - 1) * (DrawWidth - 2 * path.PathRadius);
+ float y = GetYPosition(values[i]) * (DrawHeight - 2 * path.PathRadius);
path.AddVertex(new Vector2(x, y));
}
}
diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs
index 21cdfbf5af32..ebe38db60acf 100644
--- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs
@@ -16,10 +16,6 @@ namespace osu.Game.Graphics.UserInterface
{
public class OsuTextBox : TextBox, IKeyBindingHandler
{
- protected override Color4 BackgroundUnfocused => Color4.Black.Opacity(0.5f);
- protected override Color4 BackgroundFocused => OsuColour.Gray(0.3f).Opacity(0.8f);
- protected override Color4 BackgroundCommit => BorderColour;
-
protected override float LeftRightPadding => 10;
protected override SpriteText CreatePlaceholder() => new OsuSpriteText
@@ -41,7 +37,9 @@ public OsuTextBox()
[BackgroundDependencyLoader]
private void load(OsuColour colour)
{
- BorderColour = colour.Yellow;
+ BackgroundUnfocused = Color4.Black.Opacity(0.5f);
+ BackgroundFocused = OsuColour.Gray(0.3f).Opacity(0.8f);
+ BackgroundCommit = BorderColour = colour.Yellow;
}
protected override void OnFocus(FocusEvent e)
diff --git a/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs b/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs
index c38cd19b429d..026442f32892 100644
--- a/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs
+++ b/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs
@@ -13,7 +13,6 @@ protected override void Up(MigrationBuilder migrationBuilder)
protected override void Down(MigrationBuilder migrationBuilder)
{
-
}
}
}
diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index 4d039e0b8af2..c5f6ef41c221 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -22,7 +22,7 @@ public class APIAccess : Component, IAPIProvider
private readonly OsuConfigManager config;
private readonly OAuth authentication;
- public string Endpoint = @"https://osu.ppy.sh";
+ public string Endpoint => @"https://osu.ppy.sh";
private const string client_id = @"5";
private const string client_secret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk";
@@ -70,13 +70,15 @@ public APIAccess(OsuConfigManager config)
internal new void Schedule(Action action) => base.Schedule(action);
+ ///
+ /// Register a component to receive API events.
+ /// Fires once immediately to ensure a correct state.
+ ///
+ ///
public void Register(IOnlineComponent component)
{
- Scheduler.Add(delegate
- {
- components.Add(component);
- component.APIStateChanged(this, state);
- });
+ Scheduler.Add(delegate { components.Add(component); });
+ component.APIStateChanged(this, state);
}
public void Unregister(IOnlineComponent component)
@@ -264,20 +266,18 @@ public APIState State
get => state;
private set
{
- APIState oldState = state;
- APIState newState = value;
+ if (state == value)
+ return;
+ APIState oldState = state;
state = value;
- if (oldState != newState)
+ log.Add($@"We just went {state}!");
+ Scheduler.Add(delegate
{
- log.Add($@"We just went {newState}!");
- Scheduler.Add(delegate
- {
- components.ForEach(c => c.APIStateChanged(this, newState));
- OnStateChange?.Invoke(oldState, newState);
- });
- }
+ components.ForEach(c => c.APIStateChanged(this, state));
+ OnStateChange?.Invoke(oldState, state);
+ });
}
}
diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs
index 2781a5709b88..96f3b8527237 100644
--- a/osu.Game/Online/API/APIRequest.cs
+++ b/osu.Game/Online/API/APIRequest.cs
@@ -61,9 +61,12 @@ public abstract class APIRequest
private Action pendingFailure;
- public void Perform(APIAccess api)
+ public void Perform(IAPIProvider api)
{
- API = api;
+ if (!(api is APIAccess apiAccess))
+ throw new NotSupportedException($"A {nameof(APIAccess)} is required to perform requests.");
+
+ API = apiAccess;
if (checkAndScheduleFailure())
return;
@@ -71,7 +74,7 @@ public void Perform(APIAccess api)
WebRequest = CreateWebRequest();
WebRequest.Failed += Fail;
WebRequest.AllowRetryOnTimeout = false;
- WebRequest.AddHeader("Authorization", $"Bearer {api.AccessToken}");
+ WebRequest.AddHeader("Authorization", $"Bearer {API.AccessToken}");
if (checkAndScheduleFailure())
return;
@@ -85,7 +88,7 @@ public void Perform(APIAccess api)
if (checkAndScheduleFailure())
return;
- api.Schedule(delegate { Success?.Invoke(); });
+ API.Schedule(delegate { Success?.Invoke(); });
}
public void Cancel() => Fail(new OperationCanceledException(@"Request cancelled"));
diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs
index 096ab5d8c8fe..99fde103092c 100644
--- a/osu.Game/Online/API/DummyAPIAccess.cs
+++ b/osu.Game/Online/API/DummyAPIAccess.cs
@@ -1,23 +1,44 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
+using System.Threading;
using osu.Framework.Bindables;
+using osu.Framework.Graphics;
using osu.Game.Users;
namespace osu.Game.Online.API
{
- public class DummyAPIAccess : IAPIProvider
+ public class DummyAPIAccess : Component, IAPIProvider
{
public Bindable LocalUser { get; } = new Bindable(new User
{
Username = @"Dummy",
- Id = 1,
+ Id = 1001,
});
public bool IsLoggedIn => true;
- public void Update()
+ public string ProvidedUsername => LocalUser.Value.Username;
+
+ public string Endpoint => "http://localhost";
+
+ private APIState state = APIState.Online;
+
+ private readonly List components = new List();
+
+ public APIState State
{
+ get => state;
+ private set
+ {
+ if (state == value)
+ return;
+
+ state = value;
+
+ Scheduler.Add(() => components.ForEach(c => c.APIStateChanged(this, value)));
+ }
}
public virtual void Queue(APIRequest request)
@@ -26,6 +47,36 @@ public virtual void Queue(APIRequest request)
public void Register(IOnlineComponent component)
{
+ Scheduler.Add(delegate { components.Add(component); });
+ component.APIStateChanged(this, state);
+ }
+
+ public void Unregister(IOnlineComponent component)
+ {
+ Scheduler.Add(delegate { components.Remove(component); });
+ }
+
+ public void Login(string username, string password)
+ {
+ LocalUser.Value = new User
+ {
+ Username = username,
+ Id = 1001,
+ };
+
+ State = APIState.Online;
+ }
+
+ public void Logout()
+ {
+ LocalUser.Value = new GuestUser();
+ State = APIState.Offline;
+ }
+
+ public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
+ {
+ Thread.Sleep(200);
+ return null;
}
}
}
diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs
index e4533ecb3d41..7c1f85094362 100644
--- a/osu.Game/Online/API/IAPIProvider.cs
+++ b/osu.Game/Online/API/IAPIProvider.cs
@@ -18,6 +18,19 @@ public interface IAPIProvider
///
bool IsLoggedIn { get; }
+ ///
+ /// The last username provided by the end-user.
+ /// May not be authenticated.
+ ///
+ string ProvidedUsername { get; }
+
+ ///
+ /// The URL endpoint for this API. Does not include a trailing slash.
+ ///
+ string Endpoint { get; }
+
+ APIState State { get; }
+
///
/// Queue a new request.
///
@@ -29,5 +42,32 @@ public interface IAPIProvider
///
/// The component to register.
void Register(IOnlineComponent component);
+
+ ///
+ /// Unregisters a component to receive state changes.
+ ///
+ /// The component to unregister.
+ void Unregister(IOnlineComponent component);
+
+ ///
+ /// Attempt to login using the provided credentials. This is a non-blocking operation.
+ ///
+ /// The user's username.
+ /// The user's password.
+ void Login(string username, string password);
+
+ ///
+ /// Log out the current user.
+ ///
+ void Logout();
+
+ ///
+ /// Create a new user account. This is a blocking operation.
+ ///
+ /// The email to create the account with.
+ /// The username to create the account with.
+ /// The password to create the account with.
+ /// Any errors encoutnered during account creation.
+ RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password);
}
}
diff --git a/osu.Game/Online/API/IOnlineComponent.cs b/osu.Game/Online/API/IOnlineComponent.cs
index fb40bfd1e62d..da6b784759bc 100644
--- a/osu.Game/Online/API/IOnlineComponent.cs
+++ b/osu.Game/Online/API/IOnlineComponent.cs
@@ -5,6 +5,6 @@ namespace osu.Game.Online.API
{
public interface IOnlineComponent
{
- void APIStateChanged(APIAccess api, APIState state);
+ void APIStateChanged(IAPIProvider api, APIState state);
}
}
diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs
index 38df0efd6f4f..ac1666f8eda1 100644
--- a/osu.Game/Online/Leaderboards/Leaderboard.cs
+++ b/osu.Game/Online/Leaderboards/Leaderboard.cs
@@ -174,12 +174,12 @@ protected Leaderboard()
};
}
- private APIAccess api;
+ private IAPIProvider api;
private ScheduledDelegate pendingUpdateScores;
[BackgroundDependencyLoader(true)]
- private void load(APIAccess api)
+ private void load(IAPIProvider api)
{
this.api = api;
api?.Register(this);
@@ -195,7 +195,7 @@ protected override void Dispose(bool isDisposing)
private APIRequest getScoresRequest;
- public void APIStateChanged(APIAccess api, APIState state)
+ public void APIStateChanged(IAPIProvider api, APIState state)
{
if (state == APIState.Online)
UpdateScores();
@@ -213,12 +213,6 @@ protected void UpdateScores()
pendingUpdateScores?.Cancel();
pendingUpdateScores = Schedule(() =>
{
- if (api?.IsLoggedIn != true)
- {
- PlaceholderState = PlaceholderState.NotLoggedIn;
- return;
- }
-
PlaceholderState = PlaceholderState.Retrieving;
loading.Show();
@@ -231,6 +225,12 @@ protected void UpdateScores()
if (getScoresRequest == null)
return;
+ if (api?.IsLoggedIn != true)
+ {
+ PlaceholderState = PlaceholderState.NotLoggedIn;
+ return;
+ }
+
getScoresRequest.Failure += e => Schedule(() =>
{
if (e is OperationCanceledException)
@@ -243,6 +243,11 @@ protected void UpdateScores()
});
}
+ ///
+ /// Performs a fetch/refresh of scores to be displayed.
+ ///
+ /// A callback which should be called when fetching is completed. Scheduling is not required.
+ /// An responsible for the fetch operation. This will be queued and performed automatically.
protected abstract APIRequest FetchScores(Action> scoresCallback);
private Placeholder currentPlaceholder;
diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
index 34981bf849b4..c5602fc4ad75 100644
--- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs
+++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs
@@ -64,6 +64,8 @@ private void load()
statisticsLabels = GetStatistics(score).Select(s => new ScoreComponentLabel(s)).ToList();
+ Avatar innerAvatar;
+
Children = new Drawable[]
{
new Container
@@ -109,12 +111,11 @@ private void load()
Children = new[]
{
avatar = new DelayedLoadWrapper(
- new Avatar(user)
+ innerAvatar = new Avatar(user)
{
RelativeSizeAxes = Axes.Both,
CornerRadius = corner_radius,
Masking = true,
- OnLoadComplete = d => d.FadeInFromZero(200),
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
@@ -214,6 +215,8 @@ private void load()
},
},
};
+
+ innerAvatar.OnLoadComplete += d => d.FadeInFromZero(200);
}
public override void Show()
diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs
index 77236ce3c8db..53089897f794 100644
--- a/osu.Game/Online/Multiplayer/Room.cs
+++ b/osu.Game/Online/Multiplayer/Room.cs
@@ -124,6 +124,7 @@ public void CopyFrom(Room other)
if (other.Host.Value != null && Host.Value?.Id != other.Host.Value.Id)
Host.Value = other.Host.Value;
+ ChannelId.Value = other.ChannelId.Value;
Status.Value = other.Status.Value;
Availability.Value = other.Availability.Value;
Type.Value = other.Type.Value;
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 643a673faf8e..7d55d19e5010 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -152,7 +152,6 @@ private void load()
API = new APIAccess(LocalConfig);
- dependencies.Cache(API);
dependencies.CacheAs(API);
var defaultBeatmap = new DummyWorkingBeatmap(this);
diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
index 2fc00edbc1e5..13d8df098f60 100644
--- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
+++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs
@@ -33,7 +33,7 @@ public class ScreenEntry : AccountCreationScreen
private OsuTextBox emailTextBox;
private OsuPasswordTextBox passwordTextBox;
- private APIAccess api;
+ private IAPIProvider api;
private ShakeContainer registerShake;
private IEnumerable characterCheckText;
@@ -42,7 +42,7 @@ public class ScreenEntry : AccountCreationScreen
private GameHost host;
[BackgroundDependencyLoader]
- private void load(OsuColour colours, APIAccess api, GameHost host)
+ private void load(OsuColour colours, IAPIProvider api, GameHost host)
{
this.api = api;
this.host = host;
diff --git a/osu.Game/Overlays/AccountCreation/ScreenWarning.cs b/osu.Game/Overlays/AccountCreation/ScreenWarning.cs
index 4e2cc1ea0087..be417f4aac9b 100644
--- a/osu.Game/Overlays/AccountCreation/ScreenWarning.cs
+++ b/osu.Game/Overlays/AccountCreation/ScreenWarning.cs
@@ -22,7 +22,7 @@ public class ScreenWarning : AccountCreationScreen
{
private OsuTextFlowContainer multiAccountExplanationText;
private LinkFlowContainer furtherAssistance;
- private APIAccess api;
+ private IAPIProvider api;
private const string help_centre_url = "/help/wiki/Help_Centre#login";
@@ -39,7 +39,7 @@ public override void OnEntering(IScreen last)
}
[BackgroundDependencyLoader(true)]
- private void load(OsuColour colours, APIAccess api, OsuGame game, TextureStore textures)
+ private void load(OsuColour colours, IAPIProvider api, OsuGame game, TextureStore textures)
{
this.api = api;
diff --git a/osu.Game/Overlays/AccountCreationOverlay.cs b/osu.Game/Overlays/AccountCreationOverlay.cs
index bc780538d5b6..e8e44c206ebb 100644
--- a/osu.Game/Overlays/AccountCreationOverlay.cs
+++ b/osu.Game/Overlays/AccountCreationOverlay.cs
@@ -30,7 +30,7 @@ public AccountCreationOverlay()
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours, APIAccess api)
+ private void load(OsuColour colours, IAPIProvider api)
{
api.Register(this);
@@ -96,7 +96,7 @@ protected override void PopOut()
this.FadeOut(100);
}
- public void APIStateChanged(APIAccess api, APIState state)
+ public void APIStateChanged(IAPIProvider api, APIState state)
{
switch (state)
{
diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/DownloadButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/DownloadButton.cs
index edd886f0f2c4..bbbcff0558a9 100644
--- a/osu.Game/Overlays/BeatmapSet/Buttons/DownloadButton.cs
+++ b/osu.Game/Overlays/BeatmapSet/Buttons/DownloadButton.cs
@@ -39,7 +39,7 @@ public DownloadButton(BeatmapSetInfo beatmapSet, bool noVideo = false)
}
[BackgroundDependencyLoader]
- private void load(APIAccess api, BeatmapManager beatmaps)
+ private void load(IAPIProvider api, BeatmapManager beatmaps)
{
FillFlowContainer textSprites;
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
index 6c65d491af89..3dd03fcea61b 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs
@@ -45,7 +45,7 @@ public IEnumerable Scores
}
private GetScoresRequest getScoresRequest;
- private APIAccess api;
+ private IAPIProvider api;
public BeatmapInfo Beatmap
{
@@ -129,7 +129,7 @@ public ScoresContainer()
}
[BackgroundDependencyLoader]
- private void load(APIAccess api)
+ private void load(IAPIProvider api)
{
this.api = api;
updateDisplay();
diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs
index 55c21b7fc933..c49268bc16b3 100644
--- a/osu.Game/Overlays/BeatmapSetOverlay.cs
+++ b/osu.Game/Overlays/BeatmapSetOverlay.cs
@@ -31,7 +31,7 @@ public class BeatmapSetOverlay : WaveOverlayContainer
private readonly Header header;
- private APIAccess api;
+ private IAPIProvider api;
private RulesetStore rulesets;
private readonly ScrollContainer scroll;
@@ -101,7 +101,7 @@ public BeatmapSetOverlay()
}
[BackgroundDependencyLoader]
- private void load(APIAccess api, RulesetStore rulesets)
+ private void load(IAPIProvider api, RulesetStore rulesets)
{
this.api = api;
this.rulesets = rulesets;
diff --git a/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs
index feb47b9e8e7f..71e9e4bdf34f 100644
--- a/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs
+++ b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs
@@ -190,8 +190,12 @@ protected override void PopOut()
private class HeaderSearchTextBox : SearchTextBox
{
- protected override Color4 BackgroundFocused => Color4.Black.Opacity(0.2f);
- protected override Color4 BackgroundUnfocused => Color4.Black.Opacity(0.2f);
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ BackgroundFocused = Color4.Black.Opacity(0.2f);
+ BackgroundUnfocused = Color4.Black.Opacity(0.2f);
+ }
}
}
}
diff --git a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs
index 8cedde82ff05..8111ac739433 100644
--- a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs
+++ b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs
@@ -28,6 +28,8 @@ public PrivateChannelTabItem(Channel value)
if (value.Type != ChannelType.PM)
throw new ArgumentException("Argument value needs to have the targettype user!");
+ Avatar avatar;
+
AddRange(new Drawable[]
{
new Container
@@ -49,11 +51,10 @@ public PrivateChannelTabItem(Channel value)
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Masking = true,
- Child = new DelayedLoadWrapper(new Avatar(value.Users.First())
+ Child = new DelayedLoadWrapper(avatar = new Avatar(value.Users.First())
{
RelativeSizeAxes = Axes.Both,
OpenOnClick = { Value = false },
- OnLoadComplete = d => d.FadeInFromZero(300, Easing.OutQuint),
})
{
RelativeSizeAxes = Axes.Both,
@@ -63,6 +64,8 @@ public PrivateChannelTabItem(Channel value)
},
});
+ avatar.OnLoadComplete += d => d.FadeInFromZero(300, Easing.OutQuint);
+
Text.X = ChatOverlay.TAB_AREA_HEIGHT;
TextBold.X = ChatOverlay.TAB_AREA_HEIGHT;
}
diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs
index 542827932506..77f88ab4e7c0 100644
--- a/osu.Game/Overlays/ChatOverlay.cs
+++ b/osu.Game/Overlays/ChatOverlay.cs
@@ -320,6 +320,8 @@ protected override void PopOut()
this.MoveToY(Height, transition_length, Easing.InSine);
this.FadeOut(transition_length, Easing.InSine);
+ channelSelectionOverlay.State = Visibility.Hidden;
+
textbox.HoldFocus = false;
base.PopOut();
}
diff --git a/osu.Game/Overlays/Direct/DirectGridPanel.cs b/osu.Game/Overlays/Direct/DirectGridPanel.cs
index 1413f0f8854d..b35dbde639b4 100644
--- a/osu.Game/Overlays/Direct/DirectGridPanel.cs
+++ b/osu.Game/Overlays/Direct/DirectGridPanel.cs
@@ -185,10 +185,7 @@ private void load(OsuColour colours)
Margin = new MarginPadding { Top = vertical_padding, Right = vertical_padding },
Children = new[]
{
- new Statistic(FontAwesome.fa_play_circle, SetInfo.OnlineInfo?.PlayCount ?? 0)
- {
- Margin = new MarginPadding { Right = 1 },
- },
+ new Statistic(FontAwesome.fa_play_circle, SetInfo.OnlineInfo?.PlayCount ?? 0),
new Statistic(FontAwesome.fa_heart, SetInfo.OnlineInfo?.FavouriteCount ?? 0),
},
},
diff --git a/osu.Game/Overlays/Direct/DirectListPanel.cs b/osu.Game/Overlays/Direct/DirectListPanel.cs
index 7bf372dff7cd..d857a0f04271 100644
--- a/osu.Game/Overlays/Direct/DirectListPanel.cs
+++ b/osu.Game/Overlays/Direct/DirectListPanel.cs
@@ -13,6 +13,7 @@
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Drawables;
namespace osu.Game.Overlays.Direct
{
@@ -23,6 +24,7 @@ public class DirectListPanel : DirectPanel
private const float vertical_padding = 5;
private const float height = 70;
+ private FillFlowContainer statusContainer;
private PlayButton playButton;
private Box progressBar;
@@ -108,10 +110,24 @@ private void load(OsuColour colours)
},
new FillFlowContainer
{
- AutoSizeAxes = Axes.X,
- Height = 20,
- Margin = new MarginPadding { Top = vertical_padding, Bottom = vertical_padding },
- Children = GetDifficultyIcons(),
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Children = new Drawable[]
+ {
+ statusContainer = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Margin = new MarginPadding { Vertical = vertical_padding, Horizontal = 5 },
+ Spacing = new Vector2(5),
+ },
+ new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.X,
+ Height = 20,
+ Margin = new MarginPadding { Top = vertical_padding, Bottom = vertical_padding },
+ Children = GetDifficultyIcons(),
+ },
+ },
},
},
},
@@ -144,10 +160,7 @@ private void load(OsuColour colours)
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
- new Statistic(FontAwesome.fa_play_circle, SetInfo.OnlineInfo?.PlayCount ?? 0)
- {
- Margin = new MarginPadding { Right = 1 },
- },
+ new Statistic(FontAwesome.fa_play_circle, SetInfo.OnlineInfo?.PlayCount ?? 0),
new Statistic(FontAwesome.fa_heart, SetInfo.OnlineInfo?.FavouriteCount ?? 0),
new FillFlowContainer
{
@@ -194,6 +207,23 @@ private void load(OsuColour colours)
Colour = colours.Yellow,
},
});
+
+ if (SetInfo.OnlineInfo?.HasVideo ?? false)
+ {
+ statusContainer.Add(new IconPill(FontAwesome.fa_film) { IconSize = new Vector2(20) });
+ }
+
+ if (SetInfo.OnlineInfo?.HasStoryboard ?? false)
+ {
+ statusContainer.Add(new IconPill(FontAwesome.fa_image) { IconSize = new Vector2(20) });
+ }
+
+ statusContainer.Add(new BeatmapSetOnlineStatusPill
+ {
+ TextSize = 12,
+ TextPadding = new MarginPadding { Horizontal = 10, Vertical = 4 },
+ Status = SetInfo.OnlineInfo?.Status ?? BeatmapSetOnlineStatus.None,
+ });
}
}
}
diff --git a/osu.Game/Overlays/Direct/IconPill.cs b/osu.Game/Overlays/Direct/IconPill.cs
index 83ee0cb6c542..e7f516f4491c 100644
--- a/osu.Game/Overlays/Direct/IconPill.cs
+++ b/osu.Game/Overlays/Direct/IconPill.cs
@@ -12,6 +12,14 @@ namespace osu.Game.Overlays.Direct
{
public class IconPill : CircularContainer
{
+ public Vector2 IconSize
+ {
+ get => iconContainer.Size;
+ set => iconContainer.Size = value;
+ }
+
+ private readonly Container iconContainer;
+
public IconPill(FontAwesome icon)
{
AutoSizeAxes = Axes.Both;
@@ -25,16 +33,16 @@ public IconPill(FontAwesome icon)
Colour = Color4.Black,
Alpha = 0.5f,
},
- new Container
+ iconContainer = new Container
{
- AutoSizeAxes = Axes.Both,
- Margin = new MarginPadding(5),
+ Size = new Vector2(22),
+ Padding = new MarginPadding(5),
Child = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
Icon = icon,
- Size = new Vector2(12),
},
},
};
diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs
index 0dc74b6a8877..34edbbcc8b5d 100644
--- a/osu.Game/Overlays/DirectOverlay.cs
+++ b/osu.Game/Overlays/DirectOverlay.cs
@@ -28,7 +28,7 @@ public class DirectOverlay : SearchableListOverlay Scheduler.AddOnce(updateSearch);
+ ((FilterControl)Filter).Ruleset.ValueChanged += _ => queueUpdateSearch();
Filter.DisplayStyleControl.DisplayStyle.ValueChanged += style => recreatePanels(style.NewValue);
- Filter.DisplayStyleControl.Dropdown.Current.ValueChanged += _ => Scheduler.AddOnce(updateSearch);
+ Filter.DisplayStyleControl.Dropdown.Current.ValueChanged += _ => queueUpdateSearch();
Header.Tabs.Current.ValueChanged += tab =>
{
@@ -144,24 +144,11 @@ public DirectOverlay()
{
currentQuery.Value = string.Empty;
Filter.Tabs.Current.Value = (DirectSortCriteria)Header.Tabs.Current.Value;
- Scheduler.AddOnce(updateSearch);
+ queueUpdateSearch();
}
};
- currentQuery.ValueChanged += text =>
- {
- queryChangedDebounce?.Cancel();
-
- if (string.IsNullOrEmpty(text.NewValue))
- Scheduler.AddOnce(updateSearch);
- else
- {
- BeatmapSets = null;
- ResultAmounts = null;
-
- queryChangedDebounce = Scheduler.AddDelayed(updateSearch, 500);
- }
- };
+ currentQuery.ValueChanged += text => queueUpdateSearch(!string.IsNullOrEmpty(text.NewValue));
currentQuery.BindTo(Filter.Search.Current);
@@ -170,14 +157,14 @@ public DirectOverlay()
if (Header.Tabs.Current.Value != DirectTab.Search && tab.NewValue != (DirectSortCriteria)Header.Tabs.Current.Value)
Header.Tabs.Current.Value = DirectTab.Search;
- Scheduler.AddOnce(updateSearch);
+ queueUpdateSearch();
};
updateResultCounts();
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours, APIAccess api, RulesetStore rulesets, PreviewTrackManager previewTrackManager)
+ private void load(OsuColour colours, IAPIProvider api, RulesetStore rulesets, PreviewTrackManager previewTrackManager)
{
this.api = api;
this.rulesets = rulesets;
@@ -242,37 +229,42 @@ protected override void PopIn()
// Queries are allowed to be run only on the first pop-in
if (getSetsRequest == null)
- Scheduler.AddOnce(updateSearch);
+ queueUpdateSearch();
}
private SearchBeatmapSetsRequest getSetsRequest;
- private readonly Bindable currentQuery = new Bindable();
+ private readonly Bindable currentQuery = new Bindable(string.Empty);
private ScheduledDelegate queryChangedDebounce;
private PreviewTrackManager previewTrackManager;
- private void updateSearch()
+ private void queueUpdateSearch(bool queryTextChanged = false)
{
+ BeatmapSets = null;
+ ResultAmounts = null;
+
+ getSetsRequest?.Cancel();
+
queryChangedDebounce?.Cancel();
+ queryChangedDebounce = Scheduler.AddDelayed(updateSearch, queryTextChanged ? 500 : 100);
+ }
+ private void updateSearch()
+ {
if (!IsLoaded)
return;
if (State == Visibility.Hidden)
return;
- BeatmapSets = null;
- ResultAmounts = null;
-
- getSetsRequest?.Cancel();
-
if (api == null)
return;
previewTrackManager.StopAnyPlaying(this);
- getSetsRequest = new SearchBeatmapSetsRequest(currentQuery.Value ?? string.Empty,
+ getSetsRequest = new SearchBeatmapSetsRequest(
+ currentQuery.Value,
((FilterControl)Filter).Ruleset.Value,
Filter.DisplayStyleControl.Dropdown.Current.Value,
Filter.Tabs.Current.Value); //todo: sort direction (?)
diff --git a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs
index eeb42ec99192..431ae98c2c0c 100644
--- a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs
+++ b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs
@@ -109,7 +109,7 @@ public DrawableMedal(Medal medal)
s.Font = s.Font.With(size: 16);
});
- medalContainer.OnLoadComplete = d =>
+ medalContainer.OnLoadComplete += d =>
{
unlocked.Position = new Vector2(0f, medalContainer.DrawSize.Y / 2 + 10);
infoFlow.Position = new Vector2(0f, unlocked.Position.Y + 90);
diff --git a/osu.Game/Overlays/Music/FilterControl.cs b/osu.Game/Overlays/Music/FilterControl.cs
index 6bceade271a1..99017579a23a 100644
--- a/osu.Game/Overlays/Music/FilterControl.cs
+++ b/osu.Game/Overlays/Music/FilterControl.cs
@@ -6,8 +6,8 @@
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osuTK;
-using osuTK.Graphics;
using System;
+using osu.Framework.Allocation;
using osu.Framework.Bindables;
namespace osu.Game.Overlays.Music
@@ -53,15 +53,16 @@ public FilterControl()
public class FilterTextBox : SearchTextBox
{
- protected override Color4 BackgroundUnfocused => OsuColour.Gray(0.06f);
- protected override Color4 BackgroundFocused => OsuColour.Gray(0.12f);
-
protected override bool AllowCommit => true;
- public FilterTextBox()
+ [BackgroundDependencyLoader]
+ private void load()
{
Masking = true;
CornerRadius = 5;
+
+ BackgroundUnfocused = OsuColour.Gray(0.06f);
+ BackgroundFocused = OsuColour.Gray(0.12f);
}
}
}
diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs
index 2641a0551dcf..c41d97770148 100644
--- a/osu.Game/Overlays/Profile/ProfileHeader.cs
+++ b/osu.Game/Overlays/Profile/ProfileHeader.cs
@@ -335,9 +335,12 @@ private void loadUser()
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
FillMode = FillMode.Fill,
- OnLoadComplete = d => d.FadeInFromZero(200),
Depth = float.MaxValue,
- }, coverContainer.Add);
+ }, background =>
+ {
+ coverContainer.Add(background);
+ background.FadeInFromZero(200);
+ });
if (user.IsSupporter)
SupporterTag.Show();
diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
index 497d6c3fc4c7..46c65b9db7e7 100644
--- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs
@@ -28,7 +28,7 @@ public class PaginatedContainer : FillFlowContainer
protected readonly Bindable User = new Bindable();
- protected APIAccess Api;
+ protected IAPIProvider Api;
protected APIRequest RetrievalRequest;
protected RulesetStore Rulesets;
@@ -84,7 +84,7 @@ public PaginatedContainer(Bindable user, string header, string missing)
}
[BackgroundDependencyLoader]
- private void load(APIAccess api, RulesetStore rulesets)
+ private void load(IAPIProvider api, RulesetStore rulesets)
{
Api = api;
Rulesets = rulesets;
diff --git a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs
index 7e721ac807de..8fab29e42c3b 100644
--- a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs
+++ b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent
{
public class DrawableRecentActivity : DrawableProfileRow
{
- private APIAccess api;
+ private IAPIProvider api;
private readonly APIRecentActivity activity;
@@ -28,7 +28,7 @@ public DrawableRecentActivity(APIRecentActivity activity)
}
[BackgroundDependencyLoader]
- private void load(APIAccess api)
+ private void load(IAPIProvider api)
{
this.api = api;
diff --git a/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs b/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs
index 478e3d4c9573..b0a8a0e77de4 100644
--- a/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs
+++ b/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs
@@ -127,10 +127,14 @@ protected override void Update()
private class FilterSearchTextBox : SearchTextBox
{
- protected override Color4 BackgroundUnfocused => OsuColour.Gray(0.06f);
- protected override Color4 BackgroundFocused => OsuColour.Gray(0.12f);
-
protected override bool AllowCommit => true;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ BackgroundUnfocused = OsuColour.Gray(0.06f);
+ BackgroundFocused = OsuColour.Gray(0.12f);
+ }
}
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs
index 026424e1fffd..d6738250f9a6 100644
--- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs
@@ -58,14 +58,14 @@ public LoginSettings()
}
[BackgroundDependencyLoader(permitNulls: true)]
- private void load(OsuColour colours, APIAccess api)
+ private void load(OsuColour colours, IAPIProvider api)
{
this.colours = colours;
api?.Register(this);
}
- public void APIStateChanged(APIAccess api, APIState state)
+ public void APIStateChanged(IAPIProvider api, APIState state)
{
form = null;
@@ -194,7 +194,7 @@ private class LoginForm : FillFlowContainer
{
private TextBox username;
private TextBox password;
- private APIAccess api;
+ private IAPIProvider api;
public Action RequestHide;
@@ -205,7 +205,7 @@ private void performLogin()
}
[BackgroundDependencyLoader(permitNulls: true)]
- private void load(APIAccess api, OsuConfigManager config, AccountCreationOverlay accountCreation)
+ private void load(IAPIProvider api, OsuConfigManager config, AccountCreationOverlay accountCreation)
{
this.api = api;
Direction = FillDirection.Vertical;
diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs
index 3464058abba1..daf3d1c576a7 100644
--- a/osu.Game/Overlays/SocialOverlay.cs
+++ b/osu.Game/Overlays/SocialOverlay.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Overlays
{
public class SocialOverlay : SearchableListOverlay, IOnlineComponent
{
- private APIAccess api;
+ private IAPIProvider api;
private readonly LoadingAnimation loading;
private FillFlowContainer panels;
@@ -89,7 +89,7 @@ public SocialOverlay()
}
[BackgroundDependencyLoader]
- private void load(APIAccess api)
+ private void load(IAPIProvider api)
{
this.api = api;
api.Register(this);
@@ -193,7 +193,7 @@ private void clearPanels()
}
}
- public void APIStateChanged(APIAccess api, APIState state)
+ public void APIStateChanged(IAPIProvider api, APIState state)
{
switch (state)
{
diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs
index 61b2014af833..dca0226499ea 100644
--- a/osu.Game/Overlays/Toolbar/Toolbar.cs
+++ b/osu.Game/Overlays/Toolbar/Toolbar.cs
@@ -22,7 +22,7 @@ public class Toolbar : OverlayContainer
public Action OnHome;
- private readonly ToolbarUserArea userArea;
+ private ToolbarUserArea userArea;
protected override bool BlockPositionalInput => false;
@@ -34,6 +34,13 @@ public class Toolbar : OverlayContainer
private readonly Bindable overlayActivationMode = new Bindable(OverlayActivation.All);
public Toolbar()
+ {
+ RelativeSizeAxes = Axes.X;
+ Size = new Vector2(1, HEIGHT);
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(OsuGame osuGame)
{
Children = new Drawable[]
{
@@ -76,13 +83,6 @@ public Toolbar()
}
};
- RelativeSizeAxes = Axes.X;
- Size = new Vector2(1, HEIGHT);
- }
-
- [BackgroundDependencyLoader(true)]
- private void load(OsuGame osuGame)
- {
StateChanged += visibility =>
{
if (overlayActivationMode.Value == OverlayActivation.Disabled)
diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs
index d47f3a7b16b5..8d1910fc19da 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs
@@ -43,12 +43,12 @@ public ToolbarUserButton()
}
[BackgroundDependencyLoader]
- private void load(APIAccess api)
+ private void load(IAPIProvider api)
{
api.Register(this);
}
- public void APIStateChanged(APIAccess api, APIState state)
+ public void APIStateChanged(IAPIProvider api, APIState state)
{
switch (state)
{
diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs
index 1ff1c2ce9192..48ce05597566 100644
--- a/osu.Game/Overlays/UserProfileOverlay.cs
+++ b/osu.Game/Overlays/UserProfileOverlay.cs
@@ -26,7 +26,7 @@ public class UserProfileOverlay : WaveOverlayContainer
private ProfileSection lastSection;
private ProfileSection[] sections;
private GetUserRequest userReq;
- private APIAccess api;
+ private IAPIProvider api;
protected ProfileHeader Header;
private SectionsContainer sectionsContainer;
private ProfileTabControl tabs;
@@ -56,7 +56,7 @@ public UserProfileOverlay()
}
[BackgroundDependencyLoader]
- private void load(APIAccess api)
+ private void load(IAPIProvider api)
{
this.api = api;
}
diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
index db8bdde6bbe2..aad55f8a3826 100644
--- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
+++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
@@ -37,6 +37,8 @@ protected DifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
/// A structure describing the difficulty of the beatmap.
public DifficultyAttributes Calculate(params Mod[] mods)
{
+ mods = mods.Select(m => m.CreateCopy()).ToArray();
+
beatmap.Mods.Value = mods;
IBeatmap playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo);
diff --git a/osu.Game/Rulesets/Edit/EditRulesetContainer.cs b/osu.Game/Rulesets/Edit/DrawableEditRuleset.cs
similarity index 74%
rename from osu.Game/Rulesets/Edit/EditRulesetContainer.cs
rename to osu.Game/Rulesets/Edit/DrawableEditRuleset.cs
index 8992be2da22e..76a2e7af1235 100644
--- a/osu.Game/Rulesets/Edit/EditRulesetContainer.cs
+++ b/osu.Game/Rulesets/Edit/DrawableEditRuleset.cs
@@ -11,14 +11,14 @@
namespace osu.Game.Rulesets.Edit
{
- public abstract class EditRulesetContainer : CompositeDrawable
+ public abstract class DrawableEditRuleset : CompositeDrawable
{
///
- /// The contained by this .
+ /// The contained by this .
///
public abstract Playfield Playfield { get; }
- internal EditRulesetContainer()
+ internal DrawableEditRuleset()
{
RelativeSizeAxes = Axes.Both;
}
@@ -38,21 +38,21 @@ internal EditRulesetContainer()
internal abstract DrawableHitObject Remove(HitObject hitObject);
}
- public class EditRulesetContainer : EditRulesetContainer
+ public class DrawableEditRuleset : DrawableEditRuleset
where TObject : HitObject
{
- public override Playfield Playfield => rulesetContainer.Playfield;
+ public override Playfield Playfield => drawableRuleset.Playfield;
- private Ruleset ruleset => rulesetContainer.Ruleset;
- private Beatmap beatmap => rulesetContainer.Beatmap;
+ private Ruleset ruleset => drawableRuleset.Ruleset;
+ private Beatmap beatmap => drawableRuleset.Beatmap;
- private readonly RulesetContainer rulesetContainer;
+ private readonly DrawableRuleset drawableRuleset;
- public EditRulesetContainer(RulesetContainer rulesetContainer)
+ public DrawableEditRuleset(DrawableRuleset drawableRuleset)
{
- this.rulesetContainer = rulesetContainer;
+ this.drawableRuleset = drawableRuleset;
- InternalChild = rulesetContainer;
+ InternalChild = drawableRuleset;
Playfield.DisplayJudgements.Value = false;
}
@@ -73,10 +73,10 @@ internal override DrawableHitObject Add(HitObject hitObject)
processor?.PostProcess();
// Add visual representation
- var drawableObject = rulesetContainer.GetVisualRepresentation(tObject);
+ var drawableObject = drawableRuleset.GetVisualRepresentation(tObject);
- rulesetContainer.Playfield.Add(drawableObject);
- rulesetContainer.Playfield.PostProcess();
+ drawableRuleset.Playfield.Add(drawableObject);
+ drawableRuleset.Playfield.PostProcess();
return drawableObject;
}
@@ -97,8 +97,8 @@ internal override DrawableHitObject Remove(HitObject hitObject)
// Remove visual representation
var drawableObject = Playfield.AllHitObjects.Single(d => d.HitObject == hitObject);
- rulesetContainer.Playfield.Remove(drawableObject);
- rulesetContainer.Playfield.PostProcess();
+ drawableRuleset.Playfield.Remove(drawableObject);
+ drawableRuleset.Playfield.PostProcess();
return drawableObject;
}
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index 025564e249bd..45bf9b8be7cd 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Edit
{
public abstract class HitObjectComposer : CompositeDrawable
{
- public IEnumerable HitObjects => RulesetContainer.Playfield.AllHitObjects;
+ public IEnumerable HitObjects => DrawableRuleset.Playfield.AllHitObjects;
protected readonly Ruleset Ruleset;
@@ -34,7 +34,7 @@ public abstract class HitObjectComposer : CompositeDrawable
private readonly List layerContainers = new List();
- protected EditRulesetContainer RulesetContainer { get; private set; }
+ protected DrawableEditRuleset DrawableRuleset { get; private set; }
private BlueprintContainer blueprintContainer;
@@ -54,8 +54,8 @@ private void load(IBindable beatmap, IFrameBasedClock framedCloc
try
{
- RulesetContainer = CreateRulesetContainer();
- RulesetContainer.Clock = framedClock;
+ DrawableRuleset = CreateDrawableRuleset();
+ DrawableRuleset.Clock = framedClock;
}
catch (Exception e)
{
@@ -97,7 +97,7 @@ private void load(IBindable beatmap, IFrameBasedClock framedCloc
Children = new Drawable[]
{
layerBelowRuleset,
- RulesetContainer,
+ DrawableRuleset,
layerAboveRuleset
}
}
@@ -140,27 +140,27 @@ protected override void UpdateAfterChildren()
layerContainers.ForEach(l =>
{
- l.Anchor = RulesetContainer.Playfield.Anchor;
- l.Origin = RulesetContainer.Playfield.Origin;
- l.Position = RulesetContainer.Playfield.Position;
- l.Size = RulesetContainer.Playfield.Size;
+ l.Anchor = DrawableRuleset.Playfield.Anchor;
+ l.Origin = DrawableRuleset.Playfield.Origin;
+ l.Position = DrawableRuleset.Playfield.Position;
+ l.Size = DrawableRuleset.Playfield.Size;
});
}
///
/// Whether the user's cursor is currently in an area of the that is valid for placement.
///
- public virtual bool CursorInPlacementArea => RulesetContainer.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position);
+ public virtual bool CursorInPlacementArea => DrawableRuleset.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position);
///
/// Adds a to the and visualises it.
///
/// The to add.
- public void Add(HitObject hitObject) => blueprintContainer.AddBlueprintFor(RulesetContainer.Add(hitObject));
+ public void Add(HitObject hitObject) => blueprintContainer.AddBlueprintFor(DrawableRuleset.Add(hitObject));
- public void Remove(HitObject hitObject) => blueprintContainer.RemoveBlueprintFor(RulesetContainer.Remove(hitObject));
+ public void Remove(HitObject hitObject) => blueprintContainer.RemoveBlueprintFor(DrawableRuleset.Remove(hitObject));
- internal abstract EditRulesetContainer CreateRulesetContainer();
+ internal abstract DrawableEditRuleset CreateDrawableRuleset();
protected abstract IReadOnlyList CompositionTools { get; }
@@ -189,9 +189,9 @@ protected HitObjectComposer(Ruleset ruleset)
{
}
- internal override EditRulesetContainer CreateRulesetContainer()
- => new EditRulesetContainer(CreateRulesetContainer(Ruleset, Beatmap.Value));
+ internal override DrawableEditRuleset CreateDrawableRuleset()
+ => new DrawableEditRuleset(CreateDrawableRuleset(Ruleset, Beatmap.Value));
- protected abstract RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap);
+ protected abstract DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, WorkingBeatmap beatmap);
}
}
diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs
index 0d6e11c64926..89db954c362b 100644
--- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs
+++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs
@@ -32,6 +32,12 @@ public class DrawableJudgement : CompositeDrawable
protected Container JudgementBody;
protected SpriteText JudgementText;
+ ///
+ /// Duration of initial fade in.
+ /// Default fade out will start immediately after this duration.
+ ///
+ protected virtual double FadeInDuration => 100;
+
///
/// Creates a drawable which visualises a .
///
@@ -65,11 +71,19 @@ private void load(OsuColour colours)
};
}
+ protected virtual void ApplyHitAnimations()
+ {
+ JudgementBody.ScaleTo(0.9f);
+ JudgementBody.ScaleTo(1, 500, Easing.OutElastic);
+
+ this.Delay(FadeInDuration).FadeOut(400);
+ }
+
protected override void LoadComplete()
{
base.LoadComplete();
- this.FadeInFromZero(100, Easing.OutQuint);
+ this.FadeInFromZero(FadeInDuration, Easing.OutQuint);
switch (Result.Type)
{
@@ -85,10 +99,7 @@ protected override void LoadComplete()
this.Delay(600).FadeOut(200);
break;
default:
- JudgementBody.ScaleTo(0.9f);
- JudgementBody.ScaleTo(1, 500, Easing.OutElastic);
-
- this.Delay(100).FadeOut(400);
+ ApplyHitAnimations();
break;
}
diff --git a/osu.Game/Rulesets/Mods/IApplicableToRulesetContainer.cs b/osu.Game/Rulesets/Mods/IApplicableToDrawableRuleset.cs
similarity index 50%
rename from osu.Game/Rulesets/Mods/IApplicableToRulesetContainer.cs
rename to osu.Game/Rulesets/Mods/IApplicableToDrawableRuleset.cs
index addb96a4fe04..b012beb0c064 100644
--- a/osu.Game/Rulesets/Mods/IApplicableToRulesetContainer.cs
+++ b/osu.Game/Rulesets/Mods/IApplicableToDrawableRuleset.cs
@@ -7,15 +7,15 @@
namespace osu.Game.Rulesets.Mods
{
///
- /// An interface for s that can be applied to s.
+ /// An interface for s that can be applied to s.
///
- public interface IApplicableToRulesetContainer : IApplicableMod
+ public interface IApplicableToDrawableRuleset : IApplicableMod
where TObject : HitObject
{
///
- /// Applies this to a .
+ /// Applies this to a .
///
- /// The to apply to.
- void ApplyToRulesetContainer(RulesetContainer rulesetContainer);
+ /// The to apply to.
+ void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset);
}
}
diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs
index 705c5c4ef680..1f9907caa715 100644
--- a/osu.Game/Rulesets/Mods/Mod.cs
+++ b/osu.Game/Rulesets/Mods/Mod.cs
@@ -65,5 +65,10 @@ public abstract class Mod : IMod, IJsonSerializable
///
[JsonIgnore]
public virtual Type[] IncompatibleMods => new Type[] { };
+
+ ///
+ /// Creates a copy of this initialised to a default state.
+ ///
+ public virtual Mod CreateCopy() => (Mod)Activator.CreateInstance(GetType());
}
}
diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs
index 22e70b446e13..1c76abbc4b28 100644
--- a/osu.Game/Rulesets/Mods/ModAutoplay.cs
+++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs
@@ -11,14 +11,10 @@
namespace osu.Game.Rulesets.Mods
{
- public abstract class ModAutoplay : ModAutoplay, IApplicableToRulesetContainer
+ public abstract class ModAutoplay : ModAutoplay, IApplicableToDrawableRuleset
where T : HitObject
{
- protected virtual Score CreateReplayScore(Beatmap beatmap) => new Score { Replay = new Replay() };
-
- public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0;
-
- public virtual void ApplyToRulesetContainer(RulesetContainer rulesetContainer) => rulesetContainer.SetReplayScore(CreateReplayScore(rulesetContainer.Beatmap));
+ public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) => drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap));
}
public abstract class ModAutoplay : Mod, IApplicableFailOverride
@@ -31,5 +27,9 @@ public abstract class ModAutoplay : Mod, IApplicableFailOverride
public override double ScoreMultiplier => 1;
public bool AllowFail => false;
public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail) };
+
+ public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0;
+
+ public virtual Score CreateReplayScore(IBeatmap beatmap) => new Score { Replay = new Replay() };
}
}
diff --git a/osu.Game/Rulesets/Mods/ModDaycore.cs b/osu.Game/Rulesets/Mods/ModDaycore.cs
index 2eea6a237cc0..dded688e808a 100644
--- a/osu.Game/Rulesets/Mods/ModDaycore.cs
+++ b/osu.Game/Rulesets/Mods/ModDaycore.cs
@@ -17,7 +17,7 @@ public abstract class ModDaycore : ModHalfTime
public override void ApplyToClock(IAdjustableClock clock)
{
if (clock is IHasPitchAdjust pitchAdjust)
- pitchAdjust.PitchAdjust = 0.75;
+ pitchAdjust.PitchAdjust *= RateAdjust;
else
base.ApplyToClock(clock);
}
diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs
index b69019cd916d..9ea9eb76bc8d 100644
--- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs
+++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs
@@ -2,12 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using osu.Framework.Timing;
+using System.Linq;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mods
{
- public abstract class ModDoubleTime : Mod, IApplicableToClock
+ public abstract class ModDoubleTime : ModTimeAdjust, IApplicableToClock
{
public override string Name => "Double Time";
public override string Acronym => "DT";
@@ -15,11 +15,9 @@ public abstract class ModDoubleTime : Mod, IApplicableToClock
public override ModType Type => ModType.DifficultyIncrease;
public override string Description => "Zoooooooooom...";
public override bool Ranked => true;
- public override Type[] IncompatibleMods => new[] { typeof(ModHalfTime) };
- public virtual void ApplyToClock(IAdjustableClock clock)
- {
- clock.Rate = 1.5;
- }
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModHalfTime)).ToArray();
+
+ protected override double RateAdjust => 1.5;
}
}
diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs
index dfada5614abe..23e928d991e3 100644
--- a/osu.Game/Rulesets/Mods/ModFlashlight.cs
+++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs
@@ -34,7 +34,7 @@ internal ModFlashlight()
}
}
- public abstract class ModFlashlight : ModFlashlight, IApplicableToRulesetContainer, IApplicableToScoreProcessor
+ public abstract class ModFlashlight : ModFlashlight, IApplicableToDrawableRuleset, IApplicableToScoreProcessor
where T : HitObject
{
public const double FLASHLIGHT_FADE_DURATION = 800;
@@ -45,15 +45,15 @@ public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
Combo.BindTo(scoreProcessor.Combo);
}
- public virtual void ApplyToRulesetContainer(RulesetContainer rulesetContainer)
+ public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
var flashlight = CreateFlashlight();
flashlight.Combo = Combo;
flashlight.RelativeSizeAxes = Axes.Both;
flashlight.Colour = Color4.Black;
- rulesetContainer.KeyBindingInputManager.Add(flashlight);
+ drawableRuleset.KeyBindingInputManager.Add(flashlight);
- flashlight.Breaks = rulesetContainer.Beatmap.Breaks;
+ flashlight.Breaks = drawableRuleset.Beatmap.Breaks;
}
public abstract Flashlight CreateFlashlight();
@@ -61,7 +61,7 @@ public virtual void ApplyToRulesetContainer(RulesetContainer rulesetContainer
public abstract class Flashlight : Drawable
{
internal BindableInt Combo;
- private Shader shader;
+ private IShader shader;
protected override DrawNode CreateDrawNode() => new FlashlightDrawNode();
@@ -139,7 +139,7 @@ protected Vector2 FlashlightSize
private class FlashlightDrawNode : DrawNode
{
- public Shader Shader;
+ public IShader Shader;
public Quad ScreenSpaceDrawQuad;
public Vector2 FlashlightPosition;
public Vector2 FlashlightSize;
diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs
index 1cffa37b0449..fe26c96214f2 100644
--- a/osu.Game/Rulesets/Mods/ModHalfTime.cs
+++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs
@@ -2,12 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using osu.Framework.Timing;
+using System.Linq;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mods
{
- public abstract class ModHalfTime : Mod, IApplicableToClock
+ public abstract class ModHalfTime : ModTimeAdjust, IApplicableToClock
{
public override string Name => "Half Time";
public override string Acronym => "HT";
@@ -15,11 +15,9 @@ public abstract class ModHalfTime : Mod, IApplicableToClock
public override ModType Type => ModType.DifficultyReduction;
public override string Description => "Less zoom...";
public override bool Ranked => true;
- public override Type[] IncompatibleMods => new[] { typeof(ModDoubleTime) };
- public virtual void ApplyToClock(IAdjustableClock clock)
- {
- clock.Rate = 0.75;
- }
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModDoubleTime)).ToArray();
+
+ protected override double RateAdjust => 0.75;
}
}
diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs
index e6bd532f7262..a689292ed761 100644
--- a/osu.Game/Rulesets/Mods/ModNightcore.cs
+++ b/osu.Game/Rulesets/Mods/ModNightcore.cs
@@ -17,7 +17,7 @@ public abstract class ModNightcore : ModDoubleTime
public override void ApplyToClock(IAdjustableClock clock)
{
if (clock is IHasPitchAdjust pitchAdjust)
- pitchAdjust.PitchAdjust = 1.5;
+ pitchAdjust.PitchAdjust *= RateAdjust;
else
base.ApplyToClock(clock);
}
diff --git a/osu.Game/Rulesets/Mods/ModTimeAdjust.cs b/osu.Game/Rulesets/Mods/ModTimeAdjust.cs
new file mode 100644
index 000000000000..513883f55282
--- /dev/null
+++ b/osu.Game/Rulesets/Mods/ModTimeAdjust.cs
@@ -0,0 +1,24 @@
+// 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;
+using osu.Framework.Timing;
+
+namespace osu.Game.Rulesets.Mods
+{
+ public abstract class ModTimeAdjust : Mod
+ {
+ public override Type[] IncompatibleMods => new[] { typeof(ModTimeRamp) };
+
+ protected abstract double RateAdjust { get; }
+
+ public virtual void ApplyToClock(IAdjustableClock clock)
+ {
+ if (clock is IHasTempoAdjust tempo)
+ tempo.TempoAdjust *= RateAdjust;
+ else
+ clock.Rate *= RateAdjust;
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs
new file mode 100644
index 000000000000..62407907c163
--- /dev/null
+++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs
@@ -0,0 +1,89 @@
+// 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.Linq;
+using osu.Framework.Audio;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osuTK;
+
+namespace osu.Game.Rulesets.Mods
+{
+ public abstract class ModTimeRamp : Mod
+ {
+ public override Type[] IncompatibleMods => new[] { typeof(ModTimeAdjust) };
+
+ protected abstract double FinalRateAdjustment { get; }
+ }
+
+ public abstract class ModTimeRamp : ModTimeRamp, IUpdatableByPlayfield, IApplicableToClock, IApplicableToBeatmap
+ where T : HitObject
+ {
+ private double finalRateTime;
+
+ private double beginRampTime;
+
+ private IAdjustableClock clock;
+
+ ///
+ /// The point in the beatmap at which the final ramping rate should be reached.
+ ///
+ private const double final_rate_progress = 0.75f;
+
+ public virtual void ApplyToClock(IAdjustableClock clock)
+ {
+ this.clock = clock;
+
+ lastAdjust = 1;
+
+ // for preview purposes. during gameplay, Update will overwrite this setting.
+ applyAdjustment(1);
+ }
+
+ public virtual void ApplyToBeatmap(Beatmap beatmap)
+ {
+ HitObject lastObject = beatmap.HitObjects.LastOrDefault();
+
+ beginRampTime = beatmap.HitObjects.FirstOrDefault()?.StartTime ?? 0;
+ finalRateTime = final_rate_progress * ((lastObject as IHasEndTime)?.EndTime ?? lastObject?.StartTime ?? 0);
+ }
+
+ public virtual void Update(Playfield playfield)
+ {
+ applyAdjustment((clock.CurrentTime - beginRampTime) / finalRateTime);
+ }
+
+ private double lastAdjust = 1;
+
+ ///
+ /// Adjust the rate along the specified ramp
+ ///
+ /// The amount of adjustment to apply (from 0..1).
+ private void applyAdjustment(double amount)
+ {
+ double adjust = 1 + (Math.Sign(FinalRateAdjustment) * MathHelper.Clamp(amount, 0, 1) * Math.Abs(FinalRateAdjustment));
+
+ switch (clock)
+ {
+ case IHasPitchAdjust pitch:
+ pitch.PitchAdjust /= lastAdjust;
+ pitch.PitchAdjust *= adjust;
+ break;
+ case IHasTempoAdjust tempo:
+ tempo.TempoAdjust /= lastAdjust;
+ tempo.TempoAdjust *= adjust;
+ break;
+ default:
+ clock.Rate /= lastAdjust;
+ clock.Rate *= adjust;
+ break;
+ }
+
+ lastAdjust = adjust;
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Mods/ModWindDown.cs b/osu.Game/Rulesets/Mods/ModWindDown.cs
new file mode 100644
index 000000000000..174070eb8528
--- /dev/null
+++ b/osu.Game/Rulesets/Mods/ModWindDown.cs
@@ -0,0 +1,24 @@
+// 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.Linq;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Rulesets.Mods
+{
+ public class ModWindDown : ModTimeRamp
+ where T : HitObject
+ {
+ public override string Name => "Wind Down";
+ public override string Acronym => "WD";
+ public override string Description => "Sloooow doooown...";
+ public override FontAwesome Icon => FontAwesome.fa_chevron_circle_down;
+ public override double ScoreMultiplier => 1.0;
+
+ protected override double FinalRateAdjustment => -0.25;
+
+ 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
new file mode 100644
index 000000000000..bf9af8a51d65
--- /dev/null
+++ b/osu.Game/Rulesets/Mods/ModWindUp.cs
@@ -0,0 +1,24 @@
+// 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.Linq;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Rulesets.Mods
+{
+ public class ModWindUp : ModTimeRamp
+ where T : HitObject
+ {
+ public override string Name => "Wind Up";
+ public override string Acronym => "WU";
+ public override string Description => "Can you keep up?";
+ public override FontAwesome Icon => FontAwesome.fa_chevron_circle_up;
+ public override double ScoreMultiplier => 1.0;
+
+ protected override double FinalRateAdjustment => 0.5;
+
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindDown)).ToArray();
+ }
+}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs
index 41281e805ec4..0089d1eb88e2 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs
@@ -8,12 +8,14 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
///
/// Legacy osu!catch Spinner-type, used for parsing Beatmaps.
///
- internal sealed class ConvertSpinner : HitObject, IHasEndTime, IHasCombo
+ internal sealed class ConvertSpinner : HitObject, IHasEndTime, IHasXPosition, IHasCombo
{
public double EndTime { get; set; }
public double Duration => EndTime - StartTime;
+ public float X => 256; // Required for CatchBeatmapConverter
+
public bool NewCombo { get; set; }
public int ComboOffset { get; set; }
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
index b9177a325c27..8d6bb8bd3f74 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
@@ -5,7 +5,6 @@
using osu.Game.Rulesets.Objects.Types;
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
using osu.Game.Beatmaps.Formats;
using osu.Game.Audio;
@@ -46,9 +45,11 @@ public override HitObject Parse(string text)
{
string[] split = text.Split(',');
- Vector2 pos = new Vector2((int)Convert.ToSingle(split[0], CultureInfo.InvariantCulture), (int)Convert.ToSingle(split[1], CultureInfo.InvariantCulture));
+ Vector2 pos = new Vector2((int)Parsing.ParseFloat(split[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE));
- ConvertHitObjectType type = (ConvertHitObjectType)int.Parse(split[3]);
+ double startTime = Parsing.ParseDouble(split[2]) + Offset;
+
+ ConvertHitObjectType type = (ConvertHitObjectType)Parsing.ParseInt(split[3]);
int comboOffset = (int)(type & ConvertHitObjectType.ComboOffset) >> 4;
type &= ~ConvertHitObjectType.ComboOffset;
@@ -56,7 +57,7 @@ public override HitObject Parse(string text)
bool combo = type.HasFlag(ConvertHitObjectType.NewCombo);
type &= ~ConvertHitObjectType.NewCombo;
- var soundType = (LegacySoundType)int.Parse(split[4]);
+ var soundType = (LegacySoundType)Parsing.ParseInt(split[4]);
var bankInfo = new SampleBankInfo();
HitObject result = null;
@@ -107,7 +108,7 @@ public override HitObject Parse(string text)
}
string[] temp = t.Split(':');
- points[pointIndex++] = new Vector2((int)Convert.ToDouble(temp[0], CultureInfo.InvariantCulture), (int)Convert.ToDouble(temp[1], CultureInfo.InvariantCulture)) - pos;
+ points[pointIndex++] = new Vector2((int)Parsing.ParseDouble(temp[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseDouble(temp[1], Parsing.MAX_COORDINATE_VALUE)) - pos;
}
// osu-stable special-cased colinear perfect curves to a CurveType.Linear
@@ -116,7 +117,7 @@ public override HitObject Parse(string text)
if (points.Length == 3 && pathType == PathType.PerfectCurve && isLinear(points))
pathType = PathType.Linear;
- int repeatCount = Convert.ToInt32(split[6], CultureInfo.InvariantCulture);
+ int repeatCount = Parsing.ParseInt(split[6]);
if (repeatCount > 9000)
throw new ArgumentOutOfRangeException(nameof(repeatCount), @"Repeat count is way too high");
@@ -125,7 +126,7 @@ public override HitObject Parse(string text)
repeatCount = Math.Max(0, repeatCount - 1);
if (split.Length > 7)
- length = Convert.ToDouble(split[7], CultureInfo.InvariantCulture);
+ length = Math.Max(0, Parsing.ParseDouble(split[7]));
if (split.Length > 10)
readCustomSampleBanks(split[10], bankInfo);
@@ -184,7 +185,9 @@ public override HitObject Parse(string text)
}
else if (type.HasFlag(ConvertHitObjectType.Spinner))
{
- result = CreateSpinner(new Vector2(512, 384) / 2, combo, comboOffset, Convert.ToDouble(split[5], CultureInfo.InvariantCulture) + Offset);
+ double endTime = Math.Max(startTime, Parsing.ParseDouble(split[5]) + Offset);
+
+ result = CreateSpinner(new Vector2(512, 384) / 2, combo, comboOffset, endTime);
if (split.Length > 6)
readCustomSampleBanks(split[6], bankInfo);
@@ -193,12 +196,12 @@ public override HitObject Parse(string text)
{
// Note: Hold is generated by BMS converts
- double endTime = Convert.ToDouble(split[2], CultureInfo.InvariantCulture);
+ double endTime = Math.Max(startTime, Parsing.ParseDouble(split[2]));
if (split.Length > 5 && !string.IsNullOrEmpty(split[5]))
{
string[] ss = split[5].Split(':');
- endTime = Convert.ToDouble(ss[0], CultureInfo.InvariantCulture);
+ endTime = Math.Max(startTime, Parsing.ParseDouble(ss[0]));
readCustomSampleBanks(string.Join(":", ss.Skip(1)), bankInfo);
}
@@ -211,7 +214,7 @@ public override HitObject Parse(string text)
return null;
}
- result.StartTime = Convert.ToDouble(split[2], CultureInfo.InvariantCulture) + Offset;
+ result.StartTime = startTime;
if (result.Samples.Count == 0)
result.Samples = convertSoundType(soundType, bankInfo);
@@ -222,8 +225,14 @@ public override HitObject Parse(string text)
}
catch (FormatException)
{
- throw new FormatException("One or more hit objects were malformed.");
+ Logger.Log("A hitobject could not be parsed correctly and will be ignored", LoggingTarget.Runtime, LogLevel.Important);
}
+ catch (OverflowException)
+ {
+ Logger.Log("A hitobject could not be parsed correctly and will be ignored", LoggingTarget.Runtime, LogLevel.Important);
+ }
+
+ return null;
}
private void readCustomSampleBanks(string str, SampleBankInfo bankInfo)
@@ -233,8 +242,8 @@ private void readCustomSampleBanks(string str, SampleBankInfo bankInfo)
string[] split = str.Split(':');
- var bank = (LegacyBeatmapDecoder.LegacySampleBank)int.Parse(split[0]);
- var addbank = (LegacyBeatmapDecoder.LegacySampleBank)int.Parse(split[1]);
+ var bank = (LegacyBeatmapDecoder.LegacySampleBank)Parsing.ParseInt(split[0]);
+ var addbank = (LegacyBeatmapDecoder.LegacySampleBank)Parsing.ParseInt(split[1]);
string stringBank = bank.ToString().ToLowerInvariant();
if (stringBank == @"none")
@@ -247,10 +256,10 @@ private void readCustomSampleBanks(string str, SampleBankInfo bankInfo)
bankInfo.Add = string.IsNullOrEmpty(stringAddBank) ? stringBank : stringAddBank;
if (split.Length > 2)
- bankInfo.CustomSampleBank = int.Parse(split[2]);
+ bankInfo.CustomSampleBank = Parsing.ParseInt(split[2]);
if (split.Length > 3)
- bankInfo.Volume = int.Parse(split[3]);
+ bankInfo.Volume = Math.Max(0, Parsing.ParseInt(split[3]));
bankInfo.Filename = split.Length > 4 ? split[4] : null;
}
@@ -301,7 +310,16 @@ private List convertSoundType(LegacySoundType type, SampleBankInfo b
{
// Todo: This should return the normal SampleInfos if the specified sample file isn't found, but that's a pretty edge-case scenario
if (!string.IsNullOrEmpty(bankInfo.Filename))
- return new List { new FileSampleInfo { Filename = bankInfo.Filename } };
+ {
+ return new List
+ {
+ new FileSampleInfo
+ {
+ Filename = bankInfo.Filename,
+ Volume = bankInfo.Volume
+ }
+ };
+ }
var soundTypes = new List
{
diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs
new file mode 100644
index 000000000000..7cda7485f9a6
--- /dev/null
+++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs
@@ -0,0 +1,148 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using osuTK;
+
+namespace osu.Game.Rulesets.Objects
+{
+ public static class SliderEventGenerator
+ {
+ public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount, double? legacyLastTickOffset)
+ {
+ // A very lenient maximum length of a slider for ticks to be generated.
+ // This exists for edge cases such as /b/1573664 where the beatmap has been edited by the user, and should never be reached in normal usage.
+ const double max_length = 100000;
+
+ var length = Math.Min(max_length, totalDistance);
+ tickDistance = MathHelper.Clamp(tickDistance, 0, length);
+
+ var minDistanceFromEnd = velocity * 10;
+
+ yield return new SliderEventDescriptor
+ {
+ Type = SliderEventType.Head,
+ SpanIndex = 0,
+ SpanStartTime = startTime,
+ Time = startTime,
+ PathProgress = 0,
+ };
+
+ if (tickDistance != 0)
+ {
+ for (var span = 0; span < spanCount; span++)
+ {
+ var spanStartTime = startTime + span * spanDuration;
+ var reversed = span % 2 == 1;
+
+ for (var d = tickDistance; d <= length; d += tickDistance)
+ {
+ if (d >= length - minDistanceFromEnd)
+ break;
+
+ var pathProgress = d / length;
+ var timeProgress = reversed ? 1 - pathProgress : pathProgress;
+
+ yield return new SliderEventDescriptor
+ {
+ Type = SliderEventType.Tick,
+ SpanIndex = span,
+ SpanStartTime = spanStartTime,
+ Time = spanStartTime + timeProgress * spanDuration,
+ PathProgress = pathProgress,
+ };
+ }
+
+ if (span < spanCount - 1)
+ {
+ yield return new SliderEventDescriptor
+ {
+ Type = SliderEventType.Repeat,
+ SpanIndex = span,
+ SpanStartTime = startTime + span * spanDuration,
+ Time = spanStartTime + spanDuration,
+ PathProgress = (span + 1) % 2,
+ };
+ }
+ }
+ }
+
+ double totalDuration = spanCount * spanDuration;
+
+ // Okay, I'll level with you. I made a mistake. It was 2007.
+ // Times were simpler. osu! was but in its infancy and sliders were a new concept.
+ // A hack was made, which has unfortunately lived through until this day.
+ //
+ // This legacy tick is used for some calculations and judgements where audio output is not required.
+ // Generally we are keeping this around just for difficulty compatibility.
+ // Optimistically we do not want to ever use this for anything user-facing going forwards.
+
+ int finalSpanIndex = spanCount - 1;
+ double finalSpanStartTime = startTime + finalSpanIndex * spanDuration;
+ double finalSpanEndTime = Math.Max(startTime + totalDuration / 2, (finalSpanStartTime + spanDuration) - (legacyLastTickOffset ?? 0));
+ double finalProgress = (finalSpanEndTime - finalSpanStartTime) / spanDuration;
+
+ if (spanCount % 2 == 0) finalProgress = 1 - finalProgress;
+
+ yield return new SliderEventDescriptor
+ {
+ Type = SliderEventType.LegacyLastTick,
+ SpanIndex = finalSpanIndex,
+ SpanStartTime = finalSpanStartTime,
+ Time = finalSpanEndTime,
+ PathProgress = finalProgress,
+ };
+
+ yield return new SliderEventDescriptor
+ {
+ Type = SliderEventType.Tail,
+ SpanIndex = finalSpanIndex,
+ SpanStartTime = startTime + (spanCount - 1) * spanDuration,
+ Time = startTime + totalDuration,
+ PathProgress = spanCount % 2,
+ };
+ }
+ }
+
+ ///
+ /// Describes a point in time on a slider given special meaning.
+ /// Should be used by rulesets to visualise the slider.
+ ///
+ public struct SliderEventDescriptor
+ {
+ ///
+ /// The type of event.
+ ///
+ public SliderEventType Type;
+
+ ///
+ /// The time of this event.
+ ///
+ public double Time;
+
+ ///
+ /// The zero-based index of the span. In the case of repeat sliders, this will increase after each .
+ ///
+ public int SpanIndex;
+
+ ///
+ /// The time at which the contained begins.
+ ///
+ public double SpanStartTime;
+
+ ///
+ /// The progress along the slider's at which this event occurs.
+ ///
+ public double PathProgress;
+ }
+
+ public enum SliderEventType
+ {
+ Tick,
+ LegacyLastTick,
+ Head,
+ Tail,
+ Repeat
+ }
+}
diff --git a/osu.Game/Rulesets/Replays/AutoGenerator.cs b/osu.Game/Rulesets/Replays/AutoGenerator.cs
index efca0a58839e..1d4cdbf04cbd 100644
--- a/osu.Game/Rulesets/Replays/AutoGenerator.cs
+++ b/osu.Game/Rulesets/Replays/AutoGenerator.cs
@@ -1,14 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Game.Rulesets.Objects;
using osu.Game.Beatmaps;
using osu.Game.Replays;
namespace osu.Game.Rulesets.Replays
{
- public abstract class AutoGenerator : IAutoGenerator
- where T : HitObject
+ public abstract class AutoGenerator : IAutoGenerator
{
///
/// Creates the auto replay and returns it.
@@ -21,11 +19,11 @@ public abstract class AutoGenerator : IAutoGenerator
///
/// The beatmap we're making.
///
- protected Beatmap Beatmap;
+ protected IBeatmap Beatmap;
#endregion
- protected AutoGenerator(Beatmap beatmap)
+ protected AutoGenerator(IBeatmap beatmap)
{
Beatmap = beatmap;
}
diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs
index ffab0abebf73..feac49ca2cdc 100644
--- a/osu.Game/Rulesets/Ruleset.cs
+++ b/osu.Game/Rulesets/Ruleset.cs
@@ -42,7 +42,7 @@ public IEnumerable GetAllMods() => Enum.GetValues(typeof(ModType)).CastAn enumerable of constructed s
public virtual IEnumerable ConvertLegacyMods(LegacyMods mods) => new Mod[] { };
- public Mod GetAutoplayMod() => GetAllMods().First(mod => mod is ModAutoplay);
+ public ModAutoplay GetAutoplayMod() => GetAllMods().OfType().First();
protected Ruleset(RulesetInfo rulesetInfo = null)
{
@@ -55,7 +55,7 @@ protected Ruleset(RulesetInfo rulesetInfo = null)
/// The beatmap to create the hit renderer for.
/// Unable to successfully load the beatmap to be usable with this ruleset.
///
- public abstract RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap);
+ public abstract DrawableRuleset CreateDrawableRulesetWith(WorkingBeatmap beatmap);
///
/// Creates a to convert a to one that is applicable for this .
diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
index 63e1c93dd550..0fddb19a6c67 100644
--- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
@@ -210,15 +210,15 @@ protected ScoreProcessor()
{
}
- public ScoreProcessor(RulesetContainer rulesetContainer)
+ public ScoreProcessor(DrawableRuleset drawableRuleset)
{
Debug.Assert(base_portion + combo_portion == 1.0);
- rulesetContainer.OnNewResult += applyResult;
- rulesetContainer.OnRevertResult += revertResult;
+ drawableRuleset.OnNewResult += applyResult;
+ drawableRuleset.OnRevertResult += revertResult;
- ApplyBeatmap(rulesetContainer.Beatmap);
- SimulateAutoplay(rulesetContainer.Beatmap);
+ ApplyBeatmap(drawableRuleset.Beatmap);
+ SimulateAutoplay(drawableRuleset.Beatmap);
Reset(true);
if (maxBaseScore == 0 || maxHighestCombo == 0)
diff --git a/osu.Game/Rulesets/UI/RulesetContainer.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs
similarity index 64%
rename from osu.Game/Rulesets/UI/RulesetContainer.cs
rename to osu.Game/Rulesets/UI/DrawableRuleset.cs
index 1c29cf4e2b24..31c0afd74391 100644
--- a/osu.Game/Rulesets/UI/RulesetContainer.cs
+++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs
@@ -16,23 +16,25 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Input;
+using osu.Framework.Input.Events;
using osu.Game.Configuration;
+using osu.Game.Graphics.Cursor;
using osu.Game.Input.Handlers;
using osu.Game.Overlays;
using osu.Game.Replays;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
+using osu.Game.Screens.Play;
namespace osu.Game.Rulesets.UI
{
///
- /// Base RulesetContainer. Doesn't hold objects.
- ///
- /// Should not be derived - derive instead.
- ///
+ /// Displays an interactive ruleset gameplay instance.
///
- public abstract class RulesetContainer : Container
+ /// The type of HitObject contained by this DrawableRuleset.
+ public abstract class DrawableRuleset : DrawableRuleset, IProvideCursor, ICanAttachKeyCounter
+ where TObject : HitObject
{
///
/// The selected variant.
@@ -40,27 +42,11 @@ public abstract class RulesetContainer : Container
public virtual int Variant => 0;
///
- /// The input manager for this RulesetContainer.
- ///
- internal IHasReplayHandler ReplayInputManager => KeyBindingInputManager as IHasReplayHandler;
-
- ///
- /// The key conversion input manager for this RulesetContainer.
+ /// The key conversion input manager for this DrawableRuleset.
///
public PassThroughInputManager KeyBindingInputManager;
- ///
- /// Whether a replay is currently loaded.
- ///
- public readonly BindableBool HasReplayLoaded = new BindableBool();
-
- public abstract IEnumerable Objects { get; }
-
- ///
- /// The point in time at which gameplay starts, including any required lead-in for display purposes.
- /// Defaults to two seconds before the first . Override as necessary.
- ///
- public virtual double GameplayStartTime => Objects.First().StartTime - 2000;
+ public override double GameplayStartTime => Objects.First().StartTime - 2000;
private readonly Lazy playfield;
@@ -72,26 +58,54 @@ public abstract class RulesetContainer : Container
///
/// Place to put drawables above hit objects but below UI.
///
- public Container Overlays { get; protected set; }
+ public Container Overlays { get; private set; }
///
- /// The cursor provided by this . May be null if no cursor is provided.
+ /// Invoked when a has been applied by a .
///
- public readonly CursorContainer Cursor;
+ public event Action OnNewResult;
- public readonly Ruleset Ruleset;
+ ///
+ /// Invoked when a is being reverted by a .
+ ///
+ public event Action OnRevertResult;
+
+ ///
+ /// The beatmap.
+ ///
+ public Beatmap Beatmap;
+
+ public override IEnumerable Objects => Beatmap.HitObjects;
protected IRulesetConfigManager Config { get; private set; }
+ ///
+ /// The mods which are to be applied.
+ ///
+ private readonly IEnumerable mods;
+
+ private FrameStabilityContainer frameStabilityContainer;
+
private OnScreenDisplay onScreenDisplay;
///
- /// A visual representation of a .
+ /// Creates a ruleset visualisation for the provided ruleset and beatmap.
///
- /// The ruleset being repesented.
- protected RulesetContainer(Ruleset ruleset)
+ /// The ruleset being represented.
+ /// The beatmap to create the hit renderer for.
+ protected DrawableRuleset(Ruleset ruleset, WorkingBeatmap workingBeatmap)
+ : base(ruleset)
{
- Ruleset = ruleset;
+ Debug.Assert(workingBeatmap != null, "DrawableRuleset initialized with a null beatmap.");
+
+ RelativeSizeAxes = Axes.Both;
+
+ Beatmap = (Beatmap)workingBeatmap.GetPlayableBeatmap(ruleset.RulesetInfo);
+
+ mods = workingBeatmap.Mods.Value;
+ applyBeatmapMods(mods);
+
+ KeyBindingInputManager = CreateInputManager();
playfield = new Lazy(CreatePlayfield);
IsPaused.ValueChanged += paused =>
@@ -101,8 +115,6 @@ protected RulesetContainer(Ruleset ruleset)
KeyBindingInputManager.UseParentInput = !paused.NewValue;
};
-
- Cursor = CreateCursor();
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@@ -121,158 +133,102 @@ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnl
return dependencies;
}
- public abstract ScoreProcessor CreateScoreProcessor();
-
- ///
- /// Creates a key conversion input manager. An exception will be thrown if a valid is not returned.
- ///
- /// The input manager.
- public abstract PassThroughInputManager CreateInputManager();
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ KeyBindingInputManager.AddRange(new Drawable[]
+ {
+ Playfield
+ });
- protected virtual ReplayInputHandler CreateReplayInputHandler(Replay replay) => null;
+ InternalChildren = new Drawable[]
+ {
+ frameStabilityContainer = new FrameStabilityContainer
+ {
+ Child = KeyBindingInputManager,
+ },
+ Overlays = new Container { RelativeSizeAxes = Axes.Both }
+ };
- public Score ReplayScore { get; private set; }
+ applyRulesetMods(mods, config);
- ///
- /// Whether the game is paused. Used to block user input.
- ///
- public readonly BindableBool IsPaused = new BindableBool();
+ loadObjects();
+ }
///
- /// Sets a replay to be used, overriding local input.
+ /// Creates and adds drawable representations of hit objects to the play field.
///
- /// The replay, null for local input.
- public virtual void SetReplayScore(Score replayScore)
+ private void loadObjects()
{
- if (ReplayInputManager == null)
- throw new InvalidOperationException($"A {nameof(KeyBindingInputManager)} which supports replay loading is not available");
+ foreach (TObject h in Beatmap.HitObjects)
+ addRepresentation(h);
- ReplayScore = replayScore;
- ReplayInputManager.ReplayInputHandler = replayScore != null ? CreateReplayInputHandler(replayScore.Replay) : null;
+ Playfield.PostProcess();
- HasReplayLoaded.Value = ReplayInputManager.ReplayInputHandler != null;
+ foreach (var mod in mods.OfType())
+ mod.ApplyToDrawableHitObjects(Playfield.HitObjectContainer.Objects);
}
- ///
- /// Creates the cursor. May be null if the doesn't provide a custom cursor.
- ///
- protected virtual CursorContainer CreateCursor() => null;
+ public override void RequestResume(Action continueResume) => continueResume();
///
- /// Creates a Playfield.
+ /// Creates and adds the visual representation of a to this .
///
- /// The Playfield.
- protected abstract Playfield CreatePlayfield();
-
- protected override void Dispose(bool isDisposing)
+ /// The to add the visual representation for.
+ private void addRepresentation(TObject hitObject)
{
- base.Dispose(isDisposing);
+ var drawableObject = GetVisualRepresentation(hitObject);
- if (Config != null)
- {
- onScreenDisplay?.StopTracking(this, Config);
- Config = null;
- }
+ if (drawableObject == null)
+ return;
+
+ drawableObject.OnNewResult += (_, r) => OnNewResult?.Invoke(r);
+ drawableObject.OnRevertResult += (_, r) => OnRevertResult?.Invoke(r);
+
+ Playfield.Add(drawableObject);
}
- }
- ///
- /// RulesetContainer that applies conversion to Beatmaps. Does not contain a Playfield
- /// and does not load drawable hit objects.
- ///
- /// Should not be derived - derive instead.
- ///
- ///
- /// The type of HitObject contained by this RulesetContainer.
- public abstract class RulesetContainer : RulesetContainer
- where TObject : HitObject
- {
- ///
- /// Invoked when a has been applied by a .
- ///
- public event Action OnNewResult;
+ public override void SetReplayScore(Score replayScore)
+ {
+ if (!(KeyBindingInputManager is IHasReplayHandler replayInputManager))
+ throw new InvalidOperationException($"A {nameof(KeyBindingInputManager)} which supports replay loading is not available");
- ///
- /// Invoked when a is being reverted by a .
- ///
- public event Action OnRevertResult;
+ var handler = (ReplayScore = replayScore) != null ? CreateReplayInputHandler(replayScore.Replay) : null;
- ///
- /// The Beatmap
- ///
- public Beatmap Beatmap;
+ replayInputManager.ReplayInputHandler = handler;
+ frameStabilityContainer.ReplayInputHandler = handler;
- ///
- /// All the converted hit objects contained by this hit renderer.
- ///
- public override IEnumerable Objects => Beatmap.HitObjects;
+ HasReplayLoaded.Value = replayInputManager.ReplayInputHandler != null;
- ///
- /// The mods which are to be applied.
- ///
- protected IEnumerable Mods;
+ if (replayInputManager.ReplayInputHandler != null)
+ replayInputManager.ReplayInputHandler.GamefieldToScreenSpace = Playfield.GamefieldToScreenSpace;
+ }
///
- /// The this was created with.
+ /// Creates a DrawableHitObject from a HitObject.
///
- protected readonly WorkingBeatmap WorkingBeatmap;
-
- public override ScoreProcessor CreateScoreProcessor() => new ScoreProcessor(this);
+ /// The HitObject to make drawable.
+ /// The DrawableHitObject.
+ public abstract DrawableHitObject GetVisualRepresentation(TObject h);
- protected override Container Content => content;
- private Container content;
+ public void Attach(KeyCounterCollection keyCounter) =>
+ (KeyBindingInputManager as ICanAttachKeyCounter)?.Attach(keyCounter);
///
- /// Whether to assume the beatmap passed into this is for the current ruleset.
- /// Creates a hit renderer for a beatmap.
+ /// Creates a key conversion input manager. An exception will be thrown if a valid is not returned.
///
- /// The ruleset being repesented.
- /// The beatmap to create the hit renderer for.
- protected RulesetContainer(Ruleset ruleset, WorkingBeatmap workingBeatmap)
- : base(ruleset)
- {
- Debug.Assert(workingBeatmap != null, "RulesetContainer initialized with a null beatmap.");
-
- WorkingBeatmap = workingBeatmap;
- // ReSharper disable once PossibleNullReferenceException
- Mods = workingBeatmap.Mods.Value;
-
- RelativeSizeAxes = Axes.Both;
-
- Beatmap = (Beatmap)workingBeatmap.GetPlayableBeatmap(ruleset.RulesetInfo);
-
- KeyBindingInputManager = CreateInputManager();
- KeyBindingInputManager.RelativeSizeAxes = Axes.Both;
-
- applyBeatmapMods(Mods);
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuConfigManager config)
- {
- KeyBindingInputManager.AddRange(new Drawable[]
- {
- content = new Container
- {
- RelativeSizeAxes = Axes.Both,
- },
- Playfield
- });
-
- if (Cursor != null)
- KeyBindingInputManager.Add(Cursor);
+ /// The input manager.
+ protected abstract PassThroughInputManager CreateInputManager();
- InternalChildren = new Drawable[]
- {
- KeyBindingInputManager,
- Overlays = new Container { RelativeSizeAxes = Axes.Both }
- };
+ protected virtual ReplayInputHandler CreateReplayInputHandler(Replay replay) => null;
- // Apply mods
- applyRulesetMods(Mods, config);
+ ///
+ /// Creates a Playfield.
+ ///
+ /// The Playfield.
+ protected abstract Playfield CreatePlayfield();
- loadObjects();
- }
+ public override ScoreProcessor CreateScoreProcessor() => new ScoreProcessor(this);
///
/// Applies the active mods to the Beatmap.
@@ -288,7 +244,7 @@ private void applyBeatmapMods(IEnumerable mods)
}
///
- /// Applies the active mods to this RulesetContainer.
+ /// Applies the active mods to this DrawableRuleset.
///
///
private void applyRulesetMods(IEnumerable mods, OsuConfigManager config)
@@ -296,83 +252,108 @@ private void applyRulesetMods(IEnumerable mods, OsuConfigManager config)
if (mods == null)
return;
- foreach (var mod in mods.OfType>())
- mod.ApplyToRulesetContainer(this);
+ foreach (var mod in mods.OfType>())
+ mod.ApplyToDrawableRuleset(this);
foreach (var mod in mods.OfType())
mod.ReadFromConfig(config);
}
- public override void SetReplayScore(Score replayScore)
+ #region IProvideCursor
+
+ protected override bool OnHover(HoverEvent e) => true; // required for IProvideCursor
+
+ public override CursorContainer Cursor => Playfield.Cursor;
+
+ public bool ProvidingUserCursor => Playfield.Cursor != null && !HasReplayLoaded.Value;
+
+ #endregion
+
+ protected override void Dispose(bool isDisposing)
{
- base.SetReplayScore(replayScore);
+ base.Dispose(isDisposing);
- if (ReplayInputManager?.ReplayInputHandler != null)
- ReplayInputManager.ReplayInputHandler.GamefieldToScreenSpace = Playfield.GamefieldToScreenSpace;
+ if (Config != null)
+ {
+ onScreenDisplay?.StopTracking(this, Config);
+ Config = null;
+ }
}
+ }
+ ///
+ /// Displays an interactive ruleset gameplay instance.
+ ///
+ /// This type is required only for adding non-generic type to the draw hierarchy.
+ /// Once IDrawable is a thing, this can also become an interface.
+ ///
+ ///
+ public abstract class DrawableRuleset : CompositeDrawable
+ {
///
- /// Creates and adds drawable representations of hit objects to the play field.
+ /// Whether a replay is currently loaded.
///
- private void loadObjects()
- {
- foreach (TObject h in Beatmap.HitObjects)
- AddRepresentation(h);
+ public readonly BindableBool HasReplayLoaded = new BindableBool();
- Playfield.PostProcess();
+ ///
+ /// Whether the game is paused. Used to block user input.
+ ///
+ public readonly BindableBool IsPaused = new BindableBool();
- foreach (var mod in Mods.OfType())
- mod.ApplyToDrawableHitObjects(Playfield.HitObjectContainer.Objects);
- }
+ /// ~
+ /// The associated ruleset.
+ ///
+ public readonly Ruleset Ruleset;
///
- /// Creates and adds the visual representation of a to this .
+ /// Creates a ruleset visualisation for the provided ruleset.
///
- /// The to add the visual representation for.
- internal void AddRepresentation(TObject hitObject)
+ /// The ruleset.
+ internal DrawableRuleset(Ruleset ruleset)
{
- var drawableObject = GetVisualRepresentation(hitObject);
+ Ruleset = ruleset;
+ }
- if (drawableObject == null)
- return;
+ ///
+ /// All the converted hit objects contained by this hit renderer.
+ ///
+ public abstract IEnumerable Objects { get; }
- drawableObject.OnNewResult += (_, r) => OnNewResult?.Invoke(r);
- drawableObject.OnRevertResult += (_, r) => OnRevertResult?.Invoke(r);
+ ///
+ /// The point in time at which gameplay starts, including any required lead-in for display purposes.
+ /// Defaults to two seconds before the first . Override as necessary.
+ ///
+ public abstract double GameplayStartTime { get; }
- Playfield.Add(drawableObject);
- }
+ ///
+ /// The currently loaded replay. Usually null in the case of a local player.
+ ///
+ public Score ReplayScore { get; protected set; }
///
- /// Creates a DrawableHitObject from a HitObject.
+ /// The cursor being displayed by the . May be null if no cursor is provided.
///
- /// The HitObject to make drawable.
- /// The DrawableHitObject.
- public abstract DrawableHitObject GetVisualRepresentation(TObject h);
- }
+ public abstract CursorContainer Cursor { get; }
- ///
- /// A derivable RulesetContainer that manages the Playfield and HitObjects.
- ///
- /// The type of Playfield contained by this RulesetContainer.
- /// The type of HitObject contained by this RulesetContainer.
- public abstract class RulesetContainer : RulesetContainer
- where TObject : HitObject
- where TPlayfield : Playfield
- {
///
- /// The playfield.
+ /// Sets a replay to be used, overriding local input.
///
- protected new TPlayfield Playfield => (TPlayfield)base.Playfield;
+ /// The replay, null for local input.
+ public abstract void SetReplayScore(Score replayScore);
///
- /// Creates a hit renderer for a beatmap.
+ /// Invoked when the interactive user requests resuming from a paused state.
+ /// Allows potentially delaying the resume process until an interaction is performed.
///
- /// The ruleset being repesented.
- /// The beatmap to create the hit renderer for.
- protected RulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
- : base(ruleset, beatmap)
- {
- }
+ /// The action to run when resuming is to be completed.
+ public abstract void RequestResume(Action continueResume);
+
+ ///
+ /// Create a for the associated ruleset and link with this
+ /// .
+ ///
+ /// A score processor.
+ public abstract ScoreProcessor CreateScoreProcessor();
}
public class BeatmapInvalidForRulesetException : ArgumentException
diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
new file mode 100644
index 000000000000..161e7aecb4e9
--- /dev/null
+++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs
@@ -0,0 +1,153 @@
+// 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.Timing;
+using osu.Game.Input.Handlers;
+using osu.Game.Screens.Play;
+
+namespace osu.Game.Rulesets.UI
+{
+ ///
+ /// A container which consumes a parent gameplay clock and standardises frame counts for children.
+ /// Will ensure a minimum of 40 frames per clock second is maintained, regardless of any system lag or seeks.
+ ///
+ public class FrameStabilityContainer : Container, IHasReplayHandler
+ {
+ public FrameStabilityContainer()
+ {
+ RelativeSizeAxes = Axes.Both;
+ gameplayClock = new GameplayClock(framedClock = new FramedClock(manualClock = new ManualClock()));
+ }
+
+ private readonly ManualClock manualClock;
+
+ private readonly FramedClock framedClock;
+
+ [Cached]
+ private GameplayClock gameplayClock;
+
+ private IFrameBasedClock parentGameplayClock;
+
+ [BackgroundDependencyLoader(true)]
+ private void load(GameplayClock clock)
+ {
+ if (clock != null)
+ parentGameplayClock = clock;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ setClock();
+ }
+
+ ///
+ /// Whether we are running up-to-date with our parent clock.
+ /// If not, we will need to keep processing children until we catch up.
+ ///
+ private bool requireMoreUpdateLoops;
+
+ ///
+ /// Whether we are in a valid state (ie. should we keep processing children frames).
+ /// This should be set to false when the replay is, for instance, waiting for future frames to arrive.
+ ///
+ private bool validState;
+
+ protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && validState;
+
+ private bool isAttached => ReplayInputHandler != null;
+
+ private const int max_catch_up_updates_per_frame = 50;
+
+ private const double sixty_frame_time = 1000.0 / 60;
+
+ public override bool UpdateSubTree()
+ {
+ requireMoreUpdateLoops = true;
+ validState = true;
+
+ int loops = 0;
+
+ while (validState && requireMoreUpdateLoops && loops++ < max_catch_up_updates_per_frame)
+ {
+ updateClock();
+
+ if (validState)
+ {
+ base.UpdateSubTree();
+ UpdateSubTreeMasking(this, ScreenSpaceDrawQuad.AABBFloat);
+ }
+ }
+
+ return true;
+ }
+
+ private void updateClock()
+ {
+ if (parentGameplayClock == null)
+ setClock(); // LoadComplete may not be run yet, but we still want the clock.
+
+ validState = true;
+
+ manualClock.Rate = parentGameplayClock.Rate;
+ manualClock.IsRunning = parentGameplayClock.IsRunning;
+
+ var newProposedTime = parentGameplayClock.CurrentTime;
+
+ try
+ {
+ if (Math.Abs(manualClock.CurrentTime - newProposedTime) > sixty_frame_time * 1.2f)
+ {
+ newProposedTime = manualClock.Rate > 0
+ ? Math.Min(newProposedTime, manualClock.CurrentTime + sixty_frame_time)
+ : Math.Max(newProposedTime, manualClock.CurrentTime - sixty_frame_time);
+ }
+
+ if (!isAttached)
+ {
+ manualClock.CurrentTime = newProposedTime;
+ }
+ else
+ {
+ double? newTime = ReplayInputHandler.SetFrameFromTime(newProposedTime);
+
+ if (newTime == null)
+ {
+ // we shouldn't execute for this time value. probably waiting on more replay data.
+ validState = false;
+
+ requireMoreUpdateLoops = true;
+ manualClock.CurrentTime = newProposedTime;
+ return;
+ }
+
+ manualClock.CurrentTime = newTime.Value;
+ }
+
+ requireMoreUpdateLoops = manualClock.CurrentTime != parentGameplayClock.CurrentTime;
+ }
+ finally
+ {
+ // The manual clock time has changed in the above code. The framed clock now needs to be updated
+ // to ensure that the its time is valid for our children before input is processed
+ framedClock.ProcessFrame();
+ }
+ }
+
+ private void setClock()
+ {
+ // in case a parent gameplay clock isn't available, just use the parent clock.
+ if (parentGameplayClock == null)
+ parentGameplayClock = Clock;
+
+ Clock = gameplayClock;
+ ProcessCustomClock = false;
+ }
+
+ public ReplayInputHandler ReplayInputHandler { get; set; }
+ }
+}
diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs
index 3b8a7353c6e4..78d14a27e345 100644
--- a/osu.Game/Rulesets/UI/Playfield.cs
+++ b/osu.Game/Rulesets/UI/Playfield.cs
@@ -10,6 +10,7 @@
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osuTK;
@@ -63,6 +64,10 @@ protected Playfield()
private void load(IBindable beatmap)
{
this.beatmap = beatmap.Value;
+
+ Cursor = CreateCursor();
+ if (Cursor != null)
+ CursorTargetContainer.Add(Cursor);
}
///
@@ -82,6 +87,23 @@ private void load(IBindable beatmap)
/// The DrawableHitObject to remove.
public virtual bool Remove(DrawableHitObject h) => HitObjectContainer.Remove(h);
+ ///
+ /// The cursor currently being used by this . May be null if no cursor is provided.
+ ///
+ public CursorContainer Cursor { get; private set; }
+
+ ///
+ /// Provide an optional cursor which is to be used for gameplay.
+ /// If providing a cursor, must also point to a valid target container.
+ ///
+ /// The cursor, or null if a cursor is not rqeuired.
+ protected virtual CursorContainer CreateCursor() => null;
+
+ ///
+ /// The target container to add the cursor after it is created.
+ ///
+ protected virtual Container CursorTargetContainer => null;
+
///
/// Registers a as a nested .
/// This does not add the to the draw hierarchy.
diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs
index fc61d41ab4a7..e3031667740e 100644
--- a/osu.Game/Rulesets/UI/RulesetInputManager.cs
+++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -12,7 +11,6 @@
using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges.Events;
using osu.Framework.Input.States;
-using osu.Framework.Timing;
using osu.Game.Configuration;
using osu.Game.Input.Bindings;
using osu.Game.Input.Handlers;
@@ -43,6 +41,12 @@ protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBind
InternalChild = KeyBindingContainer = CreateKeyBindingContainer(ruleset, variant, unique);
}
+ [BackgroundDependencyLoader(true)]
+ private void load(OsuConfigManager config)
+ {
+ mouseDisabled = config.GetBindable(OsuSetting.MouseDisableButtons);
+ }
+
#region Action mapping (for replays)
public override void HandleInputStateChange(InputStateChangeEvent inputStateChange)
@@ -84,128 +88,10 @@ public ReplayInputHandler ReplayInputHandler
#endregion
- #region Clock control
-
- private ManualClock clock;
- private IFrameBasedClock parentClock;
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- //our clock will now be our parent's clock, but we want to replace this to allow manual control.
- parentClock = Clock;
-
- ProcessCustomClock = false;
- Clock = new FramedClock(clock = new ManualClock
- {
- CurrentTime = parentClock.CurrentTime,
- Rate = parentClock.Rate,
- });
- }
-
- ///
- /// Whether we are running up-to-date with our parent clock.
- /// If not, we will need to keep processing children until we catch up.
- ///
- private bool requireMoreUpdateLoops;
-
- ///
- /// Whether we are in a valid state (ie. should we keep processing children frames).
- /// This should be set to false when the replay is, for instance, waiting for future frames to arrive.
- ///
- private bool validState;
-
- protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && validState;
-
- private bool isAttached => replayInputHandler != null && !UseParentInput;
-
- private const int max_catch_up_updates_per_frame = 50;
-
- private const double sixty_frame_time = 1000.0 / 60;
-
- public override bool UpdateSubTree()
- {
- requireMoreUpdateLoops = true;
- validState = true;
-
- int loops = 0;
-
- while (validState && requireMoreUpdateLoops && loops++ < max_catch_up_updates_per_frame)
- {
- updateClock();
-
- if (validState)
- {
- base.UpdateSubTree();
- UpdateSubTreeMasking(this, ScreenSpaceDrawQuad.AABBFloat);
- }
- }
-
- return true;
- }
-
- private void updateClock()
- {
- if (parentClock == null) return;
-
- clock.Rate = parentClock.Rate;
- clock.IsRunning = parentClock.IsRunning;
-
- var newProposedTime = parentClock.CurrentTime;
-
- try
- {
- if (Math.Abs(clock.CurrentTime - newProposedTime) > sixty_frame_time * 1.2f)
- {
- newProposedTime = clock.Rate > 0
- ? Math.Min(newProposedTime, clock.CurrentTime + sixty_frame_time)
- : Math.Max(newProposedTime, clock.CurrentTime - sixty_frame_time);
- }
-
- if (!isAttached)
- {
- clock.CurrentTime = newProposedTime;
- }
- else
- {
- double? newTime = replayInputHandler.SetFrameFromTime(newProposedTime);
-
- if (newTime == null)
- {
- // we shouldn't execute for this time value. probably waiting on more replay data.
- validState = false;
-
- requireMoreUpdateLoops = true;
- clock.CurrentTime = newProposedTime;
- return;
- }
-
- clock.CurrentTime = newTime.Value;
- }
-
- requireMoreUpdateLoops = clock.CurrentTime != parentClock.CurrentTime;
- }
- finally
- {
- // The manual clock time has changed in the above code. The framed clock now needs to be updated
- // to ensure that the its time is valid for our children before input is processed
- Clock.ProcessFrame();
- }
- }
-
- #endregion
-
#region Setting application (disables etc.)
private Bindable mouseDisabled;
- [BackgroundDependencyLoader]
- private void load(OsuConfigManager config)
- {
- mouseDisabled = config.GetBindable(OsuSetting.MouseDisableButtons);
- }
-
protected override bool Handle(UIEvent e)
{
switch (e)
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingRulesetContainer.cs b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
similarity index 89%
rename from osu.Game/Rulesets/UI/Scrolling/ScrollingRulesetContainer.cs
rename to osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
index 7a60e0b021da..3b368652f224 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingRulesetContainer.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.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 osu.Framework.Allocation;
@@ -20,12 +21,11 @@
namespace osu.Game.Rulesets.UI.Scrolling
{
///
- /// A type of that supports a .
- /// s inside this will scroll within the playfield.
+ /// A type of that supports a .
+ /// s inside this will scroll within the playfield.
///
- public abstract class ScrollingRulesetContainer : RulesetContainer, IKeyBindingHandler
+ public abstract class DrawableScrollingRuleset : DrawableRuleset, IKeyBindingHandler
where TObject : HitObject
- where TPlayfield : ScrollingPlayfield
{
///
/// The default span of time visible by the length of the scrolling axes.
@@ -70,7 +70,7 @@ public abstract class ScrollingRulesetContainer : RulesetCo
///
/// Provides the default s that adjust the scrolling rate of s
- /// inside this .
+ /// inside this .
///
///
private readonly SortedList controlPoints = new SortedList(Comparer.Default);
@@ -80,7 +80,7 @@ public abstract class ScrollingRulesetContainer : RulesetCo
[Cached(Type = typeof(IScrollingInfo))]
private readonly LocalScrollingInfo scrollingInfo;
- protected ScrollingRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
+ protected DrawableScrollingRuleset(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
scrollingInfo = new LocalScrollingInfo();
@@ -167,6 +167,14 @@ public bool OnPressed(GlobalAction action)
return false;
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ if (!(Playfield is ScrollingPlayfield))
+ throw new ArgumentException($"{nameof(Playfield)} must be a {nameof(ScrollingPlayfield)} when using {nameof(DrawableScrollingRuleset)}.");
+ }
+
public bool OnReleased(GlobalAction action) => false;
private class LocalScrollingInfo : IScrollingInfo
diff --git a/osu.Game/Screens/BackgroundScreenStack.cs b/osu.Game/Screens/BackgroundScreenStack.cs
index b010a70e668a..5f82329496ed 100644
--- a/osu.Game/Screens/BackgroundScreenStack.cs
+++ b/osu.Game/Screens/BackgroundScreenStack.cs
@@ -11,6 +11,7 @@ namespace osu.Game.Screens
public class BackgroundScreenStack : ScreenStack
{
public BackgroundScreenStack()
+ : base(false)
{
Scale = new Vector2(1.06f);
RelativeSizeAxes = Axes.Both;
diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs
index 13d1ff39efa1..6df418753c0c 100644
--- a/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.cs
+++ b/osu.Game/Screens/Backgrounds/BackgroundScreenBeatmap.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.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -11,8 +12,10 @@
namespace osu.Game.Screens.Backgrounds
{
- public class BackgroundScreenBeatmap : BlurrableBackgroundScreen
+ public class BackgroundScreenBeatmap : BackgroundScreen
{
+ protected Background Background;
+
private WorkingBeatmap beatmap;
///
@@ -22,11 +25,34 @@ public class BackgroundScreenBeatmap : BlurrableBackgroundScreen
public readonly Bindable StoryboardReplacesBackground = new Bindable();
+ ///
+ /// The amount of blur to be applied in addition to user-specified blur.
+ ///
+ public readonly Bindable BlurAmount = new Bindable();
+
private readonly UserDimContainer fadeContainer;
protected virtual UserDimContainer CreateFadeContainer() => new UserDimContainer { RelativeSizeAxes = Axes.Both };
- public virtual WorkingBeatmap Beatmap
+ public BackgroundScreenBeatmap(WorkingBeatmap beatmap = null)
+ {
+ Beatmap = beatmap;
+ InternalChild = fadeContainer = CreateFadeContainer();
+ fadeContainer.EnableUserDim.BindTo(EnableUserDim);
+ fadeContainer.BlurAmount.BindTo(BlurAmount);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ var background = new BeatmapBackground(beatmap);
+ LoadComponent(background);
+ switchBackground(background);
+ }
+
+ private CancellationTokenSource cancellationSource;
+
+ public WorkingBeatmap Beatmap
{
get => beatmap;
set
@@ -38,54 +64,51 @@ public virtual WorkingBeatmap Beatmap
Schedule(() =>
{
- LoadComponentAsync(new BeatmapBackground(beatmap), b => Schedule(() =>
- {
- float newDepth = 0;
- if (Background != null)
- {
- newDepth = Background.Depth + 1;
- Background.FinishTransforms();
- Background.FadeOut(250);
- Background.Expire();
- }
-
- b.Depth = newDepth;
- fadeContainer.Add(Background = b);
- Background.BlurSigma = BlurTarget;
- StoryboardReplacesBackground.BindTo(fadeContainer.StoryboardReplacesBackground);
- }));
+ if ((Background as BeatmapBackground)?.Beatmap == beatmap)
+ return;
+
+ cancellationSource?.Cancel();
+ LoadComponentAsync(new BeatmapBackground(beatmap), switchBackground, (cancellationSource = new CancellationTokenSource()).Token);
});
}
}
- public BackgroundScreenBeatmap(WorkingBeatmap beatmap = null)
+ private void switchBackground(BeatmapBackground b)
{
- Beatmap = beatmap;
- InternalChild = fadeContainer = CreateFadeContainer();
- fadeContainer.EnableUserDim.BindTo(EnableUserDim);
+ float newDepth = 0;
+ if (Background != null)
+ {
+ newDepth = Background.Depth + 1;
+ Background.FinishTransforms();
+ Background.FadeOut(250);
+ Background.Expire();
+ }
+
+ b.Depth = newDepth;
+ fadeContainer.Background = Background = b;
+ StoryboardReplacesBackground.BindTo(fadeContainer.StoryboardReplacesBackground);
}
public override bool Equals(BackgroundScreen other)
{
- var otherBeatmapBackground = other as BackgroundScreenBeatmap;
- if (otherBeatmapBackground == null) return false;
+ if (!(other is BackgroundScreenBeatmap otherBeatmapBackground)) return false;
return base.Equals(other) && beatmap == otherBeatmapBackground.Beatmap;
}
protected class BeatmapBackground : Background
{
- private readonly WorkingBeatmap beatmap;
+ public readonly WorkingBeatmap Beatmap;
public BeatmapBackground(WorkingBeatmap beatmap)
{
- this.beatmap = beatmap;
+ Beatmap = beatmap;
}
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
- Sprite.Texture = beatmap?.Background ?? textures.Get(@"Backgrounds/bg1");
+ Sprite.Texture = Beatmap?.Background ?? textures.Get(@"Backgrounds/bg1");
}
}
}
diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
index 87a6b5d5915e..7092ac0c4a47 100644
--- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
+++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs
@@ -13,8 +13,10 @@
namespace osu.Game.Screens.Backgrounds
{
- public class BackgroundScreenDefault : BlurrableBackgroundScreen
+ public class BackgroundScreenDefault : BackgroundScreen
{
+ private Background background;
+
private int currentDisplay;
private const int background_count = 5;
@@ -34,15 +36,15 @@ private void load(IAPIProvider api, SkinManager skinManager)
currentDisplay = RNG.Next(0, background_count);
- Next();
+ display(createBackground());
}
private void display(Background newBackground)
{
- Background?.FadeOut(800, Easing.InOutSine);
- Background?.Expire();
+ background?.FadeOut(800, Easing.InOutSine);
+ background?.Expire();
- AddInternal(Background = newBackground);
+ AddInternal(background = newBackground);
currentDisplay++;
}
@@ -51,19 +53,21 @@ private void display(Background newBackground)
public void Next()
{
nextTask?.Cancel();
- nextTask = Scheduler.AddDelayed(() =>
- {
- Background background;
+ nextTask = Scheduler.AddDelayed(() => { LoadComponentAsync(createBackground(), display); }, 100);
+ }
+
+ private Background createBackground()
+ {
+ Background newBackground;
- if (user.Value?.IsSupporter ?? false)
- background = new SkinnedBackground(skin.Value, backgroundName);
- else
- background = new Background(backgroundName);
+ if (user.Value?.IsSupporter ?? false)
+ newBackground = new SkinnedBackground(skin.Value, backgroundName);
+ else
+ newBackground = new Background(backgroundName);
- background.Depth = currentDisplay;
+ newBackground.Depth = currentDisplay;
- LoadComponentAsync(background, display);
- }, 100);
+ return newBackground;
}
private class SkinnedBackground : Background
diff --git a/osu.Game/Screens/BlurrableBackgroundScreen.cs b/osu.Game/Screens/BlurrableBackgroundScreen.cs
deleted file mode 100644
index d19e699acb9d..000000000000
--- a/osu.Game/Screens/BlurrableBackgroundScreen.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Transforms;
-using osu.Game.Graphics.Backgrounds;
-using osuTK;
-
-namespace osu.Game.Screens
-{
- public abstract class BlurrableBackgroundScreen : BackgroundScreen
- {
- protected Background Background;
-
- protected Vector2 BlurTarget;
-
- public TransformSequence BlurTo(Vector2 sigma, double duration, Easing easing = Easing.None)
- {
- BlurTarget = sigma;
- return Background?.BlurTo(BlurTarget, duration, easing);
- }
- }
-}
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index f2d2381d20ef..0ba1e74aca53 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -55,7 +55,7 @@ private void load(OsuColour colours, GameHost host)
{
this.host = host;
- // TODO: should probably be done at a RulesetContainer level to share logic with Player.
+ // TODO: should probably be done at a DrawableRuleset level to share logic with Player.
var sourceClock = (IAdjustableClock)Beatmap.Value.Track ?? new StopwatchClock();
clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false };
clock.ChangeSource(sourceClock);
diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs
index 0db0f1a1a34e..d858cb076a8d 100644
--- a/osu.Game/Screens/Loader.cs
+++ b/osu.Game/Screens/Loader.cs
@@ -89,7 +89,7 @@ private void load(OsuGameBase game)
///
public class ShaderPrecompiler : Drawable
{
- private readonly List loadTargets = new List();
+ private readonly List loadTargets = new List();
public bool FinishedCompiling { get; private set; }
@@ -106,7 +106,7 @@ private void load(ShaderManager manager)
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE));
}
- protected virtual bool AllLoaded => loadTargets.All(s => s.Loaded);
+ protected virtual bool AllLoaded => loadTargets.All(s => s.IsLoaded);
protected override void Update()
{
diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs
index 1f6881866991..3df4ef9059f6 100644
--- a/osu.Game/Screens/Menu/ButtonSystem.cs
+++ b/osu.Game/Screens/Menu/ButtonSystem.cs
@@ -97,7 +97,7 @@ public ButtonSystem()
private OsuGame game { get; set; }
[Resolved]
- private APIAccess api { get; set; }
+ private IAPIProvider api { get; set; }
[Resolved(CanBeNull = true)]
private NotificationOverlay notifications { get; set; }
diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs
index 89f4f9209295..e6a90f76c0b4 100644
--- a/osu.Game/Screens/Menu/Disclaimer.cs
+++ b/osu.Game/Screens/Menu/Disclaimer.cs
@@ -2,11 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Screens;
using osu.Game.Graphics;
@@ -25,16 +26,17 @@ public class Disclaimer : OsuScreen
private SpriteIcon icon;
private Color4 iconColour;
private LinkFlowContainer textFlow;
+ private LinkFlowContainer supportFlow;
public override bool HideOverlaysOnEnter => true;
public override OverlayActivation InitialOverlayActivationMode => OverlayActivation.Disabled;
public override bool CursorVisible => false;
- private readonly List supporterDrawables = new List();
private Drawable heart;
private const float icon_y = -85;
+ private const float icon_size = 30;
private readonly Bindable currentUser = new Bindable();
@@ -44,7 +46,7 @@ public Disclaimer()
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours, APIAccess api)
+ private void load(OsuColour colours, IAPIProvider api)
{
InternalChildren = new Drawable[]
{
@@ -53,19 +55,39 @@ private void load(OsuColour colours, APIAccess api)
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.fa_warning,
- Size = new Vector2(30),
+ Size = new Vector2(icon_size),
Y = icon_y,
},
- textFlow = new LinkFlowContainer
+ new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding(50),
- TextAnchor = Anchor.TopCentre,
- Y = -110,
+ Direction = FillDirection.Vertical,
+ Y = icon_y + icon_size,
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
- Spacing = new Vector2(0, 2),
+ Children = new Drawable[]
+ {
+ textFlow = new LinkFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ TextAnchor = Anchor.TopCentre,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Spacing = new Vector2(0, 2),
+ },
+ supportFlow = new LinkFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ TextAnchor = Anchor.TopCentre,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Alpha = 0,
+ Spacing = new Vector2(0, 2),
+ },
+ }
}
};
@@ -88,28 +110,45 @@ private void load(OsuColour colours, APIAccess api)
textFlow.NewParagraph();
textFlow.NewParagraph();
- supporterDrawables.AddRange(textFlow.AddText("Consider becoming an ", format));
- supporterDrawables.AddRange(textFlow.AddLink("osu!supporter", "https://osu.ppy.sh/home/support", creationParameters: format));
- supporterDrawables.AddRange(textFlow.AddText(" to help support the game", format));
-
- supporterDrawables.Add(heart = textFlow.AddIcon(FontAwesome.fa_heart, t =>
- {
- t.Padding = new MarginPadding { Left = 5 };
- t.Font = t.Font.With(size: 12);
- t.Colour = colours.Pink;
- t.Origin = Anchor.Centre;
- }).First());
-
iconColour = colours.Yellow;
currentUser.BindTo(api.LocalUser);
currentUser.BindValueChanged(e =>
{
+ supportFlow.Children.ForEach(d => d.FadeOut().Expire());
+
if (e.NewValue.IsSupporter)
- supporterDrawables.ForEach(d => d.FadeOut(500, Easing.OutQuint).Expire());
+ {
+ supportFlow.AddText("Thank you for supporting osu!", format);
+ }
+ else
+ {
+ supportFlow.AddText("Consider becoming an ", format);
+ supportFlow.AddLink("osu!supporter", "https://osu.ppy.sh/home/support", creationParameters: format);
+ supportFlow.AddText(" to help support the game", format);
+ }
+
+ heart = supportFlow.AddIcon(FontAwesome.fa_heart, t =>
+ {
+ t.Padding = new MarginPadding { Left = 5 };
+ t.Font = t.Font.With(size: 12);
+ t.Origin = Anchor.Centre;
+ t.Colour = colours.Pink;
+ }).First();
+
+ if (IsLoaded)
+ animateHeart();
+
+ if (supportFlow.IsPresent)
+ supportFlow.FadeInFromZero(500);
}, true);
}
+ private void animateHeart()
+ {
+ heart.FlashColour(Color4.White, 750, Easing.OutQuint).Loop();
+ }
+
protected override void LoadComplete()
{
base.LoadComplete();
@@ -128,7 +167,9 @@ public override void OnEntering(IScreen last)
.MoveToY(icon_y, 160, Easing.InCirc)
.RotateTo(0, 160, Easing.InCirc);
- supporterDrawables.ForEach(d => d.FadeOut().Delay(2000).FadeIn(500));
+ supportFlow.FadeOut().Delay(2000).FadeIn(500);
+
+ animateHeart();
this
.FadeInFromZero(500)
@@ -136,8 +177,6 @@ public override void OnEntering(IScreen last)
.FadeOut(250)
.ScaleTo(0.9f, 250, Easing.InQuint)
.Finally(d => this.Push(intro));
-
- heart.FlashColour(Color4.White, 750, Easing.OutQuint).Loop();
}
}
}
diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs
index e930f924be0b..a41a12927bfa 100644
--- a/osu.Game/Screens/Menu/LogoVisualisation.cs
+++ b/osu.Game/Screens/Menu/LogoVisualisation.cs
@@ -63,7 +63,7 @@ public class LogoVisualisation : Drawable, IHasAccentColour
private readonly float[] frequencyAmplitudes = new float[256];
- private Shader shader;
+ private IShader shader;
private readonly Texture texture;
public LogoVisualisation()
@@ -131,8 +131,6 @@ protected override void Update()
protected override DrawNode CreateDrawNode() => new VisualisationDrawNode();
- private readonly VisualiserSharedData sharedData = new VisualiserSharedData();
-
protected override void ApplyDrawNode(DrawNode node)
{
base.ApplyDrawNode(node);
@@ -142,29 +140,23 @@ protected override void ApplyDrawNode(DrawNode node)
visNode.Shader = shader;
visNode.Texture = texture;
visNode.Size = DrawSize.X;
- visNode.Shared = sharedData;
visNode.Colour = AccentColour;
visNode.AudioData = frequencyAmplitudes;
}
- private class VisualiserSharedData
- {
- public readonly QuadBatch VertexBatch = new QuadBatch(100, 10);
- }
-
private class VisualisationDrawNode : DrawNode
{
- public Shader Shader;
+ public IShader Shader;
public Texture Texture;
- public VisualiserSharedData Shared;
-
//Asuming the logo is a circle, we don't need a second dimension.
public float Size;
public Color4 Colour;
public float[] AudioData;
+ private readonly QuadBatch vertexBatch = new QuadBatch(100, 10);
+
public override void Draw(Action vertexAction)
{
base.Draw(vertexAction);
@@ -209,7 +201,7 @@ public override void Draw(Action vertexAction)
rectangle,
colourInfo,
null,
- Shared.VertexBatch.AddAction,
+ vertexBatch.AddAction,
//barSize by itself will make it smooth more in the X axis than in the Y axis, this reverts that.
Vector2.Divide(inflation, barSize.Yx));
}
@@ -218,6 +210,13 @@ public override void Draw(Action vertexAction)
Shader.Unbind();
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ vertexBatch.Dispose();
+ }
}
}
}
diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs
index 234fb808c344..5403f7c70253 100644
--- a/osu.Game/Screens/Menu/MainMenu.cs
+++ b/osu.Game/Screens/Menu/MainMenu.cs
@@ -40,7 +40,9 @@ public class MainMenu : OsuScreen
[Resolved]
private GameHost host { get; set; }
- protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault();
+ private BackgroundScreenDefault background;
+
+ protected override BackgroundScreen CreateBackground() => background;
[BackgroundDependencyLoader(true)]
private void load(OsuGame game = null)
@@ -89,6 +91,7 @@ private void load(OsuGame game = null)
buttons.OnDirect = game.ToggleDirect;
}
+ LoadComponentAsync(background = new BackgroundScreenDefault());
preloadSongSelect();
}
diff --git a/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs b/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs
index 8ab23a620bc2..6080458aece4 100644
--- a/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs
+++ b/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs
@@ -15,7 +15,7 @@ public class ModeTypeInfo : MultiplayerComposite
private const float height = 30;
private const float transition_duration = 100;
- private Container rulesetContainer;
+ private Container drawableRuleset;
public ModeTypeInfo()
{
@@ -35,7 +35,7 @@ private void load()
LayoutDuration = 100,
Children = new[]
{
- rulesetContainer = new Container
+ drawableRuleset = new Container
{
AutoSizeAxes = Axes.Both,
},
@@ -55,11 +55,11 @@ private void updateBeatmap(PlaylistItem item)
{
if (item?.Beatmap != null)
{
- rulesetContainer.FadeIn(transition_duration);
- rulesetContainer.Child = new DifficultyIcon(item.Beatmap, item.Ruleset) { Size = new Vector2(height) };
+ drawableRuleset.FadeIn(transition_duration);
+ drawableRuleset.Child = new DifficultyIcon(item.Beatmap, item.Ruleset) { Size = new Vector2(height) };
}
else
- rulesetContainer.FadeOut(transition_duration);
+ drawableRuleset.FadeOut(transition_duration);
}
}
}
diff --git a/osu.Game/Screens/Multi/Components/MultiplayerBackgroundSprite.cs b/osu.Game/Screens/Multi/Components/MultiplayerBackgroundSprite.cs
index 512f79942de4..968fa6e72eb9 100644
--- a/osu.Game/Screens/Multi/Components/MultiplayerBackgroundSprite.cs
+++ b/osu.Game/Screens/Multi/Components/MultiplayerBackgroundSprite.cs
@@ -9,6 +9,13 @@ namespace osu.Game.Screens.Multi.Components
{
public class MultiplayerBackgroundSprite : MultiplayerComposite
{
+ private readonly BeatmapSetCoverType beatmapSetCoverType;
+
+ public MultiplayerBackgroundSprite(BeatmapSetCoverType beatmapSetCoverType = BeatmapSetCoverType.Cover)
+ {
+ this.beatmapSetCoverType = beatmapSetCoverType;
+ }
+
[BackgroundDependencyLoader]
private void load()
{
@@ -19,6 +26,6 @@ private void load()
CurrentItem.BindValueChanged(item => sprite.Beatmap.Value = item.NewValue?.Beatmap, true);
}
- protected virtual UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new UpdateableBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both };
+ protected virtual UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new UpdateableBeatmapBackgroundSprite(beatmapSetCoverType) { RelativeSizeAxes = Axes.Both };
}
}
diff --git a/osu.Game/Screens/Multi/Components/ParticipantCount.cs b/osu.Game/Screens/Multi/Components/ParticipantCountDisplay.cs
similarity index 100%
rename from osu.Game/Screens/Multi/Components/ParticipantCount.cs
rename to osu.Game/Screens/Multi/Components/ParticipantCountDisplay.cs
diff --git a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs
index e41238a7ff77..4bab68058f80 100644
--- a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs
+++ b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs
@@ -11,6 +11,7 @@
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@@ -137,7 +138,7 @@ private void load(OsuColour colours)
Width = cover_width,
Masking = true,
Margin = new MarginPadding { Left = side_strip_width },
- Child = new MultiplayerBackgroundSprite { RelativeSizeAxes = Axes.Both }
+ Child = new MultiplayerBackgroundSprite(BeatmapSetCoverType.List) { RelativeSizeAxes = Axes.Both }
},
new Container
{
diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs
index fd9c8d7b3538..5798fce4577a 100644
--- a/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs
+++ b/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs
@@ -246,7 +246,7 @@ private void load()
}
[Resolved]
- private APIAccess api { get; set; }
+ private IAPIProvider api { get; set; }
private GetRoomScoresRequest request;
diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs
index dd1e060125c2..7cbae611ea98 100644
--- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs
+++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs
@@ -107,6 +107,14 @@ public override void OnSuspending(IScreen next)
Filter.Search.HoldFocus = false;
}
+ public override void OnResuming(IScreen last)
+ {
+ base.OnResuming(last);
+
+ if (currentRoom.Value?.RoomID.Value == null)
+ currentRoom.Value = new Room();
+ }
+
private void joinRequested(Room room)
{
processingOverlay.Show();
diff --git a/osu.Game/Screens/Multi/Match/Components/Header.cs b/osu.Game/Screens/Multi/Match/Components/Header.cs
index 6a6a1f274cf7..e1592532a347 100644
--- a/osu.Game/Screens/Multi/Match/Components/Header.cs
+++ b/osu.Game/Screens/Multi/Match/Components/Header.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@@ -13,6 +14,7 @@
using osu.Game.Graphics;
using osu.Game.Online.Multiplayer;
using osu.Game.Overlays.SearchableList;
+using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Multi.Components;
using osu.Game.Screens.Play.HUD;
using osuTK;
@@ -108,7 +110,7 @@ private void load(OsuColour colours)
},
};
- CurrentItem.BindValueChanged(item => modDisplay.Current.Value = item.NewValue?.RequiredMods, true);
+ CurrentItem.BindValueChanged(item => modDisplay.Current.Value = item.NewValue?.RequiredMods ?? Enumerable.Empty(), true);
beatmapButton.Action = () => RequestBeatmapSelection?.Invoke();
}
diff --git a/osu.Game/Screens/Multi/Match/Components/MatchChatDisplay.cs b/osu.Game/Screens/Multi/Match/Components/MatchChatDisplay.cs
index bd8e4c73354e..f8b64a54efaa 100644
--- a/osu.Game/Screens/Multi/Match/Components/MatchChatDisplay.cs
+++ b/osu.Game/Screens/Multi/Match/Components/MatchChatDisplay.cs
@@ -28,13 +28,15 @@ protected override void LoadComplete()
{
base.LoadComplete();
- roomId.BindValueChanged(_ => updateChannel(), true);
+ channelId.BindValueChanged(_ => updateChannel(), true);
}
private void updateChannel()
{
- if (roomId.Value != null)
- Channel.Value = channelManager?.JoinChannel(new Channel { Id = channelId.Value, Type = ChannelType.Multiplayer, Name = $"#mp_{roomId.Value}" });
+ if (roomId.Value == null || channelId.Value == 0)
+ return;
+
+ Channel.Value = channelManager?.JoinChannel(new Channel { Id = channelId.Value, Type = ChannelType.Multiplayer, Name = $"#lazermp_{roomId.Value}" });
}
}
}
diff --git a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs
index b310e62d7c59..586a98611198 100644
--- a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs
+++ b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs
@@ -316,8 +316,12 @@ private void onError(string text)
private class SettingsTextBox : OsuTextBox
{
- protected override Color4 BackgroundUnfocused => Color4.Black;
- protected override Color4 BackgroundFocused => Color4.Black;
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ BackgroundUnfocused = Color4.Black;
+ BackgroundFocused = Color4.Black;
+ }
}
private class SettingsNumberTextBox : SettingsTextBox
@@ -327,8 +331,12 @@ private class SettingsNumberTextBox : SettingsTextBox
private class SettingsPasswordTextBox : OsuPasswordTextBox
{
- protected override Color4 BackgroundUnfocused => Color4.Black;
- protected override Color4 BackgroundFocused => Color4.Black;
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ BackgroundUnfocused = Color4.Black;
+ BackgroundFocused = Color4.Black;
+ }
}
private class SectionContainer : FillFlowContainer
diff --git a/osu.Game/Screens/Multi/Match/Components/Participants.cs b/osu.Game/Screens/Multi/Match/Components/Participants.cs
index 2d6099c65d92..09d25572ecab 100644
--- a/osu.Game/Screens/Multi/Match/Components/Participants.cs
+++ b/osu.Game/Screens/Multi/Match/Components/Participants.cs
@@ -52,12 +52,18 @@ private void load()
Participants.BindValueChanged(participants =>
{
- usersFlow.Children = participants.NewValue.Select(u => new UserPanel(u)
+ usersFlow.Children = participants.NewValue.Select(u =>
{
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Width = 300,
- OnLoadComplete = d => d.FadeInFromZero(60),
+ var panel = new UserPanel(u)
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Width = 300,
+ };
+
+ panel.OnLoadComplete += d => d.FadeInFromZero(60);
+
+ return panel;
}).ToList();
}, true);
}
diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs
index dd01ae416050..f38fc4e3ffbe 100644
--- a/osu.Game/Screens/Multi/Multiplayer.cs
+++ b/osu.Game/Screens/Multi/Multiplayer.cs
@@ -54,7 +54,7 @@ public class Multiplayer : OsuScreen, IOnlineComponent
private OsuGameBase game { get; set; }
[Resolved]
- private APIAccess api { get; set; }
+ private IAPIProvider api { get; set; }
[Resolved(CanBeNull = true)]
private OsuLogo logo { get; set; }
@@ -163,7 +163,7 @@ public void Start(Func player)
this.Push(new PlayerLoader(player));
}
- public void APIStateChanged(APIAccess api, APIState state)
+ public void APIStateChanged(IAPIProvider api, APIState state)
{
if (state != APIState.Online)
forcefullyExit();
diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs
index 6b88403b9e2a..d5b8f1f0c8c8 100644
--- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs
+++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs
@@ -32,7 +32,7 @@ public class TimeshiftPlayer : Player
private readonly PlaylistItem playlistItem;
[Resolved]
- private APIAccess api { get; set; }
+ private IAPIProvider api { get; set; }
[Resolved]
private IBindable ruleset { get; set; }
diff --git a/osu.Game/Screens/Multi/RoomManager.cs b/osu.Game/Screens/Multi/RoomManager.cs
index c15a8471a135..385cbe20e5ed 100644
--- a/osu.Game/Screens/Multi/RoomManager.cs
+++ b/osu.Game/Screens/Multi/RoomManager.cs
@@ -30,7 +30,7 @@ public class RoomManager : PollingComponent, IRoomManager
private Bindable currentFilter { get; set; }
[Resolved]
- private APIAccess api { get; set; }
+ private IAPIProvider api { get; set; }
[Resolved]
private RulesetStore rulesets { get; set; }
diff --git a/osu.Game/Screens/Play/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs
index 024ce01dc6cf..d39078709001 100644
--- a/osu.Game/Screens/Play/BreakOverlay.cs
+++ b/osu.Game/Screens/Play/BreakOverlay.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -40,13 +41,7 @@ public List Breaks
private readonly BreakInfo info;
private readonly BreakArrows breakArrows;
- public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor)
- : this(letterboxing)
- {
- bindProcessor(scoreProcessor);
- }
-
- public BreakOverlay(bool letterboxing)
+ public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor = null)
{
RelativeSizeAxes = Axes.Both;
Child = fadeContainer = new Container
@@ -98,6 +93,14 @@ public BreakOverlay(bool letterboxing)
}
}
};
+
+ if (scoreProcessor != null) bindProcessor(scoreProcessor);
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(GameplayClock clock)
+ {
+ if (clock != null) Clock = clock;
}
protected override void LoadComplete()
diff --git a/osu.Game/Screens/Play/GameplayClock.cs b/osu.Game/Screens/Play/GameplayClock.cs
new file mode 100644
index 000000000000..0400bfbc273b
--- /dev/null
+++ b/osu.Game/Screens/Play/GameplayClock.cs
@@ -0,0 +1,43 @@
+// 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.Timing;
+
+namespace osu.Game.Screens.Play
+{
+ ///
+ /// A clock which is used for gameplay elements that need to follow audio time 1:1.
+ /// Exposed via DI by .
+ ///
+ /// The main purpose of this clock is to stop components using it from accidentally processing the main
+ /// , as this should only be done once to ensure accuracy.
+ ///
+ ///
+ public class GameplayClock : IFrameBasedClock
+ {
+ private readonly IFrameBasedClock underlyingClock;
+
+ public GameplayClock(IFrameBasedClock underlyingClock)
+ {
+ this.underlyingClock = underlyingClock;
+ }
+
+ public double CurrentTime => underlyingClock.CurrentTime;
+
+ public double Rate => underlyingClock.Rate;
+
+ public bool IsRunning => underlyingClock.IsRunning;
+
+ public void ProcessFrame()
+ {
+ // we do not want to process the underlying clock.
+ // this is handled by PauseContainer.
+ }
+
+ public double ElapsedFrameTime => underlyingClock.ElapsedFrameTime;
+
+ public double FramesPerSecond => underlyingClock.FramesPerSecond;
+
+ public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo;
+ }
+}
diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs
new file mode 100644
index 000000000000..deac5e02bf9b
--- /dev/null
+++ b/osu.Game/Screens/Play/GameplayClockContainer.cs
@@ -0,0 +1,161 @@
+// 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.Linq;
+using System.Threading.Tasks;
+using osu.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Mods;
+
+namespace osu.Game.Screens.Play
+{
+ ///
+ /// Encapsulates gameplay timing logic and provides a for children.
+ ///
+ public class GameplayClockContainer : Container
+ {
+ private readonly WorkingBeatmap beatmap;
+
+ ///
+ /// The original source (usually a 's track).
+ ///
+ private readonly IAdjustableClock sourceClock;
+
+ public readonly BindableBool IsPaused = new BindableBool();
+
+ ///
+ /// The decoupled clock used for gameplay. Should be used for seeks and clock control.
+ ///
+ private readonly DecoupleableInterpolatingFramedClock adjustableClock;
+
+ public readonly Bindable UserPlaybackRate = new BindableDouble(1)
+ {
+ Default = 1,
+ MinValue = 0.5,
+ MaxValue = 2,
+ Precision = 0.1,
+ };
+
+ ///
+ /// The final clock which is exposed to underlying components.
+ ///
+ [Cached]
+ public readonly GameplayClock GameplayClock;
+
+ private Bindable userAudioOffset;
+
+ private readonly FramedOffsetClock offsetClock;
+
+ public GameplayClockContainer(WorkingBeatmap beatmap, bool allowLeadIn, double gameplayStartTime)
+ {
+ this.beatmap = beatmap;
+
+ RelativeSizeAxes = Axes.Both;
+
+ sourceClock = (IAdjustableClock)beatmap.Track ?? new StopwatchClock();
+
+ adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
+
+ adjustableClock.Seek(allowLeadIn
+ ? Math.Min(0, gameplayStartTime - beatmap.BeatmapInfo.AudioLeadIn)
+ : gameplayStartTime);
+
+ adjustableClock.ProcessFrame();
+
+ // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited.
+ // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck.
+ var platformOffsetClock = new FramedOffsetClock(adjustableClock) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 22 : 0 };
+
+ // the final usable gameplay clock with user-set offsets applied.
+ offsetClock = new FramedOffsetClock(platformOffsetClock);
+
+ // the clock to be exposed via DI to children.
+ GameplayClock = new GameplayClock(offsetClock);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ userAudioOffset = config.GetBindable(OsuSetting.AudioOffset);
+ userAudioOffset.BindValueChanged(offset => offsetClock.Offset = offset.NewValue, true);
+
+ UserPlaybackRate.ValueChanged += _ => updateRate();
+ }
+
+ public void Restart()
+ {
+ Task.Run(() =>
+ {
+ sourceClock.Reset();
+
+ Schedule(() =>
+ {
+ adjustableClock.ChangeSource(sourceClock);
+ updateRate();
+
+ this.Delay(750).Schedule(() =>
+ {
+ if (!IsPaused.Value)
+ {
+ adjustableClock.Start();
+ }
+ });
+ });
+ });
+ }
+
+ public void Start()
+ {
+ // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time
+ // This accounts for the audio clock source potentially taking time to enter a completely stopped state
+ adjustableClock.Seek(adjustableClock.CurrentTime);
+ adjustableClock.Start();
+ IsPaused.Value = false;
+ }
+
+ public void Seek(double time) => adjustableClock.Seek(time);
+
+ public void Stop()
+ {
+ adjustableClock.Stop();
+ IsPaused.Value = true;
+ }
+
+ public void ResetLocalAdjustments()
+ {
+ // In the case of replays, we may have changed the playback rate.
+ UserPlaybackRate.Value = 1;
+ }
+
+ protected override void Update()
+ {
+ if (!IsPaused.Value)
+ offsetClock.ProcessFrame();
+
+ base.Update();
+ }
+
+ private void updateRate()
+ {
+ if (sourceClock == null) return;
+
+ sourceClock.ResetSpeedAdjustments();
+
+ if (sourceClock is IHasTempoAdjust tempo)
+ tempo.TempoAdjust = UserPlaybackRate.Value;
+ else
+ sourceClock.Rate = UserPlaybackRate.Value;
+
+ foreach (var mod in beatmap.Mods.Value.OfType())
+ mod.ApplyToClock(sourceClock);
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/GameplayMenuOverlay.cs b/osu.Game/Screens/Play/GameplayMenuOverlay.cs
index 5d210446c378..2fac8de79989 100644
--- a/osu.Game/Screens/Play/GameplayMenuOverlay.cs
+++ b/osu.Game/Screens/Play/GameplayMenuOverlay.cs
@@ -38,9 +38,15 @@ public abstract class GameplayMenuOverlay : OverlayContainer, IKeyBindingHandler
///
/// Action that is invoked when is triggered.
///
- protected virtual Action BackAction => () => InternalButtons.Children.Last().Click();
+ protected virtual Action BackAction => () => InternalButtons.Children.LastOrDefault()?.Click();
+
+ ///
+ /// Action that is invoked when is triggered.
+ ///
+ protected virtual Action SelectAction => () => InternalButtons.Children.FirstOrDefault(f => f.Selected.Value)?.Click();
public abstract string Header { get; }
+
public abstract string Description { get; }
protected internal FillFlowContainer InternalButtons;
@@ -229,16 +235,30 @@ protected override bool OnKeyDown(KeyDownEvent e)
public bool OnPressed(GlobalAction action)
{
- if (action == GlobalAction.Back)
+ switch (action)
{
- BackAction.Invoke();
- return true;
+ case GlobalAction.Back:
+ BackAction.Invoke();
+ return true;
+ case GlobalAction.Select:
+ SelectAction.Invoke();
+ return true;
}
return false;
}
- public bool OnReleased(GlobalAction action) => action == GlobalAction.Back;
+ public bool OnReleased(GlobalAction action)
+ {
+ switch (action)
+ {
+ case GlobalAction.Back:
+ case GlobalAction.Select:
+ return true;
+ }
+
+ return false;
+ }
private void buttonSelectionChanged(DialogButton button, bool isSelected)
{
@@ -288,15 +308,6 @@ protected override bool OnMouseMove(MouseMoveEvent e)
Selected.Value = true;
return base.OnMouseMove(e);
}
-
- protected override bool OnKeyDown(KeyDownEvent e)
- {
- if (e.Repeat || e.Key != Key.Enter || !Selected.Value)
- return false;
-
- Click();
- return true;
- }
}
}
}
diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs
index ca4cce892903..50bc34726aae 100644
--- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs
+++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs
@@ -92,30 +92,6 @@ private class Button : HoldToConfirmContainer, IKeyBindingHandler
public Action HoverGained;
public Action HoverLost;
- public bool OnPressed(GlobalAction action)
- {
- switch (action)
- {
- case GlobalAction.Back:
- BeginConfirm();
- return true;
- }
-
- return false;
- }
-
- public bool OnReleased(GlobalAction action)
- {
- switch (action)
- {
- case GlobalAction.Back:
- AbortConfirm();
- return true;
- }
-
- return false;
- }
-
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
@@ -178,7 +154,7 @@ protected override void Confirm()
// avoid starting a new confirm call until we finish animating.
pendingAnimation = true;
- Progress.Value = 0;
+ AbortConfirm();
overlayCircle.ScaleTo(0, 100)
.Then().FadeOut().ScaleTo(1).FadeIn(500)
@@ -207,6 +183,31 @@ protected override void OnHoverLost(HoverLostEvent e)
base.OnHoverLost(e);
}
+ public bool OnPressed(GlobalAction action)
+ {
+ switch (action)
+ {
+ case GlobalAction.Back:
+ if (!pendingAnimation)
+ BeginConfirm();
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool OnReleased(GlobalAction action)
+ {
+ switch (action)
+ {
+ case GlobalAction.Back:
+ AbortConfirm();
+ return true;
+ }
+
+ return false;
+ }
+
protected override bool OnMouseDown(MouseDownEvent e)
{
if (!pendingAnimation && e.CurrentState.Mouse.Buttons.Count() == 1)
diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs
index d3dba8828166..e99f6d836e94 100644
--- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs
+++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs
@@ -19,7 +19,9 @@ public class PlayerSettingsOverlay : VisibilityContainer
public readonly PlaybackSettings PlaybackSettings;
public readonly VisualSettings VisualSettings;
+
//public readonly CollectionSettings CollectionSettings;
+
//public readonly DiscussionSettings DiscussionSettings;
public PlayerSettingsOverlay()
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index 130d2ecc9589..285e6eab2361 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -1,12 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
-using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
@@ -40,7 +40,9 @@ public class HUDOverlay : Container
private static bool hasShownNotificationOnce;
- public HUDOverlay(ScoreProcessor scoreProcessor, RulesetContainer rulesetContainer, WorkingBeatmap working, IClock offsetClock, IAdjustableClock adjustableClock)
+ public Action RequestSeek;
+
+ public HUDOverlay(ScoreProcessor scoreProcessor, DrawableRuleset drawableRuleset, WorkingBeatmap working)
{
RelativeSizeAxes = Axes.Both;
@@ -81,23 +83,20 @@ public HUDOverlay(ScoreProcessor scoreProcessor, RulesetContainer rulesetContain
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
- KeyCounter = CreateKeyCounter(adjustableClock as IFrameBasedClock),
+ KeyCounter = CreateKeyCounter(),
HoldToQuit = CreateHoldForMenuButton(),
}
}
};
BindProcessor(scoreProcessor);
- BindRulesetContainer(rulesetContainer);
+ BindDrawableRuleset(drawableRuleset);
- Progress.Objects = rulesetContainer.Objects;
- Progress.AudioClock = offsetClock;
- Progress.AllowSeeking = rulesetContainer.HasReplayLoaded.Value;
- Progress.OnSeek = pos => adjustableClock.Seek(pos);
+ Progress.Objects = drawableRuleset.Objects;
+ Progress.AllowSeeking = drawableRuleset.HasReplayLoaded.Value;
+ Progress.RequestSeek = time => RequestSeek(time);
ModDisplay.Current.BindTo(working.Mods);
-
- PlayerSettingsOverlay.PlaybackSettings.AdjustableClock = adjustableClock;
}
[BackgroundDependencyLoader(true)]
@@ -144,13 +143,13 @@ private void replayLoadedValueChanged(ValueChangedEvent e)
}
}
- protected virtual void BindRulesetContainer(RulesetContainer rulesetContainer)
+ protected virtual void BindDrawableRuleset(DrawableRuleset drawableRuleset)
{
- (rulesetContainer.KeyBindingInputManager as ICanAttachKeyCounter)?.Attach(KeyCounter);
+ (drawableRuleset as ICanAttachKeyCounter)?.Attach(KeyCounter);
- replayLoaded.BindTo(rulesetContainer.HasReplayLoaded);
+ replayLoaded.BindTo(drawableRuleset.HasReplayLoaded);
- Progress.BindRulestContainer(rulesetContainer);
+ Progress.BindDrawableRuleset(drawableRuleset);
}
protected override bool OnKeyDown(KeyDownEvent e)
@@ -202,13 +201,12 @@ protected override bool OnKeyDown(KeyDownEvent e)
Margin = new MarginPadding { Top = 20 }
};
- protected virtual KeyCounterCollection CreateKeyCounter(IFrameBasedClock offsetClock) => new KeyCounterCollection
+ protected virtual KeyCounterCollection CreateKeyCounter() => new KeyCounterCollection
{
FadeTime = 50,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding(10),
- AudioClock = offsetClock
};
protected virtual SongProgress CreateProgress() => new SongProgress
diff --git a/osu.Game/Screens/Play/KeyCounter.cs b/osu.Game/Screens/Play/KeyCounter.cs
index 0e1f938137d8..0626c403346f 100644
--- a/osu.Game/Screens/Play/KeyCounter.cs
+++ b/osu.Game/Screens/Play/KeyCounter.cs
@@ -71,9 +71,12 @@ protected KeyCounter(string name)
Name = name;
}
- [BackgroundDependencyLoader]
- private void load(TextureStore textures)
+ [BackgroundDependencyLoader(true)]
+ private void load(TextureStore textures, GameplayClock clock)
{
+ if (clock != null)
+ Clock = clock;
+
Children = new Drawable[]
{
buttonSprite = new Sprite
diff --git a/osu.Game/Screens/Play/KeyCounterCollection.cs b/osu.Game/Screens/Play/KeyCounterCollection.cs
index 0259258636cc..1b437377318b 100644
--- a/osu.Game/Screens/Play/KeyCounterCollection.cs
+++ b/osu.Game/Screens/Play/KeyCounterCollection.cs
@@ -8,7 +8,6 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
-using osu.Framework.Timing;
using osu.Game.Configuration;
using osuTK;
using osuTK.Graphics;
@@ -37,9 +36,6 @@ public override void Add(KeyCounter key)
key.FadeTime = FadeTime;
key.KeyDownTextColor = KeyDownTextColor;
key.KeyUpTextColor = KeyUpTextColor;
- // Use the same clock object as SongProgress for saving KeyCounter state
- if (AudioClock != null)
- key.Clock = AudioClock;
}
public void ResetCount()
@@ -125,8 +121,6 @@ public Color4 KeyUpTextColor
public override bool HandleNonPositionalInput => receptor == null;
public override bool HandlePositionalInput => receptor == null;
- public IFrameBasedClock AudioClock { get; set; }
-
private Receptor receptor;
public Receptor GetReceptor()
diff --git a/osu.Game/Screens/Play/PauseContainer.cs b/osu.Game/Screens/Play/PauseContainer.cs
deleted file mode 100644
index 8961d9176354..000000000000
--- a/osu.Game/Screens/Play/PauseContainer.cs
+++ /dev/null
@@ -1,149 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Linq;
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Timing;
-using osu.Game.Graphics;
-using osuTK.Graphics;
-
-namespace osu.Game.Screens.Play
-{
- ///
- /// A container which handles pausing children, displaying a pause overlay with choices etc.
- /// This alleviates a lot of the intricate pause logic from being in
- ///
- public class PauseContainer : Container
- {
- public readonly BindableBool IsPaused = new BindableBool();
-
- public Func CheckCanPause;
-
- private const double pause_cooldown = 1000;
- private double lastPauseActionTime;
-
- private readonly PauseOverlay pauseOverlay;
-
- private readonly Container content;
-
- protected override Container Content => content;
-
- public int Retries
- {
- set => pauseOverlay.Retries = value;
- }
-
- public bool CanPause => (CheckCanPause?.Invoke() ?? true) && Time.Current >= lastPauseActionTime + pause_cooldown;
- public bool IsResuming { get; private set; }
-
- public Action OnRetry;
- public Action OnQuit;
-
- private readonly FramedClock framedClock;
- private readonly DecoupleableInterpolatingFramedClock decoupledClock;
-
- ///
- /// Creates a new .
- ///
- /// The gameplay clock. This is the clock that will process frames.
- /// The seekable clock. This is the clock that will be paused and resumed.
- public PauseContainer(FramedClock framedClock, DecoupleableInterpolatingFramedClock decoupledClock)
- {
- this.framedClock = framedClock;
- this.decoupledClock = decoupledClock;
-
- RelativeSizeAxes = Axes.Both;
-
- AddInternal(content = new Container
- {
- Clock = this.framedClock,
- ProcessCustomClock = false,
- RelativeSizeAxes = Axes.Both
- });
-
- AddInternal(pauseOverlay = new PauseOverlay
- {
- OnResume = () =>
- {
- IsResuming = true;
- this.Delay(400).Schedule(Resume);
- },
- OnRetry = () => OnRetry(),
- OnQuit = () => OnQuit(),
- });
- }
-
- public void Pause(bool force = false) => Schedule(() => // Scheduled to ensure a stable position in execution order, no matter how it was called.
- {
- if (!CanPause && !force) return;
-
- if (IsPaused.Value) return;
-
- // stop the seekable clock (stops the audio eventually)
- decoupledClock.Stop();
- IsPaused.Value = true;
-
- pauseOverlay.Show();
-
- lastPauseActionTime = Time.Current;
- });
-
- public void Resume()
- {
- if (!IsPaused.Value) return;
-
- IsPaused.Value = false;
- IsResuming = false;
- lastPauseActionTime = Time.Current;
-
- // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time
- // This accounts for the audio clock source potentially taking time to enter a completely stopped state
- decoupledClock.Seek(decoupledClock.CurrentTime);
- decoupledClock.Start();
-
- pauseOverlay.Hide();
- }
-
- private OsuGameBase game;
-
- [BackgroundDependencyLoader]
- private void load(OsuGameBase game)
- {
- this.game = game;
- }
-
- protected override void Update()
- {
- // eagerly pause when we lose window focus (if we are locally playing).
- if (!game.IsActive.Value && CanPause)
- Pause();
-
- if (!IsPaused.Value)
- framedClock.ProcessFrame();
-
- base.Update();
- }
-
- public class PauseOverlay : GameplayMenuOverlay
- {
- public Action OnResume;
-
- public override string Header => "paused";
- public override string Description => "you're not going to do what i think you're going to do, are ya?";
-
- protected override Action BackAction => () => InternalButtons.Children.First().Click();
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- AddButton("Continue", colours.Green, () => OnResume?.Invoke());
- AddButton("Retry", colours.YellowDark, () => OnRetry?.Invoke());
- AddButton("Quit", new Color4(170, 27, 39, 255), () => OnQuit?.Invoke());
- }
- }
- }
-}
diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs
new file mode 100644
index 000000000000..6cc6027a037f
--- /dev/null
+++ b/osu.Game/Screens/Play/PauseOverlay.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 System;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Game.Graphics;
+using osuTK.Graphics;
+
+namespace osu.Game.Screens.Play
+{
+ public class PauseOverlay : GameplayMenuOverlay
+ {
+ public Action OnResume;
+
+ public override string Header => "paused";
+ public override string Description => "you're not going to do what i think you're going to do, are ya?";
+
+ protected override Action BackAction => () => InternalButtons.Children.First().Click();
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ AddButton("Continue", colours.Green, () => OnResume?.Invoke());
+ AddButton("Retry", colours.YellowDark, () => OnRetry?.Invoke());
+ AddButton("Quit", new Color4(170, 27, 39, 255), () => OnQuit?.Invoke());
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index ee59df0e983a..7b1cdd21a6b5 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -3,24 +3,19 @@
using System;
using System.Linq;
-using System.Threading.Tasks;
-using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Cursor;
using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Framework.Threading;
-using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
-using osu.Game.Graphics.Cursor;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Rulesets;
@@ -34,7 +29,7 @@
namespace osu.Game.Screens.Play
{
- public class Player : ScreenWithBeatmapBackground, IProvideCursor
+ public class Player : ScreenWithBeatmapBackground
{
protected override bool AllowBackButton => false; // handled by HoldForMenuButton
@@ -53,174 +48,91 @@ public class Player : ScreenWithBeatmapBackground, IProvideCursor
public bool AllowResults { get; set; } = true;
private Bindable mouseWheelDisabled;
- private Bindable userAudioOffset;
private readonly Bindable storyboardReplacesBackground = new Bindable();
public int RestartCount;
- public CursorContainer Cursor => RulesetContainer.Cursor;
- public bool ProvidingUserCursor => RulesetContainer?.Cursor != null && !RulesetContainer.HasReplayLoaded.Value;
-
- private IAdjustableClock sourceClock;
-
- ///
- /// The decoupled clock used for gameplay. Should be used for seeks and clock control.
- ///
- private DecoupleableInterpolatingFramedClock adjustableClock;
-
[Resolved]
private ScoreManager scoreManager { get; set; }
- protected PauseContainer PauseContainer { get; private set; }
-
private RulesetInfo ruleset;
- private APIAccess api;
+ private IAPIProvider api;
private SampleChannel sampleRestart;
protected ScoreProcessor ScoreProcessor { get; private set; }
- protected RulesetContainer RulesetContainer { get; private set; }
+ protected DrawableRuleset DrawableRuleset { get; private set; }
protected HUDOverlay HUDOverlay { get; private set; }
- private FailOverlay failOverlay;
- private DrawableStoryboard storyboard;
- protected UserDimContainer StoryboardContainer { get; private set; }
+ public bool LoadedBeatmapSuccessfully => DrawableRuleset?.Objects.Any() == true;
- protected virtual UserDimContainer CreateStoryboardContainer() => new UserDimContainer(true)
- {
- RelativeSizeAxes = Axes.Both,
- Alpha = 1,
- EnableUserDim = { Value = true }
- };
-
- public bool LoadedBeatmapSuccessfully => RulesetContainer?.Objects.Any() == true;
+ protected GameplayClockContainer GameplayClockContainer { get; private set; }
[BackgroundDependencyLoader]
- private void load(AudioManager audio, APIAccess api, OsuConfigManager config)
+ private void load(AudioManager audio, IAPIProvider api, OsuConfigManager config)
{
this.api = api;
- WorkingBeatmap working = Beatmap.Value;
- if (working is DummyWorkingBeatmap)
+ WorkingBeatmap working = loadBeatmap();
+
+ if (working == null)
return;
sampleRestart = audio.Sample.Get(@"Gameplay/restart");
mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel);
- userAudioOffset = config.GetBindable(OsuSetting.AudioOffset);
-
- IBeatmap beatmap;
+ showStoryboard = config.GetBindable(OsuSetting.ShowStoryboard);
- try
- {
- beatmap = working.Beatmap;
-
- if (beatmap == null)
- throw new InvalidOperationException("Beatmap was not loaded");
+ ScoreProcessor = DrawableRuleset.CreateScoreProcessor();
+ if (!ScoreProcessor.Mode.Disabled)
+ config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode);
- ruleset = Ruleset.Value ?? beatmap.BeatmapInfo.Ruleset;
- var rulesetInstance = ruleset.CreateInstance();
+ InternalChild = GameplayClockContainer = new GameplayClockContainer(working, AllowLeadIn, DrawableRuleset.GameplayStartTime);
- try
+ GameplayClockContainer.Children = new[]
+ {
+ StoryboardContainer = CreateStoryboardContainer(),
+ new ScalingContainer(ScalingMode.Gameplay)
{
- RulesetContainer = rulesetInstance.CreateRulesetContainerWith(working);
- }
- catch (BeatmapInvalidForRulesetException)
+ Child = new LocalSkinOverrideContainer(working.Skin)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = DrawableRuleset
+ }
+ },
+ new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor)
{
- // we may fail to create a RulesetContainer if the beatmap cannot be loaded with the user's preferred ruleset
- // let's try again forcing the beatmap's ruleset.
- ruleset = beatmap.BeatmapInfo.Ruleset;
- rulesetInstance = ruleset.CreateInstance();
- RulesetContainer = rulesetInstance.CreateRulesetContainerWith(Beatmap.Value);
- }
-
- if (!RulesetContainer.Objects.Any())
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Breaks = working.Beatmap.Breaks
+ },
+ // display the cursor above some HUD elements.
+ DrawableRuleset.Cursor?.CreateProxy() ?? new Container(),
+ HUDOverlay = new HUDOverlay(ScoreProcessor, DrawableRuleset, working)
{
- Logger.Log("Beatmap contains no hit objects!", level: LogLevel.Error);
- return;
- }
- }
- catch (Exception e)
- {
- Logger.Error(e, "Could not load beatmap sucessfully!");
- //couldn't load, hard abort!
- return;
- }
-
- sourceClock = (IAdjustableClock)working.Track ?? new StopwatchClock();
- adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
-
- adjustableClock.Seek(AllowLeadIn
- ? Math.Min(0, RulesetContainer.GameplayStartTime - beatmap.BeatmapInfo.AudioLeadIn)
- : RulesetContainer.GameplayStartTime);
-
- adjustableClock.ProcessFrame();
-
- // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited.
- // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck.
- var platformOffsetClock = new FramedOffsetClock(adjustableClock) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 22 : 0 };
-
- // the final usable gameplay clock with user-set offsets applied.
- var offsetClock = new FramedOffsetClock(platformOffsetClock);
-
- userAudioOffset.ValueChanged += offset => offsetClock.Offset = offset.NewValue;
- userAudioOffset.TriggerChange();
-
- ScoreProcessor = RulesetContainer.CreateScoreProcessor();
- if (!ScoreProcessor.Mode.Disabled)
- config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode);
-
- InternalChildren = new Drawable[]
- {
- PauseContainer = new PauseContainer(offsetClock, adjustableClock)
+ HoldToQuit = { Action = performUserRequestedExit },
+ PlayerSettingsOverlay = { PlaybackSettings = { UserPlaybackRate = { BindTarget = GameplayClockContainer.UserPlaybackRate } } },
+ KeyCounter = { Visible = { BindTarget = DrawableRuleset.HasReplayLoaded } },
+ RequestSeek = GameplayClockContainer.Seek,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ },
+ new SkipOverlay(DrawableRuleset.GameplayStartTime)
+ {
+ RequestSeek = GameplayClockContainer.Seek
+ },
+ FailOverlay = new FailOverlay
{
- Retries = RestartCount,
OnRetry = Restart,
OnQuit = performUserRequestedExit,
- CheckCanPause = () => AllowPause && ValidForResume && !HasFailed && !RulesetContainer.HasReplayLoaded.Value,
- Children = new Container[]
- {
- StoryboardContainer = CreateStoryboardContainer(),
- new ScalingContainer(ScalingMode.Gameplay)
- {
- Child = new LocalSkinOverrideContainer(working.Skin)
- {
- RelativeSizeAxes = Axes.Both,
- Child = RulesetContainer
- }
- },
- new BreakOverlay(beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor)
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- ProcessCustomClock = false,
- Breaks = beatmap.Breaks
- },
- new ScalingContainer(ScalingMode.Gameplay)
- {
- Child = RulesetContainer.Cursor?.CreateProxy() ?? new Container(),
- },
- HUDOverlay = new HUDOverlay(ScoreProcessor, RulesetContainer, working, offsetClock, adjustableClock)
- {
- Clock = Clock, // hud overlay doesn't want to use the audio clock directly
- ProcessCustomClock = false,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre
- },
- new SkipOverlay(RulesetContainer.GameplayStartTime)
- {
- Clock = Clock, // skip button doesn't want to use the audio clock directly
- ProcessCustomClock = false,
- AdjustableClock = adjustableClock,
- FramedClock = offsetClock,
- },
- }
},
- failOverlay = new FailOverlay
+ PauseOverlay = new PauseOverlay
{
+ OnResume = Resume,
+ Retries = RestartCount,
OnRetry = Restart,
OnQuit = performUserRequestedExit,
},
@@ -236,13 +148,11 @@ private void load(AudioManager audio, APIAccess api, OsuConfigManager config)
}
};
- HUDOverlay.HoldToQuit.Action = performUserRequestedExit;
- HUDOverlay.KeyCounter.Visible.BindTo(RulesetContainer.HasReplayLoaded);
-
- RulesetContainer.IsPaused.BindTo(PauseContainer.IsPaused);
+ // bind clock into components that require it
+ DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused);
- if (ShowStoryboard.Value)
- initializeStoryboard(false);
+ // load storyboard as part of player's load if we can
+ initializeStoryboard(false);
// Bind ScoreProcessor to ourselves
ScoreProcessor.AllJudged += onCompletion;
@@ -252,13 +162,49 @@ private void load(AudioManager audio, APIAccess api, OsuConfigManager config)
mod.ApplyToScoreProcessor(ScoreProcessor);
}
- private void applyRateFromMods()
+ private WorkingBeatmap loadBeatmap()
{
- if (sourceClock == null) return;
+ WorkingBeatmap working = Beatmap.Value;
+ if (working is DummyWorkingBeatmap)
+ return null;
+
+ try
+ {
+ var beatmap = working.Beatmap;
+
+ if (beatmap == null)
+ throw new InvalidOperationException("Beatmap was not loaded");
+
+ ruleset = Ruleset.Value ?? beatmap.BeatmapInfo.Ruleset;
+ var rulesetInstance = ruleset.CreateInstance();
+
+ try
+ {
+ DrawableRuleset = rulesetInstance.CreateDrawableRulesetWith(working);
+ }
+ catch (BeatmapInvalidForRulesetException)
+ {
+ // we may fail to create a DrawableRuleset if the beatmap cannot be loaded with the user's preferred ruleset
+ // let's try again forcing the beatmap's ruleset.
+ ruleset = beatmap.BeatmapInfo.Ruleset;
+ rulesetInstance = ruleset.CreateInstance();
+ DrawableRuleset = rulesetInstance.CreateDrawableRulesetWith(Beatmap.Value);
+ }
+
+ if (!DrawableRuleset.Objects.Any())
+ {
+ Logger.Log("Beatmap contains no hit objects!", level: LogLevel.Error);
+ return null;
+ }
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, "Could not load beatmap sucessfully!");
+ //couldn't load, hard abort!
+ return null;
+ }
- sourceClock.Rate = 1;
- foreach (var mod in Beatmap.Value.Mods.Value.OfType())
- mod.ApplyToClock(sourceClock);
+ return working;
}
private void performUserRequestedExit()
@@ -297,7 +243,7 @@ private void onCompletion()
if (!this.IsCurrentScreen()) return;
var score = CreateScore();
- if (RulesetContainer.ReplayScore == null)
+ if (DrawableRuleset.ReplayScore == null)
scoreManager.Import(score);
this.Push(CreateResults(score));
@@ -309,7 +255,7 @@ private void onCompletion()
protected virtual ScoreInfo CreateScore()
{
- var score = RulesetContainer.ReplayScore?.ScoreInfo ?? new ScoreInfo
+ var score = DrawableRuleset.ReplayScore?.ScoreInfo ?? new ScoreInfo
{
Beatmap = Beatmap.Value.BeatmapInfo,
Ruleset = ruleset,
@@ -322,19 +268,148 @@ protected virtual ScoreInfo CreateScore()
return score;
}
+ protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value;
+
+ protected virtual Results CreateResults(ScoreInfo score) => new SoloResults(score);
+
+ #region Storyboard
+
+ private DrawableStoryboard storyboard;
+ protected UserDimContainer StoryboardContainer { get; private set; }
+
+ protected virtual UserDimContainer CreateStoryboardContainer() => new UserDimContainer(true)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 1,
+ EnableUserDim = { Value = true }
+ };
+
+ private Bindable