diff --git a/osu.Android.props b/osu.Android.props
index 0f6e32d664..93a9a073a4 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -60,7 +60,7 @@
-
-
+
+
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 7c282f449b..c527a81f51 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
@@ -4,7 +4,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index 6f1a7873ec..71e05083be 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Catch
{
public class CatchRuleset : Ruleset
{
- public override DrawableRuleset CreateDrawableRulesetWith(WorkingBeatmap beatmap, IReadOnlyList mods) => new DrawableCatchRuleset(this, beatmap, mods);
+ public override DrawableRuleset CreateDrawableRulesetWith(IWorkingBeatmap beatmap, IReadOnlyList mods) => new DrawableCatchRuleset(this, beatmap, mods);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap);
public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new CatchBeatmapProcessor(beatmap);
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs
index f4218061d4..00734810b3 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs
@@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt;
- protected override void UpdateInitialTransforms() => this.FadeIn(200);
+ protected override void UpdateInitialTransforms() => this.FadeInFromZero(200);
protected override void UpdateStateTransforms(ArmedState state)
{
diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
index f48b84e344..6b7f00c5d0 100644
--- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
@@ -25,11 +25,11 @@ namespace osu.Game.Rulesets.Catch.UI
protected override bool UserScrollSpeedAdjustment => false;
- public DrawableCatchRuleset(Ruleset ruleset, WorkingBeatmap beatmap, IReadOnlyList mods)
+ public DrawableCatchRuleset(Ruleset ruleset, IWorkingBeatmap beatmap, IReadOnlyList mods)
: base(ruleset, beatmap, mods)
{
Direction.Value = ScrollingDirection.Down;
- TimeRange.Value = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450);
+ TimeRange.Value = BeatmapDifficulty.DifficultyRange(beatmap.Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450);
}
public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(this);
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 4dcfc1b81f..af10d5e06e 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
@@ -4,7 +4,7 @@
-
+
diff --git a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditRuleset.cs b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditRuleset.cs
index e5f379f608..97d8aaa052 100644
--- a/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditRuleset.cs
+++ b/osu.Game.Rulesets.Mania/Edit/DrawableManiaEditRuleset.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Edit
{
public new IScrollingInfo ScrollingInfo => base.ScrollingInfo;
- public DrawableManiaEditRuleset(Ruleset ruleset, WorkingBeatmap beatmap, IReadOnlyList mods)
+ public DrawableManiaEditRuleset(Ruleset ruleset, IWorkingBeatmap beatmap, IReadOnlyList mods)
: base(ruleset, beatmap, mods)
{
}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
index 2729621ab3..0bfe6f9517 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Edit
[Cached(Type = typeof(IManiaHitObjectComposer))]
public class ManiaHitObjectComposer : HitObjectComposer, IManiaHitObjectComposer
{
- protected new DrawableManiaEditRuleset DrawableRuleset { get; private set; }
+ private DrawableManiaEditRuleset drawableRuleset;
public ManiaHitObjectComposer(Ruleset ruleset)
: base(ruleset)
@@ -33,23 +33,23 @@ namespace osu.Game.Rulesets.Mania.Edit
///
/// The screen-space position.
/// The column which intersects with .
- public Column ColumnAt(Vector2 screenSpacePosition) => DrawableRuleset.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)DrawableRuleset.Playfield).TotalColumns;
+ public int TotalColumns => ((ManiaPlayfield)drawableRuleset.Playfield).TotalColumns;
- protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, WorkingBeatmap beatmap, IReadOnlyList mods)
+ protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IWorkingBeatmap beatmap, IReadOnlyList mods)
{
- DrawableRuleset = new DrawableManiaEditRuleset(ruleset, beatmap, mods);
+ drawableRuleset = new DrawableManiaEditRuleset(ruleset, beatmap, mods);
// This is the earliest we can cache the scrolling info to ourselves, before masks are added to the hierarchy and inject it
- dependencies.CacheAs(DrawableRuleset.ScrollingInfo);
+ dependencies.CacheAs(drawableRuleset.ScrollingInfo);
- return DrawableRuleset;
+ 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 8966b5058f..0de86c2149 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania
{
public class ManiaRuleset : Ruleset
{
- public override DrawableRuleset CreateDrawableRulesetWith(WorkingBeatmap beatmap, IReadOnlyList mods) => new DrawableManiaRuleset(this, beatmap, mods);
+ public override DrawableRuleset CreateDrawableRulesetWith(IWorkingBeatmap beatmap, IReadOnlyList mods) => new DrawableManiaRuleset(this, beatmap, mods);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap);
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new ManiaPerformanceCalculator(this, beatmap, score);
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
index ce1484d460..e5b114ca81 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
@@ -46,8 +46,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Anchor = Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
}
- protected override void UpdateInitialTransforms() => this.FadeIn();
-
protected override void UpdateStateTransforms(ArmedState state)
{
switch (state)
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
index c8aeda8fe4..f26526fe70 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
@@ -36,11 +36,13 @@ namespace osu.Game.Rulesets.Mania.UI
public IEnumerable BarLines;
+ protected override bool RelativeScaleBeatLengths => true;
+
protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config;
private readonly Bindable configDirection = new Bindable();
- public DrawableManiaRuleset(Ruleset ruleset, WorkingBeatmap beatmap, IReadOnlyList mods)
+ public DrawableManiaRuleset(Ruleset ruleset, IWorkingBeatmap beatmap, IReadOnlyList mods)
: base(ruleset, beatmap, mods)
{
// Generate the bar lines
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/cursor@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/cursor@2x.png
new file mode 100755
index 0000000000..75f9ba5ea6
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/cursor@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/cursormiddle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/cursormiddle@2x.png
new file mode 100755
index 0000000000..ebf59c18ba
Binary files /dev/null and b/osu.Game.Rulesets.Osu.Tests/Resources/default-skin/cursormiddle@2x.png differ
diff --git a/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs b/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs
index 02716dc1d5..29e5146ff1 100644
--- a/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs
+++ b/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs
@@ -37,10 +37,21 @@ namespace osu.Game.Rulesets.Osu.Tests
public void SetContents(Func creationFunction)
{
- Cell(0).Child = new LocalSkinOverrideContainer(null) { RelativeSizeAxes = Axes.Both }.WithChild(creationFunction());
- Cell(1).Child = new LocalSkinOverrideContainer(metricsSkin) { RelativeSizeAxes = Axes.Both }.WithChild(creationFunction());
- Cell(2).Child = new LocalSkinOverrideContainer(defaultSkin) { RelativeSizeAxes = Axes.Both }.WithChild(creationFunction());
- Cell(3).Child = new LocalSkinOverrideContainer(specialSkin) { RelativeSizeAxes = Axes.Both }.WithChild(creationFunction());
+ Cell(0).Child = createProvider(null, creationFunction);
+ Cell(1).Child = createProvider(metricsSkin, creationFunction);
+ Cell(2).Child = createProvider(defaultSkin, creationFunction);
+ Cell(3).Child = createProvider(specialSkin, creationFunction);
+ }
+
+ private Drawable createProvider(Skin skin, Func creationFunction)
+ {
+ var mainProvider = new SkinProvidingContainer(skin);
+
+ return mainProvider
+ .WithChild(new SkinProvidingContainer(Ruleset.Value.CreateInstance().CreateLegacySkinProvider(mainProvider))
+ {
+ Child = creationFunction()
+ });
}
private class TestLegacySkin : LegacySkin
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
index 1b1cfa89c0..ebb6cd3a5a 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
@@ -6,29 +6,23 @@ using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Cursor;
-using osu.Game.Graphics.Cursor;
using osu.Game.Rulesets.Osu.UI.Cursor;
-using osu.Game.Rulesets.UI;
-using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
- public class TestSceneGameplayCursor : OsuTestScene, IProvideCursor
+ public class TestSceneGameplayCursor : SkinnableTestScene
{
- private GameplayCursorContainer cursorContainer;
-
public override IReadOnlyList RequiredTypes => new[] { typeof(CursorTrail) };
- public CursorContainer Cursor => cursorContainer;
-
- public bool ProvidingUserCursor => true;
-
[BackgroundDependencyLoader]
private void load()
{
- Add(cursorContainer = new OsuCursorContainer { RelativeSizeAxes = Axes.Both });
+ SetContents(() => new OsuCursorContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ });
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
new file mode 100644
index 0000000000..731b0a84e9
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
@@ -0,0 +1,157 @@
+// 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 NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Audio.Sample;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Timing;
+using osu.Game.Audio;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Screens.Play;
+using osu.Game.Skinning;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ [TestFixture]
+ public class TestSceneSkinFallbacks : PlayerTestScene
+ {
+ private readonly TestSource testUserSkin;
+ private readonly TestSource testBeatmapSkin;
+
+ public TestSceneSkinFallbacks()
+ : base(new OsuRuleset())
+ {
+ testUserSkin = new TestSource("user");
+ testBeatmapSkin = new TestSource("beatmap");
+ }
+
+ [Test]
+ public void TestBeatmapSkinDefault()
+ {
+ AddStep("enable user provider", () => testUserSkin.Enabled = true);
+
+ AddStep("enable beatmap skin", () => LocalConfig.Set(OsuSetting.BeatmapSkins, true));
+ checkNextHitObject("beatmap");
+
+ AddStep("disable beatmap skin", () => LocalConfig.Set(OsuSetting.BeatmapSkins, false));
+ checkNextHitObject("user");
+
+ AddStep("disable user provider", () => testUserSkin.Enabled = false);
+ checkNextHitObject(null);
+ }
+
+ private void checkNextHitObject(string skin) =>
+ AddUntilStep($"check skin from {skin}", () =>
+ {
+ var firstObject = ((TestPlayer)Player).DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.OfType().FirstOrDefault();
+
+ if (firstObject == null)
+ return false;
+
+ var skinnable = firstObject.ApproachCircle.Child as SkinnableDrawable;
+
+ if (skin == null && skinnable?.Drawable is Sprite)
+ // check for default skin provider
+ return true;
+
+ var text = skinnable?.Drawable as SpriteText;
+
+ return text?.Text == skin;
+ });
+
+ [Resolved]
+ private AudioManager audio { get; set; }
+
+ protected override Player CreatePlayer(Ruleset ruleset) => new SkinProvidingPlayer(testUserSkin);
+
+ protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap) => new CustomSkinWorkingBeatmap(beatmap, Clock, audio, testBeatmapSkin);
+
+ public class CustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap
+ {
+ private readonly ISkinSource skin;
+
+ public CustomSkinWorkingBeatmap(IBeatmap beatmap, IFrameBasedClock frameBasedClock, AudioManager audio, ISkinSource skin)
+ : base(beatmap, frameBasedClock, audio)
+ {
+ this.skin = skin;
+ }
+
+ protected override ISkin GetSkin() => skin;
+ }
+
+ public class SkinProvidingPlayer : TestPlayer
+ {
+ private readonly TestSource userSkin;
+
+ public SkinProvidingPlayer(TestSource userSkin)
+ {
+ this.userSkin = userSkin;
+ }
+
+ private DependencyContainer dependencies;
+
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
+ {
+ dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
+
+ dependencies.CacheAs(userSkin);
+
+ return dependencies;
+ }
+ }
+
+ public class TestSource : ISkinSource
+ {
+ private readonly string identifier;
+
+ public TestSource(string identifier)
+ {
+ this.identifier = identifier;
+ }
+
+ public Drawable GetDrawableComponent(string componentName)
+ {
+ if (!enabled) return null;
+
+ return new SpriteText
+ {
+ Text = identifier,
+ Font = OsuFont.Default.With(size: 30),
+ };
+ }
+
+ public Texture GetTexture(string componentName) => null;
+
+ public SampleChannel GetSample(ISampleInfo sampleInfo) => null;
+
+ public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => default;
+
+ public event Action SourceChanged;
+
+ private bool enabled = true;
+
+ public bool Enabled
+ {
+ get => enabled;
+ set
+ {
+ if (value == enabled)
+ return;
+
+ enabled = value;
+ SourceChanged?.Invoke();
+ }
+ }
+ }
+ }
+}
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 197309c7c4..c331c811d2 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
@@ -4,7 +4,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs
index bcb6099cfb..cc08d356f9 100644
--- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs
+++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public class DrawableOsuEditRuleset : DrawableOsuRuleset
{
- public DrawableOsuEditRuleset(Ruleset ruleset, WorkingBeatmap beatmap, IReadOnlyList mods)
+ public DrawableOsuEditRuleset(Ruleset ruleset, IWorkingBeatmap beatmap, IReadOnlyList mods)
: base(ruleset, beatmap, mods)
{
}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index c5452ae0aa..1c040e9dee 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{
}
- protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, WorkingBeatmap beatmap, IReadOnlyList mods)
+ protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IWorkingBeatmap beatmap, IReadOnlyList mods)
=> new DrawableOsuEditRuleset(ruleset, beatmap, mods);
protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[]
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index ca124e9214..0af278f6a4 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly HitArea hitArea;
+ private readonly SkinnableDrawable mainContent;
+
public DrawableHitCircle(HitCircle h)
: base(h)
{
@@ -56,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return true;
},
},
- new SkinnableDrawable("Play/osu/hitcircle", _ => new MainCirclePiece(HitObject.IndexInCurrentCombo)),
+ mainContent = new SkinnableDrawable("Play/osu/hitcircle", _ => new MainCirclePiece(HitObject.IndexInCurrentCombo)),
ApproachCircle = new ApproachCircle
{
Alpha = 0,
@@ -108,6 +110,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.UpdateInitialTransforms();
+ mainContent.FadeInFromZero(HitObject.TimeFadeIn);
+
ApproachCircle.FadeIn(Math.Min(HitObject.TimeFadeIn * 2, HitObject.TimePreempt));
ApproachCircle.ScaleTo(1f, HitObject.TimePreempt);
ApproachCircle.Expire(true);
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
index a89fb8b682..b4f5642f45 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs
@@ -36,8 +36,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt;
- protected override void UpdateInitialTransforms() => this.FadeIn(HitObject.TimeFadeIn);
-
private OsuInputManager osuActionInputManager;
internal OsuInputManager OsuActionInputManager => osuActionInputManager ?? (osuActionInputManager = GetContainingInputManager() as OsuInputManager);
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index a0626707af..1749ea1f60 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -93,6 +93,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
}
+ protected override void UpdateInitialTransforms()
+ {
+ base.UpdateInitialTransforms();
+
+ Body.FadeInFromZero(HitObject.TimeFadeIn);
+ }
+
[BackgroundDependencyLoader]
private void load()
{
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index d50d4f401c..49676933e1 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -23,13 +23,15 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Difficulty;
+using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Scoring;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets.Osu
{
public class OsuRuleset : Ruleset
{
- public override DrawableRuleset CreateDrawableRulesetWith(WorkingBeatmap beatmap, IReadOnlyList mods) => new DrawableOsuRuleset(this, beatmap, mods);
+ public override DrawableRuleset CreateDrawableRulesetWith(IWorkingBeatmap beatmap, IReadOnlyList mods) => new DrawableOsuRuleset(this, beatmap, mods);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap);
public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new OsuBeatmapProcessor(beatmap);
@@ -163,6 +165,8 @@ namespace osu.Game.Rulesets.Osu
public override RulesetSettingsSubsection CreateSettings() => new OsuSettingsSubsection(this);
+ public override ISkin CreateLegacySkinProvider(ISkinSource source) => new OsuLegacySkin(source);
+
public override int? LegacyID => 0;
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame();
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyCursor.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyCursor.cs
new file mode 100644
index 0000000000..470ba3acae
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyCursor.cs
@@ -0,0 +1,42 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Skinning;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Skinning
+{
+ public class LegacyCursor : CompositeDrawable
+ {
+ public LegacyCursor()
+ {
+ Size = new Vector2(50);
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin)
+ {
+ InternalChildren = new Drawable[]
+ {
+ new NonPlayfieldSprite
+ {
+ Texture = skin.GetTexture("cursormiddle"),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ new NonPlayfieldSprite
+ {
+ Texture = skin.GetTexture("cursor"),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
new file mode 100644
index 0000000000..a7906ddd24
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs
@@ -0,0 +1,81 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Skinning;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning
+{
+ public class LegacyMainCirclePiece : CompositeDrawable
+ {
+ public LegacyMainCirclePiece()
+ {
+ Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
+ }
+
+ private readonly IBindable state = new Bindable();
+
+ private readonly Bindable accentColour = new Bindable();
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject drawableObject, ISkinSource skin)
+ {
+ Sprite hitCircleSprite;
+
+ InternalChildren = new Drawable[]
+ {
+ hitCircleSprite = new Sprite
+ {
+ Texture = skin.GetTexture("hitcircle"),
+ Colour = drawableObject.AccentColour.Value,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ new SkinnableSpriteText("Play/osu/number-text", _ => new OsuSpriteText
+ {
+ Font = OsuFont.Numeric.With(size: 40),
+ UseFullGlyphHeight = false,
+ }, confineMode: ConfineMode.NoScaling)
+ {
+ Text = (((IHasComboInformation)drawableObject.HitObject).IndexInCurrentCombo + 1).ToString()
+ },
+ new Sprite
+ {
+ Texture = skin.GetTexture("hitcircleoverlay"),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ };
+
+ state.BindTo(drawableObject.State);
+ state.BindValueChanged(updateState, true);
+
+ accentColour.BindTo(drawableObject.AccentColour);
+ accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true);
+ }
+
+ private void updateState(ValueChangedEvent state)
+ {
+ const double legacy_fade_duration = 240;
+
+ switch (state.NewValue)
+ {
+ case ArmedState.Hit:
+ this.FadeOut(legacy_fade_duration, Easing.Out);
+ this.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
+ break;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs
new file mode 100644
index 0000000000..ec838c596d
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs
@@ -0,0 +1,44 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Skinning;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning
+{
+ public class LegacySliderBall : CompositeDrawable
+ {
+ private readonly Drawable animationContent;
+
+ public LegacySliderBall(Drawable animationContent)
+ {
+ this.animationContent = animationContent;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ISkinSource skin, DrawableHitObject drawableObject)
+ {
+ animationContent.Colour = skin.GetValue(s => s.CustomColours.ContainsKey("SliderBall") ? s.CustomColours["SliderBall"] : (Color4?)null) ?? Color4.White;
+
+ InternalChildren = new[]
+ {
+ new Sprite
+ {
+ Texture = skin.GetTexture("sliderb-nd"),
+ Colour = new Color4(5, 5, 5, 255),
+ },
+ animationContent,
+ new Sprite
+ {
+ Texture = skin.GetTexture("sliderb-spec"),
+ Blending = BlendingParameters.Additive,
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/NonPlayfieldSprite.cs b/osu.Game.Rulesets.Osu/Skinning/NonPlayfieldSprite.cs
new file mode 100644
index 0000000000..55257106e2
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/NonPlayfieldSprite.cs
@@ -0,0 +1,28 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Osu.Skinning
+{
+ ///
+ /// A sprite which is displayed within the playfield, but historically was not considered part of the playfield.
+ /// Performs scale adjustment to undo the scale applied by (osu! ruleset specifically).
+ ///
+ public class NonPlayfieldSprite : Sprite
+ {
+ public override Texture Texture
+ {
+ get => base.Texture;
+ set
+ {
+ if (value != null)
+ // stable "magic ratio". see OsuPlayfieldAdjustmentContainer for full explanation.
+ value.ScaleAdjust *= 1.6f;
+ base.Texture = value;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkin.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkin.cs
new file mode 100644
index 0000000000..ea7257d258
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkin.cs
@@ -0,0 +1,128 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Audio.Sample;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Audio;
+using osu.Game.Skinning;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning
+{
+ public class OsuLegacySkin : ISkin
+ {
+ private readonly ISkin source;
+
+ private Lazy configuration;
+
+ private Lazy hasHitCircle;
+
+ ///
+ /// On osu-stable, hitcircles have 5 pixels of transparent padding on each side to allow for shadows etc.
+ /// Their hittable area is 128px, but the actual circle portion is 118px.
+ /// We must account for some gameplay elements such as slider bodies, where this padding is not present.
+ ///
+ private const float legacy_circle_radius = 64 - 5;
+
+ public OsuLegacySkin(ISkinSource source)
+ {
+ this.source = source;
+
+ source.SourceChanged += sourceChanged;
+ sourceChanged();
+ }
+
+ private void sourceChanged()
+ {
+ // these need to be lazy in order to ensure they aren't called before the dependencies have been loaded into our source.
+ configuration = new Lazy(() =>
+ {
+ var config = new SkinConfiguration();
+ if (hasHitCircle.Value)
+ config.SliderPathRadius = legacy_circle_radius;
+
+ // defaults should only be applied for non-beatmap skins (which are parsed via this constructor).
+ config.CustomColours["SliderBall"] =
+ source.GetValue(s => s.CustomColours.TryGetValue("SliderBall", out var val) ? val : (Color4?)null)
+ ?? new Color4(2, 170, 255, 255);
+
+ return config;
+ });
+
+ hasHitCircle = new Lazy(() => source.GetTexture("hitcircle") != null);
+ }
+
+ public Drawable GetDrawableComponent(string componentName)
+ {
+ switch (componentName)
+ {
+ case "Play/osu/sliderfollowcircle":
+ return this.GetAnimation(componentName, true, true);
+
+ case "Play/osu/sliderball":
+ var sliderBallContent = this.GetAnimation("sliderb", true, true, "");
+
+ if (sliderBallContent != null)
+ {
+ var size = sliderBallContent.Size;
+
+ sliderBallContent.RelativeSizeAxes = Axes.Both;
+ sliderBallContent.Size = Vector2.One;
+
+ return new LegacySliderBall(sliderBallContent)
+ {
+ Size = size
+ };
+ }
+
+ return null;
+
+ case "Play/osu/hitcircle":
+ if (hasHitCircle.Value)
+ return new LegacyMainCirclePiece();
+
+ return null;
+
+ case "Play/osu/cursor":
+ if (source.GetTexture("cursor") != null)
+ return new LegacyCursor();
+
+ return null;
+
+ case "Play/osu/number-text":
+
+ string font = GetValue(config => config.HitCircleFont);
+ var overlap = GetValue(config => config.HitCircleOverlap);
+
+ return !hasFont(font)
+ ? null
+ : new LegacySpriteText(source, font)
+ {
+ // Spacing value was reverse-engineered from the ratio of the rendered sprite size in the visual inspector vs the actual texture size
+ Scale = new Vector2(0.96f),
+ Spacing = new Vector2(-overlap * 0.89f, 0)
+ };
+ }
+
+ return null;
+ }
+
+ public Texture GetTexture(string componentName) => source.GetTexture(componentName);
+
+ public SampleChannel GetSample(ISampleInfo sample) => source.GetSample(sample);
+
+ public TValue GetValue(Func query) where TConfiguration : SkinConfiguration
+ {
+ TValue val;
+ if (configuration.Value is TConfiguration conf && (val = query.Invoke(conf)) != null)
+ return val;
+
+ return source.GetValue(query);
+ }
+
+ private bool hasFont(string fontName) => source.GetTexture($"{fontName}-0") != null;
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs
index d185d7d4c9..aa61fb6922 100644
--- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.UI
{
protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config;
- public DrawableOsuRuleset(Ruleset ruleset, WorkingBeatmap beatmap, IReadOnlyList mods)
+ public DrawableOsuRuleset(Ruleset ruleset, IWorkingBeatmap beatmap, IReadOnlyList mods)
: base(ruleset, beatmap, mods)
{
}
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index 9037faf606..ea7eee8bb8 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -42,9 +42,8 @@ namespace osu.Game.Rulesets.Osu.UI
},
// Todo: This should not exist, but currently helps to reduce LOH allocations due to unbinding skin source events on judgement disposal
// Todo: Remove when hitobjects are properly pooled
- new LocalSkinOverrideContainer(null)
+ new SkinProvidingContainer(null)
{
- RelativeSizeAxes = Axes.Both,
Child = HitObjectContainer,
},
approachCircles = new ApproachCircleProxyContainer
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs
new file mode 100644
index 0000000000..f27e329e8e
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneSwellJudgements.cs
@@ -0,0 +1,74 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Taiko.Judgements;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Screens.Play;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Taiko.Tests
+{
+ public class TestSceneSwellJudgements : PlayerTestScene
+ {
+ protected new TestPlayer Player => (TestPlayer)base.Player;
+
+ public TestSceneSwellJudgements()
+ : base(new TaikoRuleset())
+ {
+ }
+
+ [Test]
+ public void TestZeroTickTimeOffsets()
+ {
+ AddUntilStep("gameplay finished", () => Player.ScoreProcessor.HasCompleted);
+ AddAssert("all tick offsets are 0", () => Player.Results.Where(r => r.Judgement is TaikoSwellTickJudgement).All(r => r.TimeOffset == 0));
+ }
+
+ protected override bool Autoplay => true;
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
+ {
+ var beatmap = new Beatmap
+ {
+ BeatmapInfo = { Ruleset = new TaikoRuleset().RulesetInfo },
+ HitObjects =
+ {
+ new Swell
+ {
+ StartTime = 1000,
+ Duration = 1000,
+ }
+ }
+ };
+
+ return beatmap;
+ }
+
+ protected override Player CreatePlayer(Ruleset ruleset) => new TestPlayer();
+
+ protected class TestPlayer : Player
+ {
+ public readonly List Results = new List();
+
+ public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
+
+ public TestPlayer()
+ : base(false, false)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ ScoreProcessor.NewJudgement += r => Results.Add(r);
+ }
+ }
+ }
+}
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 a5db1625d9..d2a0a8fa6f 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
@@ -4,7 +4,7 @@
-
+
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs
index 8b27d78101..ce875ebba8 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Graphics;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
@@ -14,7 +15,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
}
- public void TriggerResult(HitResult type) => ApplyResult(r => r.Type = type);
+ protected override void UpdateInitialTransforms() => this.FadeOut();
+
+ public void TriggerResult(HitResult type)
+ {
+ HitObject.StartTime = Time.Current;
+ ApplyResult(r => r.Type = type);
+ }
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
index 5f3bab5b28..5424ccb4de 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
@@ -78,8 +78,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
public abstract bool OnPressed(TaikoAction action);
public virtual bool OnReleased(TaikoAction action) => false;
- protected override void UpdateInitialTransforms() => this.FadeIn();
-
public override double LifetimeStart
{
get => base.LifetimeStart;
diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
index 83356b77c2..6d0a5eb1e1 100644
--- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko
{
public class TaikoRuleset : Ruleset
{
- public override DrawableRuleset CreateDrawableRulesetWith(WorkingBeatmap beatmap, IReadOnlyList mods) => new DrawableTaikoRuleset(this, beatmap, mods);
+ public override DrawableRuleset CreateDrawableRulesetWith(IWorkingBeatmap beatmap, IReadOnlyList mods) => new DrawableTaikoRuleset(this, beatmap, mods);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap);
public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[]
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
index ec3a56e9c7..b03bea578e 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.UI
protected override bool UserScrollSpeedAdjustment => false;
- public DrawableTaikoRuleset(Ruleset ruleset, WorkingBeatmap beatmap, IReadOnlyList mods)
+ public DrawableTaikoRuleset(Ruleset ruleset, IWorkingBeatmap beatmap, IReadOnlyList mods)
: base(ruleset, beatmap, mods)
{
Direction.Value = ScrollingDirection.Left;
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
index e62dc45cab..a10f70a344 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
@@ -44,9 +44,8 @@ namespace osu.Game.Rulesets.Taiko.UI
private readonly JudgementContainer judgementContainer;
internal readonly HitTarget HitTarget;
- private readonly Container topLevelHitContainer;
-
- private readonly Container barlineContainer;
+ private readonly ProxyContainer topLevelHitContainer;
+ private readonly ProxyContainer barlineContainer;
private readonly Container overlayBackgroundContainer;
private readonly Container backgroundContainer;
@@ -108,7 +107,7 @@ namespace osu.Game.Rulesets.Taiko.UI
}
}
},
- barlineContainer = new Container
+ barlineContainer = new ProxyContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = HIT_TARGET_OFFSET }
@@ -183,7 +182,7 @@ namespace osu.Game.Rulesets.Taiko.UI
}
}
},
- topLevelHitContainer = new Container
+ topLevelHitContainer = new ProxyContainer
{
Name = "Top level hit objects",
RelativeSizeAxes = Axes.Both,
@@ -256,5 +255,15 @@ namespace osu.Game.Rulesets.Taiko.UI
break;
}
}
+
+ private class ProxyContainer : LifetimeManagementContainer
+ {
+ public new MarginPadding Padding
+ {
+ set => base.Padding = value;
+ }
+
+ public void Add(Drawable proxy) => AddInternal(proxy);
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs
index 7accbe2fa8..0ea73fb3de 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs
@@ -16,15 +16,13 @@ using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Screens.Edit.Compose;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Tests.Visual.Editor
{
[TestFixture]
- [Cached(Type = typeof(IPlacementHandler))]
- public class TestSceneHitObjectComposer : OsuTestScene, IPlacementHandler
+ public class TestSceneHitObjectComposer : OsuTestScene
{
public override IReadOnlyList RequiredTypes => new[]
{
@@ -39,8 +37,6 @@ namespace osu.Game.Tests.Visual.Editor
typeof(HitCirclePlacementBlueprint),
};
- private HitObjectComposer composer;
-
[BackgroundDependencyLoader]
private void load()
{
@@ -67,15 +63,7 @@ namespace osu.Game.Tests.Visual.Editor
Dependencies.CacheAs(clock);
Dependencies.CacheAs(clock);
- Child = composer = new OsuHitObjectComposer(new OsuRuleset());
+ Child = new OsuHitObjectComposer(new OsuRuleset());
}
-
- public void BeginPlacement(HitObject hitObject)
- {
- }
-
- public void EndPlacement(HitObject hitObject) => composer.Add(hitObject);
-
- public void Delete(HitObject hitObject) => composer.Remove(hitObject);
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBarHitErrorMeter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBarHitErrorMeter.cs
new file mode 100644
index 0000000000..f20440249b
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBarHitErrorMeter.cs
@@ -0,0 +1,119 @@
+// 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.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Catch.Objects;
+using System;
+using System.Collections.Generic;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osu.Framework.MathUtils;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Screens.Play.HUD.HitErrorMeters;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneBarHitErrorMeter : OsuTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(HitErrorMeter),
+ };
+
+ private HitErrorMeter meter;
+ private HitErrorMeter meter2;
+ private HitWindows hitWindows;
+
+ public TestSceneBarHitErrorMeter()
+ {
+ recreateDisplay(new OsuHitWindows(), 5);
+
+ AddRepeatStep("New random judgement", () => newJudgement(), 40);
+
+ AddRepeatStep("New max negative", () => newJudgement(-hitWindows.HalfWindowFor(HitResult.Meh)), 20);
+ AddRepeatStep("New max positive", () => newJudgement(hitWindows.HalfWindowFor(HitResult.Meh)), 20);
+ AddStep("New fixed judgement (50ms)", () => newJudgement(50));
+ }
+
+ [Test]
+ public void TestOsu()
+ {
+ AddStep("OD 1", () => recreateDisplay(new OsuHitWindows(), 1));
+ AddStep("OD 10", () => recreateDisplay(new OsuHitWindows(), 10));
+ }
+
+ [Test]
+ public void TestTaiko()
+ {
+ AddStep("OD 1", () => recreateDisplay(new TaikoHitWindows(), 1));
+ AddStep("OD 10", () => recreateDisplay(new TaikoHitWindows(), 10));
+ }
+
+ [Test]
+ public void TestMania()
+ {
+ AddStep("OD 1", () => recreateDisplay(new ManiaHitWindows(), 1));
+ AddStep("OD 10", () => recreateDisplay(new ManiaHitWindows(), 10));
+ }
+
+ [Test]
+ public void TestCatch()
+ {
+ AddStep("OD 1", () => recreateDisplay(new CatchHitWindows(), 1));
+ AddStep("OD 10", () => recreateDisplay(new CatchHitWindows(), 10));
+ }
+
+ private void recreateDisplay(HitWindows hitWindows, float overallDifficulty)
+ {
+ this.hitWindows = hitWindows;
+
+ hitWindows?.SetDifficulty(overallDifficulty);
+
+ Clear();
+
+ Add(new FillFlowContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Direction = FillDirection.Vertical,
+ AutoSizeAxes = Axes.Both,
+ Children = new[]
+ {
+ new SpriteText { Text = $@"Great: {hitWindows?.Great}" },
+ new SpriteText { Text = $@"Good: {hitWindows?.Good}" },
+ new SpriteText { Text = $@"Meh: {hitWindows?.Meh}" },
+ }
+ });
+
+ Add(meter = new BarHitErrorMeter(hitWindows, true)
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ });
+
+ Add(meter2 = new BarHitErrorMeter(hitWindows, false)
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ });
+ }
+
+ private void newJudgement(double offset = 0)
+ {
+ var judgement = new JudgementResult(new Judgement())
+ {
+ TimeOffset = offset == 0 ? RNG.Next(-150, 150) : offset,
+ Type = HitResult.Perfect,
+ };
+
+ meter.OnNewJudgement(judgement);
+ meter2.OnNewJudgement(judgement);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs
new file mode 100644
index 0000000000..60ace8ea69
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs
@@ -0,0 +1,305 @@
+// 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.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input;
+using osu.Framework.MathUtils;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Configuration;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Difficulty;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneDrawableScrollingRuleset : OsuTestScene
+ {
+ ///
+ /// The amount of time visible by the "view window" of the playfield.
+ /// All hitobjects added through are spaced apart by this value, such that for a beat length of 1000,
+ /// there will be at most 2 hitobjects visible in the "view window".
+ ///
+ private const double time_range = 1000;
+
+ private readonly ManualClock testClock = new ManualClock();
+ private TestDrawableScrollingRuleset drawableRuleset;
+
+ [SetUp]
+ public void Setup() => Schedule(() => testClock.CurrentTime = 0);
+
+ [Test]
+ public void TestRelativeBeatLengthScaleSingleTimingPoint()
+ {
+ var beatmap = createBeatmap(new TimingControlPoint { BeatLength = time_range / 2 });
+
+ createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
+
+ assertPosition(0, 0f);
+
+ // The single timing point is 1x speed relative to itself, such that the hitobject occurring time_range milliseconds later should appear
+ // at the bottom of the view window regardless of the timing point's beat length
+ assertPosition(1, 1f);
+ }
+
+ [Test]
+ public void TestRelativeBeatLengthScaleTimingPointBeyondEndDoesNotBecomeDominant()
+ {
+ var beatmap = createBeatmap(
+ new TimingControlPoint { BeatLength = time_range / 2 },
+ new TimingControlPoint { Time = 12000, BeatLength = time_range },
+ new TimingControlPoint { Time = 100000, BeatLength = time_range });
+
+ createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
+
+ assertPosition(0, 0f);
+ assertPosition(1, 1f);
+ }
+
+ [Test]
+ public void TestRelativeBeatLengthScaleFromSecondTimingPoint()
+ {
+ var beatmap = createBeatmap(
+ new TimingControlPoint { BeatLength = time_range },
+ new TimingControlPoint { Time = 3 * time_range, BeatLength = time_range / 2 });
+
+ createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true);
+
+ // The first timing point should have a relative velocity of 2
+ assertPosition(0, 0f);
+ assertPosition(1, 0.5f);
+ assertPosition(2, 1f);
+
+ // Move to the second timing point
+ setTime(3 * time_range);
+ assertPosition(3, 0f);
+
+ // As above, this is the timing point that is 1x speed relative to itself, so the hitobject occurring time_range milliseconds later should be at the bottom of the view window
+ assertPosition(4, 1f);
+ }
+
+ [Test]
+ public void TestNonRelativeScale()
+ {
+ var beatmap = createBeatmap(
+ new TimingControlPoint { BeatLength = time_range },
+ new TimingControlPoint { Time = 3 * time_range, BeatLength = time_range / 2 });
+
+ createTest(beatmap);
+
+ assertPosition(0, 0f);
+ assertPosition(1, 1);
+
+ // Move to the second timing point
+ setTime(3 * time_range);
+ assertPosition(3, 0f);
+
+ // For a beat length of 500, the view window of this timing point is elongated 2x (1000 / 500), such that the second hitobject is two TimeRanges away (offscreen)
+ // To bring it on-screen, half TimeRange is added to the current time, bringing the second half of the view window into view, and the hitobject should appear at the bottom
+ setTime(3 * time_range + time_range / 2);
+ assertPosition(4, 1f);
+ }
+
+ private void assertPosition(int index, float relativeY) => AddAssert($"hitobject {index} at {relativeY}",
+ () => Precision.AlmostEquals(drawableRuleset.Playfield.AllHitObjects.ElementAt(index).DrawPosition.Y, drawableRuleset.Playfield.HitObjectContainer.DrawHeight * relativeY));
+
+ private void setTime(double time)
+ {
+ AddStep($"set time = {time}", () => testClock.CurrentTime = time);
+ }
+
+ ///
+ /// Creates an , containing 10 hitobjects and user-provided timing points.
+ /// The hitobjects are spaced milliseconds apart.
+ ///
+ /// The timing points to add to the beatmap.
+ /// The .
+ private IBeatmap createBeatmap(params TimingControlPoint[] timingControlPoints)
+ {
+ var beatmap = new Beatmap { BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo } };
+
+ beatmap.ControlPointInfo.TimingPoints.AddRange(timingControlPoints);
+
+ for (int i = 0; i < 10; i++)
+ beatmap.HitObjects.Add(new HitObject { StartTime = i * time_range });
+
+ return beatmap;
+ }
+
+ private void createTest(IBeatmap beatmap, Action overrideAction = null) => AddStep("create test", () =>
+ {
+ var ruleset = new TestScrollingRuleset();
+
+ drawableRuleset = (TestDrawableScrollingRuleset)ruleset.CreateDrawableRulesetWith(CreateWorkingBeatmap(beatmap), Array.Empty());
+ drawableRuleset.FrameStablePlayback = false;
+
+ overrideAction?.Invoke(drawableRuleset);
+
+ Child = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Y,
+ Height = 0.75f,
+ Width = 400,
+ Masking = true,
+ Clock = new FramedClock(testClock),
+ Child = drawableRuleset
+ };
+ });
+
+ #region Ruleset
+
+ private class TestScrollingRuleset : Ruleset
+ {
+ public TestScrollingRuleset(RulesetInfo rulesetInfo = null)
+ : base(rulesetInfo)
+ {
+ }
+
+ public override IEnumerable GetModsFor(ModType type) => throw new NotImplementedException();
+
+ public override DrawableRuleset CreateDrawableRulesetWith(IWorkingBeatmap beatmap, IReadOnlyList mods) => new TestDrawableScrollingRuleset(this, beatmap, mods);
+
+ public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TestBeatmapConverter(beatmap);
+
+ public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => throw new NotImplementedException();
+
+ public override string Description { get; } = string.Empty;
+
+ public override string ShortName { get; } = string.Empty;
+ }
+
+ private class TestDrawableScrollingRuleset : DrawableScrollingRuleset
+ {
+ public bool RelativeScaleBeatLengthsOverride { get; set; }
+
+ protected override bool RelativeScaleBeatLengths => RelativeScaleBeatLengthsOverride;
+
+ protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
+
+ public TestDrawableScrollingRuleset(Ruleset ruleset, IWorkingBeatmap beatmap, IReadOnlyList mods)
+ : base(ruleset, beatmap, mods)
+ {
+ TimeRange.Value = time_range;
+ }
+
+ public override DrawableHitObject CreateDrawableRepresentation(TestHitObject h) => new DrawableTestHitObject(h);
+
+ protected override PassThroughInputManager CreateInputManager() => new PassThroughInputManager();
+
+ protected override Playfield CreatePlayfield() => new TestPlayfield();
+ }
+
+ private class TestPlayfield : ScrollingPlayfield
+ {
+ public TestPlayfield()
+ {
+ AddInternal(new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0.2f,
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Padding = new MarginPadding { Top = 150 },
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.X,
+ Height = 2,
+ Colour = Color4.Green
+ },
+ HitObjectContainer
+ }
+ }
+ }
+ });
+ }
+ }
+
+ private class TestBeatmapConverter : BeatmapConverter
+ {
+ public TestBeatmapConverter(IBeatmap beatmap)
+ : base(beatmap)
+ {
+ }
+
+ protected override IEnumerable ValidConversionTypes => new[] { typeof(HitObject) };
+
+ protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap)
+ {
+ yield return new TestHitObject
+ {
+ StartTime = original.StartTime,
+ EndTime = (original as IHasEndTime)?.EndTime ?? (original.StartTime + 100)
+ };
+ }
+ }
+
+ #endregion
+
+ #region HitObject
+
+ private class TestHitObject : HitObject, IHasEndTime
+ {
+ public double EndTime { get; set; }
+
+ public double Duration => EndTime - StartTime;
+ }
+
+ private class DrawableTestHitObject : DrawableHitObject
+ {
+ public DrawableTestHitObject(TestHitObject hitObject)
+ : base(hitObject)
+ {
+ Anchor = Anchor.TopCentre;
+ Origin = Anchor.TopCentre;
+
+ Size = new Vector2(100, 25);
+
+ AddRangeInternal(new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.LightPink
+ },
+ new Box
+ {
+ Origin = Anchor.CentreLeft,
+ RelativeSizeAxes = Axes.X,
+ Height = 2,
+ Colour = Color4.Red
+ }
+ });
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
index 6c003e62ec..96dc864577 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("setup layout larger source", () =>
{
- Child = new LocalSkinOverrideContainer(new SizedSource(50))
+ Child = new SkinProvidingContainer(new SizedSource(50))
{
RelativeSizeAxes = Axes.Both,
Child = fill = new FillFlowContainer
@@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("setup layout larger source", () =>
{
- Child = new LocalSkinOverrideContainer(new SizedSource(30))
+ Child = new SkinProvidingContainer(new SizedSource(30))
{
RelativeSizeAxes = Axes.Both,
Child = fill = new FillFlowContainer
@@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual.Gameplay
Child = new SkinSourceContainer
{
RelativeSizeAxes = Axes.Both,
- Child = new LocalSkinOverrideContainer(secondarySource)
+ Child = new SkinProvidingContainer(secondarySource)
{
RelativeSizeAxes = Axes.Both,
Child = consumer = new SkinConsumer("test", name => new NamedBox("Default Implementation"), source => true)
@@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay
Child = new SkinSourceContainer
{
RelativeSizeAxes = Axes.Both,
- Child = target = new LocalSkinOverrideContainer(secondarySource)
+ Child = target = new SkinProvidingContainer(secondarySource)
{
RelativeSizeAxes = Axes.Both,
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
index 8f19df65a9..ee9e088dcc 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
@@ -176,6 +176,8 @@ namespace osu.Game.Tests.Visual.Online
HasVideo = true,
HasStoryboard = true,
Covers = new BeatmapSetOnlineCovers(),
+ Language = new BeatmapSetOnlineLanguage { Id = 3, Name = "English" },
+ Genre = new BeatmapSetOnlineGenre { Id = 4, Name = "Rock" },
},
Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() },
Beatmaps = new List
diff --git a/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs b/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
new file mode 100644
index 0000000000..325d657f0e
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneKudosuHistory.cs
@@ -0,0 +1,246 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Overlays.Profile.Sections.Kudosu;
+using System.Collections.Generic;
+using System;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Framework.Extensions.IEnumerableExtensions;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ public class TestSceneKudosuHistory : OsuTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(DrawableKudosuHistoryItem),
+ };
+
+ private readonly Box background;
+
+ public TestSceneKudosuHistory()
+ {
+ FillFlowContainer content;
+
+ AddRange(new Drawable[]
+ {
+ background = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ content = new FillFlowContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.X,
+ Width = 0.7f,
+ AutoSizeAxes = Axes.Y,
+ }
+ });
+
+ items.ForEach(t => content.Add(new DrawableKudosuHistoryItem(t)));
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ background.Colour = colours.GreySeafoam;
+ }
+
+ private readonly IEnumerable items = new[]
+ {
+ new APIKudosuHistory
+ {
+ Amount = 10,
+ CreatedAt = new DateTimeOffset(new DateTime(2011, 11, 11)),
+ Source = KudosuSource.DenyKudosu,
+ Action = KudosuAction.Reset,
+ Post = new APIKudosuHistory.ModdingPost
+ {
+ Title = @"Random post 1",
+ Url = @"https://osu.ppy.sh/b/1234",
+ },
+ Giver = new APIKudosuHistory.KudosuGiver
+ {
+ Username = @"Username1",
+ Url = @"https://osu.ppy.sh/u/1234"
+ }
+ },
+ new APIKudosuHistory
+ {
+ Amount = 5,
+ CreatedAt = new DateTimeOffset(new DateTime(2012, 10, 11)),
+ Source = KudosuSource.Forum,
+ Action = KudosuAction.Give,
+ Post = new APIKudosuHistory.ModdingPost
+ {
+ Title = @"Random post 2",
+ Url = @"https://osu.ppy.sh/b/1234",
+ },
+ Giver = new APIKudosuHistory.KudosuGiver
+ {
+ Username = @"Username2",
+ Url = @"https://osu.ppy.sh/u/1234"
+ }
+ },
+ new APIKudosuHistory
+ {
+ Amount = 8,
+ CreatedAt = new DateTimeOffset(new DateTime(2013, 9, 11)),
+ Source = KudosuSource.Forum,
+ Action = KudosuAction.Reset,
+ Post = new APIKudosuHistory.ModdingPost
+ {
+ Title = @"Random post 3",
+ Url = @"https://osu.ppy.sh/b/1234",
+ },
+ Giver = new APIKudosuHistory.KudosuGiver
+ {
+ Username = @"Username3",
+ Url = @"https://osu.ppy.sh/u/1234"
+ }
+ },
+ new APIKudosuHistory
+ {
+ Amount = 7,
+ CreatedAt = new DateTimeOffset(new DateTime(2014, 8, 11)),
+ Source = KudosuSource.Forum,
+ Action = KudosuAction.Revoke,
+ Post = new APIKudosuHistory.ModdingPost
+ {
+ Title = @"Random post 4",
+ Url = @"https://osu.ppy.sh/b/1234",
+ },
+ Giver = new APIKudosuHistory.KudosuGiver
+ {
+ Username = @"Username4",
+ Url = @"https://osu.ppy.sh/u/1234"
+ }
+ },
+ new APIKudosuHistory
+ {
+ Amount = 100,
+ CreatedAt = new DateTimeOffset(new DateTime(2015, 7, 11)),
+ Source = KudosuSource.Vote,
+ Action = KudosuAction.Give,
+ Post = new APIKudosuHistory.ModdingPost
+ {
+ Title = @"Random post 5",
+ Url = @"https://osu.ppy.sh/b/1234",
+ },
+ Giver = new APIKudosuHistory.KudosuGiver
+ {
+ Username = @"Username5",
+ Url = @"https://osu.ppy.sh/u/1234"
+ }
+ },
+ new APIKudosuHistory
+ {
+ Amount = 20,
+ CreatedAt = new DateTimeOffset(new DateTime(2016, 6, 11)),
+ Source = KudosuSource.Vote,
+ Action = KudosuAction.Reset,
+ Post = new APIKudosuHistory.ModdingPost
+ {
+ Title = @"Random post 6",
+ Url = @"https://osu.ppy.sh/b/1234",
+ },
+ Giver = new APIKudosuHistory.KudosuGiver
+ {
+ Username = @"Username6",
+ Url = @"https://osu.ppy.sh/u/1234"
+ }
+ },
+ new APIKudosuHistory
+ {
+ Amount = 11,
+ CreatedAt = new DateTimeOffset(new DateTime(2016, 6, 11)),
+ Source = KudosuSource.AllowKudosu,
+ Action = KudosuAction.Give,
+ Post = new APIKudosuHistory.ModdingPost
+ {
+ Title = @"Random post 7",
+ Url = @"https://osu.ppy.sh/b/1234",
+ },
+ Giver = new APIKudosuHistory.KudosuGiver
+ {
+ Username = @"Username7",
+ Url = @"https://osu.ppy.sh/u/1234"
+ }
+ },
+ new APIKudosuHistory
+ {
+ Amount = 24,
+ CreatedAt = new DateTimeOffset(new DateTime(2014, 6, 11)),
+ Source = KudosuSource.Delete,
+ Action = KudosuAction.Reset,
+ Post = new APIKudosuHistory.ModdingPost
+ {
+ Title = @"Random post 8",
+ Url = @"https://osu.ppy.sh/b/1234",
+ },
+ Giver = new APIKudosuHistory.KudosuGiver
+ {
+ Username = @"Username8",
+ Url = @"https://osu.ppy.sh/u/1234"
+ }
+ },
+ new APIKudosuHistory
+ {
+ Amount = 12,
+ CreatedAt = new DateTimeOffset(new DateTime(2016, 6, 11)),
+ Source = KudosuSource.Restore,
+ Action = KudosuAction.Give,
+ Post = new APIKudosuHistory.ModdingPost
+ {
+ Title = @"Random post 9",
+ Url = @"https://osu.ppy.sh/b/1234",
+ },
+ Giver = new APIKudosuHistory.KudosuGiver
+ {
+ Username = @"Username9",
+ Url = @"https://osu.ppy.sh/u/1234"
+ }
+ },
+ new APIKudosuHistory
+ {
+ Amount = 2,
+ CreatedAt = new DateTimeOffset(new DateTime(2012, 6, 11)),
+ Source = KudosuSource.Recalculate,
+ Action = KudosuAction.Give,
+ Post = new APIKudosuHistory.ModdingPost
+ {
+ Title = @"Random post 10",
+ Url = @"https://osu.ppy.sh/b/1234",
+ },
+ Giver = new APIKudosuHistory.KudosuGiver
+ {
+ Username = @"Username10",
+ Url = @"https://osu.ppy.sh/u/1234"
+ }
+ },
+ new APIKudosuHistory
+ {
+ Amount = 32,
+ CreatedAt = new DateTimeOffset(new DateTime(2019, 8, 11)),
+ Source = KudosuSource.Recalculate,
+ Action = KudosuAction.Reset,
+ Post = new APIKudosuHistory.ModdingPost
+ {
+ Title = @"Random post 11",
+ Url = @"https://osu.ppy.sh/b/1234",
+ },
+ Giver = new APIKudosuHistory.KudosuGiver
+ {
+ Username = @"Username11",
+ Url = @"https://osu.ppy.sh/u/1234"
+ }
+ }
+ };
+ }
+}
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index 4a9d88f3a6..84f67c9319 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
index 2a8bd393da..bba3c92245 100644
--- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
+++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
@@ -7,7 +7,7 @@
-
+
WinExe
diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs
index a09a1bb9cb..5435e86dfd 100644
--- a/osu.Game/Beatmaps/Beatmap.cs
+++ b/osu.Game/Beatmaps/Beatmap.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Beatmaps
///
/// A Beatmap containing converted HitObjects.
///
- public class Beatmap : IBeatmap
+ public class Beatmap : IBeatmap
where T : HitObject
{
public BeatmapInfo BeatmapInfo { get; set; } = new BeatmapInfo
@@ -36,17 +36,13 @@ namespace osu.Game.Beatmaps
public List Breaks { get; set; } = new List();
- ///
- /// Total amount of break time in the beatmap.
- ///
[JsonIgnore]
public double TotalBreakTime => Breaks.Sum(b => b.Duration);
- ///
- /// The HitObjects this Beatmap contains.
- ///
[JsonConverter(typeof(TypedListConverter))]
- public List HitObjects = new List();
+ public List HitObjects { get; set; } = new List();
+
+ IReadOnlyList IBeatmap.HitObjects => HitObjects;
IReadOnlyList IBeatmap.HitObjects => HitObjects;
diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs
index 2d8a0b1249..5bbffc2f77 100644
--- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs
@@ -136,7 +136,7 @@ namespace osu.Game.Beatmaps
return storyboard;
}
- protected override Skin GetSkin()
+ protected override ISkin GetSkin()
{
try
{
diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs b/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs
index df3a45d1cc..06dee4d3f5 100644
--- a/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs
@@ -75,6 +75,28 @@ namespace osu.Game.Beatmaps
/// The availability of this beatmap set.
///
public BeatmapSetOnlineAvailability Availability { get; set; }
+
+ ///
+ /// The song genre of this beatmap set.
+ ///
+ public BeatmapSetOnlineGenre Genre { get; set; }
+
+ ///
+ /// The song language of this beatmap set.
+ ///
+ public BeatmapSetOnlineLanguage Language { get; set; }
+ }
+
+ public class BeatmapSetOnlineGenre
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ }
+
+ public class BeatmapSetOnlineLanguage
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
}
public class BeatmapSetOnlineCovers
diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
index e5815a3f3b..ccb8a92b3a 100644
--- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
@@ -14,6 +14,8 @@ namespace osu.Game.Beatmaps.ControlPoints
///
public TimeSignatures TimeSignature = TimeSignatures.SimpleQuadruple;
+ public const double DEFAULT_BEAT_LENGTH = 1000;
+
///
/// The beat length at this control point.
///
@@ -23,7 +25,7 @@ namespace osu.Game.Beatmaps.ControlPoints
set => beatLength = MathHelper.Clamp(value, 6, 60000);
}
- private double beatLength = 1000;
+ private double beatLength = DEFAULT_BEAT_LENGTH;
public bool Equals(TimingControlPoint other)
=> base.Equals(other)
diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
index 3a4c677bd1..29ade24328 100644
--- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
@@ -54,7 +54,7 @@ namespace osu.Game.Beatmaps
{
public override IEnumerable GetModsFor(ModType type) => new Mod[] { };
- public override DrawableRuleset CreateDrawableRulesetWith(WorkingBeatmap beatmap, IReadOnlyList mods)
+ public override DrawableRuleset CreateDrawableRulesetWith(IWorkingBeatmap beatmap, IReadOnlyList mods)
{
throw new NotImplementedException();
}
diff --git a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs
index 540f616ea9..2c493254e0 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDifficultyCalculatorBeatmapDecoder.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Beatmaps.Formats
private class LegacyDifficultyCalculatorControlPoint : TimingControlPoint
{
- public override double BeatLength { get; set; } = 1000;
+ public override double BeatLength { get; set; } = DEFAULT_BEAT_LENGTH;
}
}
}
diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs
index 512fe25809..8f27e0b0e9 100644
--- a/osu.Game/Beatmaps/IBeatmap.cs
+++ b/osu.Game/Beatmaps/IBeatmap.cs
@@ -53,4 +53,13 @@ namespace osu.Game.Beatmaps
/// The shallow-cloned beatmap.
IBeatmap Clone();
}
+
+ public interface IBeatmap : IBeatmap
+ where T : HitObject
+ {
+ ///
+ /// The hitobjects contained by this beatmap.
+ ///
+ new IReadOnlyList HitObjects { get; }
+ }
}
diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs
new file mode 100644
index 0000000000..44071d9cc1
--- /dev/null
+++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs
@@ -0,0 +1,61 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Audio.Track;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.UI;
+using osu.Game.Skinning;
+using osu.Game.Storyboards;
+
+namespace osu.Game.Beatmaps
+{
+ public interface IWorkingBeatmap
+ {
+ ///
+ /// Retrieves the which this represents.
+ ///
+ IBeatmap Beatmap { get; }
+
+ ///
+ /// Retrieves the background for this .
+ ///
+ Texture Background { get; }
+
+ ///
+ /// Retrieves the audio track for this .
+ ///
+ Track Track { get; }
+
+ ///
+ /// Retrieves the for the of this .
+ ///
+ Waveform Waveform { get; }
+
+ ///
+ /// Retrieves the which this provides.
+ ///
+ Storyboard Storyboard { get; }
+
+ ///
+ /// Retrieves the which this provides.
+ ///
+ ISkin Skin { get; }
+
+ ///
+ /// Constructs a playable from using the applicable converters for a specific .
+ ///
+ /// The returned is in a playable state - all and s
+ /// have been applied, and s have been fully constructed.
+ ///
+ ///
+ /// The to create a playable for.
+ /// The s to apply to the .
+ /// The converted .
+ /// If could not be converted to .
+ IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods);
+ }
+}
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index 8605caa5fe..d8ab411beb 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -16,14 +16,13 @@ using osu.Framework.Audio;
using osu.Framework.Statistics;
using osu.Game.IO.Serialization;
using osu.Game.Rulesets;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
namespace osu.Game.Beatmaps
{
- public abstract class WorkingBeatmap : IDisposable
+ public abstract class WorkingBeatmap : IWorkingBeatmap, IDisposable
{
public readonly BeatmapInfo BeatmapInfo;
@@ -46,7 +45,7 @@ namespace osu.Game.Beatmaps
background = new RecyclableLazy(GetBackground, BackgroundStillValid);
waveform = new RecyclableLazy(GetWaveform);
storyboard = new RecyclableLazy(GetStoryboard);
- skin = new RecyclableLazy(GetSkin);
+ skin = new RecyclableLazy(GetSkin);
total_count.Value++;
}
@@ -97,17 +96,6 @@ namespace osu.Game.Beatmaps
/// The applicable .
protected virtual IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) => ruleset.CreateBeatmapConverter(beatmap);
- ///
- /// Constructs a playable from using the applicable converters for a specific .
- ///
- /// The returned is in a playable state - all and s
- /// have been applied, and s have been fully constructed.
- ///
- ///
- /// The to create a playable for.
- /// The s to apply to the .
- /// The converted .
- /// If could not be converted to .
public IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods)
{
var rulesetInstance = ruleset.CreateInstance();
@@ -214,10 +202,10 @@ namespace osu.Game.Beatmaps
private readonly RecyclableLazy storyboard;
public bool SkinLoaded => skin.IsResultAvailable;
- public Skin Skin => skin.Value;
+ public ISkin Skin => skin.Value;
- protected virtual Skin GetSkin() => new DefaultSkin();
- private readonly RecyclableLazy skin;
+ protected virtual ISkin GetSkin() => new DefaultSkin();
+ private readonly RecyclableLazy skin;
///
/// Transfer pieces of a beatmap to a new one, where possible, to save on loading.
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index b13e115387..0cecbb225f 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Configuration
{
// UI/selection defaults
Set(OsuSetting.Ruleset, 0, 0, int.MaxValue);
- Set(OsuSetting.Skin, 0, 0, int.MaxValue);
+ Set(OsuSetting.Skin, 0, -1, int.MaxValue);
Set(OsuSetting.BeatmapDetailTab, BeatmapDetailTab.Details);
@@ -83,6 +83,7 @@ namespace osu.Game.Configuration
Set(OsuSetting.ShowInterface, true);
Set(OsuSetting.ShowHealthDisplayWhenCantFail, true);
Set(OsuSetting.KeyOverlay, false);
+ Set(OsuSetting.ScoreMeter, ScoreMeterType.HitErrorBoth);
Set(OsuSetting.FloatingComments, false);
@@ -136,6 +137,7 @@ namespace osu.Game.Configuration
BlurLevel,
ShowStoryboard,
KeyOverlay,
+ ScoreMeter,
FloatingComments,
ShowInterface,
ShowHealthDisplayWhenCantFail,
diff --git a/osu.Game/Configuration/ScoreMeterType.cs b/osu.Game/Configuration/ScoreMeterType.cs
index 21a63fb3ed..b85ef9309d 100644
--- a/osu.Game/Configuration/ScoreMeterType.cs
+++ b/osu.Game/Configuration/ScoreMeterType.cs
@@ -1,12 +1,22 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.ComponentModel;
+
namespace osu.Game.Configuration
{
public enum ScoreMeterType
{
+ [Description("None")]
None,
- Colour,
- Error
+
+ [Description("Hit Error (left)")]
+ HitErrorLeft,
+
+ [Description("Hit Error (right)")]
+ HitErrorRight,
+
+ [Description("Hit Error (both)")]
+ HitErrorBoth,
}
}
diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs
index 0f7b26835b..9c948d6f90 100644
--- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs
+++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs
@@ -62,15 +62,23 @@ namespace osu.Game.Graphics.Containers
protected override bool OnClick(ClickEvent e)
{
- if (!base.ReceivePositionalInputAt(e.ScreenSpaceMousePosition))
- {
- Hide();
- return true;
- }
+ closeIfOutside(e);
return base.OnClick(e);
}
+ protected override bool OnDragEnd(DragEndEvent e)
+ {
+ closeIfOutside(e);
+ return base.OnDragEnd(e);
+ }
+
+ private void closeIfOutside(MouseEvent e)
+ {
+ if (!base.ReceivePositionalInputAt(e.ScreenSpaceMousePosition))
+ Hide();
+ }
+
public virtual bool OnPressed(GlobalAction action)
{
switch (action)
diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs
index 125c994c92..533f02af7b 100644
--- a/osu.Game/Graphics/DrawableDate.cs
+++ b/osu.Game/Graphics/DrawableDate.cs
@@ -2,11 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Game.Graphics.Sprites;
+using osu.Game.Utils;
namespace osu.Game.Graphics
{
@@ -71,7 +71,7 @@ namespace osu.Game.Graphics
Scheduler.AddDelayed(updateTimeWithReschedule, timeUntilNextUpdate);
}
- protected virtual string Format() => Date.Humanize();
+ protected virtual string Format() => HumanizerUtils.Humanize(Date);
private void updateTime() => Text = Format();
diff --git a/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs b/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs
new file mode 100644
index 0000000000..e90e297672
--- /dev/null
+++ b/osu.Game/Online/API/Requests/GetUserKudosuHistoryRequest.cs
@@ -0,0 +1,21 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Online.API.Requests.Responses;
+
+namespace osu.Game.Online.API.Requests
+{
+ public class GetUserKudosuHistoryRequest : PaginatedAPIRequest>
+ {
+ private readonly long userId;
+
+ public GetUserKudosuHistoryRequest(long userId, int page = 0, int itemsPerPage = 5)
+ : base(page, itemsPerPage)
+ {
+ this.userId = userId;
+ }
+
+ protected override string Target => $"users/{userId}/kudosu";
+ }
+}
diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs
index e5bfde8f8f..1ca14256e5 100644
--- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs
@@ -69,6 +69,12 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"availability")]
private BeatmapSetOnlineAvailability availability { get; set; }
+ [JsonProperty(@"genre")]
+ private BeatmapSetOnlineGenre genre { get; set; }
+
+ [JsonProperty(@"language")]
+ private BeatmapSetOnlineLanguage language { get; set; }
+
[JsonProperty(@"beatmaps")]
private IEnumerable beatmaps { get; set; }
@@ -95,6 +101,8 @@ namespace osu.Game.Online.API.Requests.Responses
LastUpdated = lastUpdated,
Availability = availability,
HasFavourited = hasFavourited,
+ Genre = genre,
+ Language = language
},
Beatmaps = beatmaps?.Select(b => b.ToBeatmap(rulesets)).ToList(),
};
diff --git a/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs b/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
new file mode 100644
index 0000000000..d596ddc560
--- /dev/null
+++ b/osu.Game/Online/API/Requests/Responses/APIKudosuHistory.cs
@@ -0,0 +1,83 @@
+// 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 Newtonsoft.Json;
+
+namespace osu.Game.Online.API.Requests.Responses
+{
+ public class APIKudosuHistory
+ {
+ [JsonProperty("created_at")]
+ public DateTimeOffset CreatedAt;
+
+ [JsonProperty("amount")]
+ public int Amount;
+
+ [JsonProperty("post")]
+ public ModdingPost Post;
+
+ public class ModdingPost
+ {
+ [JsonProperty("url")]
+ public string Url;
+
+ [JsonProperty("title")]
+ public string Title;
+ }
+
+ [JsonProperty("giver")]
+ public KudosuGiver Giver;
+
+ public class KudosuGiver
+ {
+ [JsonProperty("url")]
+ public string Url;
+
+ [JsonProperty("username")]
+ public string Username;
+ }
+
+ public KudosuSource Source;
+
+ public KudosuAction Action;
+
+ [JsonProperty("action")]
+ private string action
+ {
+ set
+ {
+ // incoming action may contain a prefix. if it doesn't, it's a legacy forum event.
+
+ string[] split = value.Split('.');
+
+ if (split.Length > 1)
+ Enum.TryParse(split.First().Replace("_", ""), true, out Source);
+ else
+ Source = KudosuSource.Forum;
+
+ Enum.TryParse(split.Last(), true, out Action);
+ }
+ }
+ }
+
+ public enum KudosuSource
+ {
+ Unknown,
+ AllowKudosu,
+ Delete,
+ DenyKudosu,
+ Forum,
+ Recalculate,
+ Restore,
+ Vote
+ }
+
+ public enum KudosuAction
+ {
+ Give,
+ Reset,
+ Revoke,
+ }
+}
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 0e804ecbaf..8fa8ffaf9b 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -182,7 +182,26 @@ namespace osu.Game
// bind config int to database SkinInfo
configSkin = LocalConfig.GetBindable(OsuSetting.Skin);
SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID;
- configSkin.ValueChanged += skinId => SkinManager.CurrentSkinInfo.Value = SkinManager.Query(s => s.ID == skinId.NewValue) ?? SkinInfo.Default;
+ configSkin.ValueChanged += skinId =>
+ {
+ var skinInfo = SkinManager.Query(s => s.ID == skinId.NewValue);
+
+ if (skinInfo == null)
+ {
+ switch (skinId.NewValue)
+ {
+ case -1:
+ skinInfo = DefaultLegacySkin.Info;
+ break;
+
+ default:
+ skinInfo = SkinInfo.Default;
+ break;
+ }
+ }
+
+ SkinManager.CurrentSkinInfo.Value = skinInfo;
+ };
configSkin.TriggerChange();
IsActive.BindValueChanged(active => updateActiveState(active.NewValue), true);
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 076c9ada78..de8f316b06 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -158,7 +158,7 @@ namespace osu.Game
runMigrations();
- dependencies.Cache(SkinManager = new SkinManager(Host.Storage, contextFactory, Host, Audio));
+ dependencies.Cache(SkinManager = new SkinManager(Host.Storage, contextFactory, Host, Audio, new NamespacedResourceStore(Resources, "Skins/Legacy")));
dependencies.CacheAs(SkinManager);
API = new APIAccess(LocalConfig);
diff --git a/osu.Game/Overlays/BeatmapSet/Info.cs b/osu.Game/Overlays/BeatmapSet/Info.cs
index 44827f0a0c..16d6236051 100644
--- a/osu.Game/Overlays/BeatmapSet/Info.cs
+++ b/osu.Game/Overlays/BeatmapSet/Info.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Overlays.BeatmapSet
public Info()
{
- MetadataSection source, tags;
+ MetadataSection source, tags, genre, language;
RelativeSizeAxes = Axes.X;
Height = 220;
Masking = true;
@@ -83,11 +83,12 @@ namespace osu.Game.Overlays.BeatmapSet
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- LayoutDuration = transition_duration,
+ Direction = FillDirection.Full,
Children = new[]
{
source = new MetadataSection("Source"),
+ genre = new MetadataSection("Genre") { Width = 0.5f },
+ language = new MetadataSection("Language") { Width = 0.5f },
tags = new MetadataSection("Tags"),
},
},
@@ -119,6 +120,8 @@ namespace osu.Game.Overlays.BeatmapSet
{
source.Text = b.NewValue?.Metadata.Source ?? string.Empty;
tags.Text = b.NewValue?.Metadata.Tags ?? string.Empty;
+ genre.Text = b.NewValue?.OnlineInfo?.Genre?.Name ?? string.Empty;
+ language.Text = b.NewValue?.OnlineInfo?.Language?.Name ?? string.Empty;
};
}
@@ -139,7 +142,7 @@ namespace osu.Game.Overlays.BeatmapSet
{
if (string.IsNullOrEmpty(value))
{
- this.FadeOut(transition_duration);
+ Hide();
return;
}
@@ -149,12 +152,6 @@ namespace osu.Game.Overlays.BeatmapSet
}
}
- public Color4 TextColour
- {
- get => textFlow.Colour;
- set => textFlow.Colour = value;
- }
-
public MetadataSection(string title)
{
RelativeSizeAxes = Axes.X;
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs
index ffc39e5af2..38a909411a 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.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 Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -14,6 +13,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Online.Leaderboards;
using osu.Game.Scoring;
using osu.Game.Users.Drawables;
+using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
@@ -132,7 +132,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
{
avatar.User = value.User;
flag.Country = value.User.Country;
- date.Text = $@"achieved {value.Date.Humanize()}";
+ date.Text = $@"achieved {HumanizerUtils.Humanize(value.Date)}";
usernameText.Clear();
usernameText.AddUserLink(value.User);
diff --git a/osu.Game/Overlays/Chat/Selection/ChannelListItem.cs b/osu.Game/Overlays/Chat/Selection/ChannelListItem.cs
index 4d77e5f93d..cb0639d85d 100644
--- a/osu.Game/Overlays/Chat/Selection/ChannelListItem.cs
+++ b/osu.Game/Overlays/Chat/Selection/ChannelListItem.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Overlays.Chat.Selection
private Color4 topicColour;
private Color4 hoverColour;
- public IEnumerable FilterTerms => new[] { channel.Name };
+ public IEnumerable FilterTerms => new[] { channel.Name, channel.Topic };
public bool MatchingFilter
{
diff --git a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs
index 24ed0cc022..c6d96c5917 100644
--- a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs
@@ -296,7 +296,11 @@ namespace osu.Game.Overlays.Profile.Header.Components
this.MoveTo(pos, 200, Easing.OutQuint);
}
- protected override void PopIn() => this.FadeIn(200, Easing.OutQuint);
+ protected override void PopIn()
+ {
+ instantMove |= !IsPresent;
+ this.FadeIn(200, Easing.OutQuint);
+ }
protected override void PopOut() => this.FadeOut(200, Easing.OutQuint);
}
diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
new file mode 100644
index 0000000000..d0cfe9fa54
--- /dev/null
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/DrawableKudosuHistoryItem.cs
@@ -0,0 +1,147 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Chat;
+using System;
+using osuTK;
+
+namespace osu.Game.Overlays.Profile.Sections.Kudosu
+{
+ public class DrawableKudosuHistoryItem : CompositeDrawable
+ {
+ private const int height = 25;
+
+ [Resolved]
+ private OsuColour colours { get; set; }
+
+ private readonly APIKudosuHistory historyItem;
+ private readonly LinkFlowContainer linkFlowContainer;
+ private readonly DrawableDate date;
+
+ public DrawableKudosuHistoryItem(APIKudosuHistory historyItem)
+ {
+ this.historyItem = historyItem;
+
+ Height = height;
+ RelativeSizeAxes = Axes.X;
+ AddRangeInternal(new Drawable[]
+ {
+ linkFlowContainer = new LinkFlowContainer
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ AutoSizeAxes = Axes.Both,
+ Spacing = new Vector2(0, 3),
+ },
+ date = new DrawableDate(historyItem.CreatedAt)
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ }
+ });
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ date.Colour = colours.GreySeafoamLighter;
+ var formattedSource = MessageFormatter.FormatText(getString(historyItem));
+ linkFlowContainer.AddLinks(formattedSource.Text, formattedSource.Links);
+ }
+
+ private string getString(APIKudosuHistory item)
+ {
+ string amount = $"{Math.Abs(item.Amount)} kudosu";
+ string post = $"[{item.Post.Title}]({item.Post.Url})";
+
+ switch (item.Source)
+ {
+ case KudosuSource.AllowKudosu:
+ switch (item.Action)
+ {
+ case KudosuAction.Give:
+ return $"Received {amount} from kudosu deny repeal of modding post {post}";
+ }
+
+ break;
+
+ case KudosuSource.DenyKudosu:
+ switch (item.Action)
+ {
+ case KudosuAction.Reset:
+ return $"Denied {amount} from modding post {post}";
+ }
+
+ break;
+
+ case KudosuSource.Delete:
+ switch (item.Action)
+ {
+ case KudosuAction.Reset:
+ return $"Lost {amount} from modding post deletion of {post}";
+ }
+
+ break;
+
+ case KudosuSource.Restore:
+ switch (item.Action)
+ {
+ case KudosuAction.Give:
+ return $"Received {amount} from modding post restoration of {post}";
+ }
+
+ break;
+
+ case KudosuSource.Vote:
+ switch (item.Action)
+ {
+ case KudosuAction.Give:
+ return $"Received {amount} from obtaining votes in modding post of {post}";
+
+ case KudosuAction.Reset:
+ return $"Lost {amount} from losing votes in modding post of {post}";
+ }
+
+ break;
+
+ case KudosuSource.Recalculate:
+ switch (item.Action)
+ {
+ case KudosuAction.Give:
+ return $"Received {amount} from votes recalculation in modding post of {post}";
+
+ case KudosuAction.Reset:
+ return $"Lost {amount} from votes recalculation in modding post of {post}";
+ }
+
+ break;
+
+ case KudosuSource.Forum:
+
+ string giver = $"[{item.Giver?.Username}]({item.Giver?.Url})";
+
+ switch (historyItem.Action)
+ {
+ case KudosuAction.Give:
+ return $"Received {amount} from {giver} for a post at {post}";
+
+ case KudosuAction.Reset:
+ return $"Kudosu reset by {giver} for the post {post}";
+
+ case KudosuAction.Revoke:
+ return $"Denied kudosu by {giver} for the post {post}";
+ }
+
+ break;
+ }
+
+ return $"Unknown event ({amount} change)";
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs
new file mode 100644
index 0000000000..0e7cfc37c0
--- /dev/null
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs
@@ -0,0 +1,27 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Online.API.Requests;
+using osu.Game.Users;
+using osu.Framework.Bindables;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.API;
+using System.Collections.Generic;
+
+namespace osu.Game.Overlays.Profile.Sections.Kudosu
+{
+ public class PaginatedKudosuHistoryContainer : PaginatedContainer
+ {
+ public PaginatedKudosuHistoryContainer(Bindable user, string header, string missing)
+ : base(user, header, missing)
+ {
+ ItemsPerPage = 5;
+ }
+
+ protected override APIRequest> CreateRequest()
+ => new GetUserKudosuHistoryRequest(User.Value.Id, VisiblePages++, ItemsPerPage);
+
+ protected override Drawable CreateDrawableItem(APIKudosuHistory item) => new DrawableKudosuHistoryItem(item);
+ }
+}
diff --git a/osu.Game/Overlays/Profile/Sections/KudosuSection.cs b/osu.Game/Overlays/Profile/Sections/KudosuSection.cs
index a17b68933c..9ccce7d837 100644
--- a/osu.Game/Overlays/Profile/Sections/KudosuSection.cs
+++ b/osu.Game/Overlays/Profile/Sections/KudosuSection.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Graphics;
using osu.Game.Overlays.Profile.Sections.Kudosu;
namespace osu.Game.Overlays.Profile.Sections
@@ -13,9 +14,10 @@ namespace osu.Game.Overlays.Profile.Sections
public KudosuSection()
{
- Children = new[]
+ Children = new Drawable[]
{
new KudosuInfo(User),
+ new PaginatedKudosuHistoryContainer(User, null, @"This user hasn't received any kudosu!"),
};
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
index 9142492610..520a8852b3 100644
--- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs
@@ -44,6 +44,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
LabelText = "Always show key overlay",
Bindable = config.GetBindable(OsuSetting.KeyOverlay)
},
+ new SettingsEnumDropdown
+ {
+ LabelText = "Score meter type",
+ Bindable = config.GetBindable(OsuSetting.ScoreMeter)
+ },
new SettingsEnumDropdown
{
LabelText = "Score display mode",
diff --git a/osu.Game/Rulesets/Edit/DrawableEditRuleset.cs b/osu.Game/Rulesets/Edit/DrawableEditRuleset.cs
deleted file mode 100644
index e85ebb5f3a..0000000000
--- a/osu.Game/Rulesets/Edit/DrawableEditRuleset.cs
+++ /dev/null
@@ -1,116 +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.Linq;
-using osu.Framework.Allocation;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Beatmaps;
-using osu.Game.Rulesets.Objects;
-using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.UI;
-
-namespace osu.Game.Rulesets.Edit
-{
- public abstract class DrawableEditRuleset : CompositeDrawable
- {
- ///
- /// The contained by this .
- ///
- public abstract Playfield Playfield { get; }
-
- public abstract PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer();
-
- internal DrawableEditRuleset()
- {
- RelativeSizeAxes = Axes.Both;
- }
-
- ///
- /// Adds a to the and displays a visual representation of it.
- ///
- /// The to add.
- /// The visual representation of .
- internal abstract DrawableHitObject Add(HitObject hitObject);
-
- ///
- /// Removes a from the and the display.
- ///
- /// The to remove.
- /// The visual representation of the removed .
- internal abstract DrawableHitObject Remove(HitObject hitObject);
- }
-
- public class DrawableEditRuleset : DrawableEditRuleset
- where TObject : HitObject
- {
- public override Playfield Playfield => drawableRuleset.Playfield;
-
- public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => drawableRuleset.CreatePlayfieldAdjustmentContainer();
-
- private Ruleset ruleset => drawableRuleset.Ruleset;
- private Beatmap beatmap => drawableRuleset.Beatmap;
-
- private readonly DrawableRuleset drawableRuleset;
-
- public DrawableEditRuleset(DrawableRuleset drawableRuleset)
- {
- this.drawableRuleset = drawableRuleset;
-
- InternalChild = drawableRuleset;
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- drawableRuleset.FrameStablePlayback = false;
- Playfield.DisplayJudgements.Value = false;
- }
-
- internal override DrawableHitObject Add(HitObject hitObject)
- {
- var tObject = (TObject)hitObject;
-
- // Add to beatmap, preserving sorting order
- var insertionIndex = beatmap.HitObjects.FindLastIndex(h => h.StartTime <= hitObject.StartTime);
- beatmap.HitObjects.Insert(insertionIndex + 1, tObject);
-
- // Process object
- var processor = ruleset.CreateBeatmapProcessor(beatmap);
-
- processor?.PreProcess();
- tObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty);
- processor?.PostProcess();
-
- // Add visual representation
- var drawableObject = drawableRuleset.CreateDrawableRepresentation(tObject);
-
- drawableRuleset.Playfield.Add(drawableObject);
- drawableRuleset.Playfield.PostProcess();
-
- return drawableObject;
- }
-
- internal override DrawableHitObject Remove(HitObject hitObject)
- {
- var tObject = (TObject)hitObject;
-
- // Remove from beatmap
- beatmap.HitObjects.Remove(tObject);
-
- // Process the beatmap
- var processor = ruleset.CreateBeatmapProcessor(beatmap);
-
- processor?.PreProcess();
- processor?.PostProcess();
-
- // Remove visual representation
- var drawableObject = Playfield.AllHitObjects.Single(d => d.HitObject == hitObject);
-
- drawableRuleset.Playfield.Remove(drawableObject);
- drawableRuleset.Playfield.PostProcess();
-
- return drawableObject;
- }
- }
-}
diff --git a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs
new file mode 100644
index 0000000000..af565f8896
--- /dev/null
+++ b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs
@@ -0,0 +1,80 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Edit;
+
+namespace osu.Game.Rulesets.Edit
+{
+ ///
+ /// A wrapper for a . Handles adding visual representations of s to the underlying .
+ ///
+ internal class DrawableEditRulesetWrapper : CompositeDrawable
+ where TObject : HitObject
+ {
+ public Playfield Playfield => drawableRuleset.Playfield;
+
+ private readonly DrawableRuleset drawableRuleset;
+
+ [Resolved]
+ private IEditorBeatmap beatmap { get; set; }
+
+ public DrawableEditRulesetWrapper(DrawableRuleset drawableRuleset)
+ {
+ this.drawableRuleset = drawableRuleset;
+
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChild = drawableRuleset;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ drawableRuleset.FrameStablePlayback = false;
+ Playfield.DisplayJudgements.Value = false;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ beatmap.HitObjectAdded += addHitObject;
+ beatmap.HitObjectRemoved += removeHitObject;
+ }
+
+ private void addHitObject(HitObject hitObject)
+ {
+ var drawableObject = drawableRuleset.CreateDrawableRepresentation((TObject)hitObject);
+
+ drawableRuleset.Playfield.Add(drawableObject);
+ drawableRuleset.Playfield.PostProcess();
+ }
+
+ private void removeHitObject(HitObject hitObject)
+ {
+ var drawableObject = Playfield.AllHitObjects.Single(d => d.HitObject == hitObject);
+
+ drawableRuleset.Playfield.Remove(drawableObject);
+ drawableRuleset.Playfield.PostProcess();
+ }
+
+ public PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => drawableRuleset.CreatePlayfieldAdjustmentContainer();
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (beatmap != null)
+ {
+ beatmap.HitObjectAdded -= addHitObject;
+ beatmap.HitObjectRemoved -= removeHitObject;
+ }
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index 38ec09535d..fc324d7021 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -18,45 +18,47 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.RadioButtons;
+using osu.Game.Screens.Edit.Compose;
using osu.Game.Screens.Edit.Compose.Components;
namespace osu.Game.Rulesets.Edit
{
- public abstract class HitObjectComposer : CompositeDrawable
+ [Cached(Type = typeof(IPlacementHandler))]
+ public abstract class HitObjectComposer : HitObjectComposer, IPlacementHandler
+ where TObject : HitObject
{
- public IEnumerable HitObjects => DrawableRuleset.Playfield.AllHitObjects;
+ protected IRulesetConfigManager Config { get; private set; }
protected readonly Ruleset Ruleset;
- protected readonly IBindable Beatmap = new Bindable();
-
- protected IRulesetConfigManager Config { get; private set; }
-
- private readonly List layerContainers = new List();
-
- protected DrawableEditRuleset DrawableRuleset { get; private set; }
+ private IWorkingBeatmap workingBeatmap;
+ private Beatmap playableBeatmap;
+ private EditorBeatmap editorBeatmap;
+ private IBeatmapProcessor beatmapProcessor;
+ private DrawableEditRulesetWrapper drawableRulesetWrapper;
private BlueprintContainer blueprintContainer;
+ private readonly List layerContainers = new List();
private InputManager inputManager;
- internal HitObjectComposer(Ruleset ruleset)
+ protected HitObjectComposer(Ruleset ruleset)
{
Ruleset = ruleset;
-
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
- private void load(IBindable beatmap, IFrameBasedClock framedClock)
+ private void load(IFrameBasedClock framedClock)
{
- Beatmap.BindTo(beatmap);
-
try
{
- DrawableRuleset = CreateDrawableRuleset();
- DrawableRuleset.Clock = framedClock;
+ drawableRulesetWrapper = new DrawableEditRulesetWrapper(CreateDrawableRuleset(Ruleset, workingBeatmap, Array.Empty()))
+ {
+ Clock = framedClock
+ };
}
catch (Exception e)
{
@@ -64,10 +66,10 @@ namespace osu.Game.Rulesets.Edit
return;
}
- var layerBelowRuleset = DrawableRuleset.CreatePlayfieldAdjustmentContainer();
+ var layerBelowRuleset = drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer();
layerBelowRuleset.Child = new EditorPlayfieldBorder { RelativeSizeAxes = Axes.Both };
- var layerAboveRuleset = DrawableRuleset.CreatePlayfieldAdjustmentContainer();
+ var layerAboveRuleset = drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer();
layerAboveRuleset.Child = blueprintContainer = new BlueprintContainer();
layerContainers.Add(layerBelowRuleset);
@@ -98,7 +100,7 @@ namespace osu.Game.Rulesets.Edit
Children = new Drawable[]
{
layerBelowRuleset,
- DrawableRuleset,
+ drawableRulesetWrapper,
layerAboveRuleset
}
}
@@ -118,6 +120,28 @@ namespace osu.Game.Rulesets.Edit
toolboxCollection.Items[0].Select();
}
+ protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
+ {
+ var parentWorkingBeatmap = parent.Get>().Value;
+
+ playableBeatmap = (Beatmap)parentWorkingBeatmap.GetPlayableBeatmap(Ruleset.RulesetInfo, Array.Empty());
+ workingBeatmap = new EditorWorkingBeatmap(playableBeatmap, parentWorkingBeatmap);
+
+ beatmapProcessor = Ruleset.CreateBeatmapProcessor(playableBeatmap);
+
+ editorBeatmap = new EditorBeatmap(playableBeatmap);
+ editorBeatmap.HitObjectAdded += addHitObject;
+ editorBeatmap.HitObjectRemoved += removeHitObject;
+
+ var dependencies = new DependencyContainer(parent);
+ dependencies.CacheAs(editorBeatmap);
+ dependencies.CacheAs>(editorBeatmap);
+
+ Config = dependencies.Get().GetConfigFor(Ruleset);
+
+ return base.CreateChildDependencies(dependencies);
+ }
+
protected override void LoadComplete()
{
base.LoadComplete();
@@ -125,45 +149,76 @@ namespace osu.Game.Rulesets.Edit
inputManager = GetContainingInputManager();
}
- protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
- {
- var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
-
- dependencies.CacheAs(this);
- Config = dependencies.Get().GetConfigFor(Ruleset);
-
- return dependencies;
- }
-
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
layerContainers.ForEach(l =>
{
- l.Anchor = DrawableRuleset.Playfield.Anchor;
- l.Origin = DrawableRuleset.Playfield.Origin;
- l.Position = DrawableRuleset.Playfield.Position;
- l.Size = DrawableRuleset.Playfield.Size;
+ l.Anchor = drawableRulesetWrapper.Playfield.Anchor;
+ l.Origin = drawableRulesetWrapper.Playfield.Origin;
+ l.Position = drawableRulesetWrapper.Playfield.Position;
+ l.Size = drawableRulesetWrapper.Playfield.Size;
});
}
+ private void addHitObject(HitObject hitObject)
+ {
+ beatmapProcessor?.PreProcess();
+ hitObject.ApplyDefaults(playableBeatmap.ControlPointInfo, playableBeatmap.BeatmapInfo.BaseDifficulty);
+ beatmapProcessor?.PostProcess();
+ }
+
+ private void removeHitObject(HitObject hitObject)
+ {
+ beatmapProcessor?.PreProcess();
+ beatmapProcessor?.PostProcess();
+ }
+
+ public override IEnumerable HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects;
+ public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position);
+
+ protected abstract IReadOnlyList CompositionTools { get; }
+
+ protected abstract DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IWorkingBeatmap beatmap, IReadOnlyList mods);
+
+ public void BeginPlacement(HitObject hitObject)
+ {
+ }
+
+ public void EndPlacement(HitObject hitObject) => editorBeatmap.Add(hitObject);
+
+ public void Delete(HitObject hitObject) => editorBeatmap.Remove(hitObject);
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (editorBeatmap != null)
+ {
+ editorBeatmap.HitObjectAdded -= addHitObject;
+ editorBeatmap.HitObjectRemoved -= removeHitObject;
+ }
+ }
+ }
+
+ [Cached(typeof(HitObjectComposer))]
+ public abstract class HitObjectComposer : CompositeDrawable
+ {
+ internal HitObjectComposer()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ ///
+ /// All the s.
+ ///
+ public abstract IEnumerable HitObjects { get; }
+
///
/// Whether the user's cursor is currently in an area of the that is valid for placement.
///
- 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(DrawableRuleset.Add(hitObject));
-
- public void Remove(HitObject hitObject) => blueprintContainer.RemoveBlueprintFor(DrawableRuleset.Remove(hitObject));
-
- internal abstract DrawableEditRuleset CreateDrawableRuleset();
-
- protected abstract IReadOnlyList CompositionTools { get; }
+ public abstract bool CursorInPlacementArea { get; }
///
/// Creates a for a specific .
@@ -176,18 +231,4 @@ namespace osu.Game.Rulesets.Edit
///
public virtual SelectionHandler CreateSelectionHandler() => new SelectionHandler();
}
-
- public abstract class HitObjectComposer : HitObjectComposer
- where TObject : HitObject
- {
- protected HitObjectComposer(Ruleset ruleset)
- : base(ruleset)
- {
- }
-
- internal override DrawableEditRuleset CreateDrawableRuleset()
- => new DrawableEditRuleset(CreateDrawableRuleset(Ruleset, Beatmap.Value, Array.Empty()));
-
- protected abstract DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, WorkingBeatmap beatmap, IReadOnlyList mods);
- }
}
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index 80e70589bd..a24476418c 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -7,7 +7,9 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions;
+using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
+using osu.Framework.Threading;
using osu.Game.Audio;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
@@ -186,6 +188,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
///
/// Apply (generally fade-in) transforms leading into the start time.
/// The local drawable hierarchy is recursively delayed to for convenience.
+ ///
+ /// By default this will fade in the object from zero with no duration.
///
///
/// This is called once before every . This is to ensure a good state in the case
@@ -193,6 +197,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
///
protected virtual void UpdateInitialTransforms()
{
+ this.FadeInFromZero();
}
///
@@ -274,6 +279,14 @@ namespace osu.Game.Rulesets.Objects.Drawables
UpdateResult(false);
}
+ ///
+ /// Schedules an to this .
+ ///
+ ///
+ /// Only provided temporarily until hitobject pooling is implemented.
+ ///
+ protected internal new ScheduledDelegate Schedule(Action action) => base.Schedule(action);
+
private double? lifetimeStart;
public override double LifetimeStart
diff --git a/osu.Game/Rulesets/Objects/HitWindows.cs b/osu.Game/Rulesets/Objects/HitWindows.cs
index fe099aaee7..e88af67c7c 100644
--- a/osu.Game/Rulesets/Objects/HitWindows.cs
+++ b/osu.Game/Rulesets/Objects/HitWindows.cs
@@ -65,6 +65,19 @@ namespace osu.Game.Rulesets.Objects
return HitResult.None;
}
+ ///
+ /// Retrieves a mapping of s to their half window timing for all allowed s.
+ ///
+ ///
+ public IEnumerable<(HitResult result, double length)> GetAllAvailableHalfWindows()
+ {
+ for (var result = HitResult.Meh; result <= HitResult.Perfect; ++result)
+ {
+ if (IsHitResultAllowed(result))
+ yield return (result, HalfWindowFor(result));
+ }
+ }
+
///
/// Check whether it is possible to achieve the provided .
///
diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs
index 42b1322cae..b63292757d 100644
--- a/osu.Game/Rulesets/Ruleset.cs
+++ b/osu.Game/Rulesets/Ruleset.cs
@@ -18,6 +18,7 @@ using osu.Game.Configuration;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Scoring;
+using osu.Game.Skinning;
namespace osu.Game.Rulesets
{
@@ -44,6 +45,8 @@ namespace osu.Game.Rulesets
public ModAutoplay GetAutoplayMod() => GetAllMods().OfType().First();
+ public virtual ISkin CreateLegacySkinProvider(ISkinSource source) => null;
+
protected Ruleset(RulesetInfo rulesetInfo = null)
{
RulesetInfo = rulesetInfo ?? createRulesetInfo();
@@ -56,7 +59,7 @@ namespace osu.Game.Rulesets
/// The s to apply.
/// Unable to successfully load the beatmap to be usable with this ruleset.
///
- public abstract DrawableRuleset CreateDrawableRulesetWith(WorkingBeatmap beatmap, IReadOnlyList mods);
+ public abstract DrawableRuleset CreateDrawableRulesetWith(IWorkingBeatmap beatmap, IReadOnlyList mods);
///
/// 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 e47df6b473..3b7e457990 100644
--- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs
@@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Scoring
///
/// Whether all s have been processed.
///
- protected virtual bool HasCompleted => false;
+ public virtual bool HasCompleted => false;
///
/// Whether this ScoreProcessor has already triggered the failed state.
@@ -205,7 +205,7 @@ namespace osu.Game.Rulesets.Scoring
private const double combo_portion = 0.7;
private const double max_score = 1000000;
- protected sealed override bool HasCompleted => JudgedHits == MaxHits;
+ public sealed override bool HasCompleted => JudgedHits == MaxHits;
protected int MaxHits { get; private set; }
protected int JudgedHits { get; private set; }
diff --git a/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs b/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs
index 9bab065d1e..4b3c3f90f0 100644
--- a/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs
+++ b/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs
@@ -20,7 +20,13 @@ namespace osu.Game.Rulesets.Timing
///
/// The aggregate multiplier which this provides.
///
- public double Multiplier => Velocity * DifficultyPoint.SpeedMultiplier * 1000 / TimingPoint.BeatLength;
+ public double Multiplier => Velocity * DifficultyPoint.SpeedMultiplier * BaseBeatLength / TimingPoint.BeatLength;
+
+ ///
+ /// The base beat length to scale the provided multiplier relative to.
+ ///
+ /// For a of 1000, a with a beat length of 500 will increase the multiplier by 2.
+ public double BaseBeatLength = TimingControlPoint.DEFAULT_BEAT_LENGTH;
///
/// The velocity multiplier.
diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs
index ccfd89adca..0ee9196fb8 100644
--- a/osu.Game/Rulesets/UI/DrawableRuleset.cs
+++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs
@@ -113,7 +113,7 @@ namespace osu.Game.Rulesets.UI
/// The ruleset being represented.
/// The beatmap to create the hit renderer for.
/// The s to apply.
- protected DrawableRuleset(Ruleset ruleset, WorkingBeatmap workingBeatmap, IReadOnlyList mods)
+ protected DrawableRuleset(Ruleset ruleset, IWorkingBeatmap workingBeatmap, IReadOnlyList mods)
: base(ruleset)
{
if (workingBeatmap == null)
@@ -215,10 +215,6 @@ namespace osu.Game.Rulesets.UI
continueResume();
}
- public ResumeOverlay ResumeOverlay { get; private set; }
-
- protected virtual ResumeOverlay CreateResumeOverlay() => null;
-
///
/// Creates and adds the visual representation of a to this .
///
@@ -389,6 +385,13 @@ namespace osu.Game.Rulesets.UI
///
public abstract GameplayCursorContainer Cursor { get; }
+ ///
+ /// An optional overlay used when resuming gameplay from a paused state.
+ ///
+ public ResumeOverlay ResumeOverlay { get; protected set; }
+
+ protected virtual ResumeOverlay CreateResumeOverlay() => null;
+
///
/// Sets a replay to be used, overriding local input.
///
diff --git a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
index 42ec0b79b9..64e491858b 100644
--- a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
@@ -69,6 +69,11 @@ namespace osu.Game.Rulesets.UI.Scrolling
///
protected virtual bool UserScrollSpeedAdjustment => true;
+ ///
+ /// Whether beat lengths should scale relative to the most common beat length in the .
+ ///
+ protected virtual bool RelativeScaleBeatLengths => false;
+
///
/// Provides the default s that adjust the scrolling rate of s
/// inside this .
@@ -81,7 +86,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
[Cached(Type = typeof(IScrollingInfo))]
private readonly LocalScrollingInfo scrollingInfo;
- protected DrawableScrollingRuleset(Ruleset ruleset, WorkingBeatmap beatmap, IReadOnlyList mods)
+ protected DrawableScrollingRuleset(Ruleset ruleset, IWorkingBeatmap beatmap, IReadOnlyList mods)
: base(ruleset, beatmap, mods)
{
scrollingInfo = new LocalScrollingInfo();
@@ -107,16 +112,38 @@ namespace osu.Game.Rulesets.UI.Scrolling
[BackgroundDependencyLoader]
private void load()
{
- // Calculate default multiplier control points
+ double lastObjectTime = (Objects.LastOrDefault() as IHasEndTime)?.EndTime ?? Objects.LastOrDefault()?.StartTime ?? double.MaxValue;
+ double baseBeatLength = TimingControlPoint.DEFAULT_BEAT_LENGTH;
+
+ if (RelativeScaleBeatLengths)
+ {
+ IReadOnlyList timingPoints = Beatmap.ControlPointInfo.TimingPoints;
+ double maxDuration = 0;
+
+ for (int i = 0; i < timingPoints.Count; i++)
+ {
+ if (timingPoints[i].Time > lastObjectTime)
+ break;
+
+ double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time : lastObjectTime;
+ double duration = endTime - timingPoints[i].Time;
+
+ if (duration > maxDuration)
+ {
+ maxDuration = duration;
+ baseBeatLength = timingPoints[i].BeatLength;
+ }
+ }
+ }
+
+ // Merge sequences of timing and difficulty control points to create the aggregate "multiplier" control point
var lastTimingPoint = new TimingControlPoint();
var lastDifficultyPoint = new DifficultyControlPoint();
-
- // Merge timing + difficulty points
var allPoints = new SortedList(Comparer.Default);
allPoints.AddRange(Beatmap.ControlPointInfo.TimingPoints);
allPoints.AddRange(Beatmap.ControlPointInfo.DifficultyPoints);
- // Generate the timing points, making non-timing changes use the previous timing change
+ // Generate the timing points, making non-timing changes use the previous timing change and vice-versa
var timingChanges = allPoints.Select(c =>
{
var timingPoint = c as TimingControlPoint;
@@ -131,14 +158,13 @@ namespace osu.Game.Rulesets.UI.Scrolling
return new MultiplierControlPoint(c.Time)
{
Velocity = Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier,
+ BaseBeatLength = baseBeatLength,
TimingPoint = lastTimingPoint,
DifficultyPoint = lastDifficultyPoint
};
});
- double lastObjectTime = (Objects.LastOrDefault() as IHasEndTime)?.EndTime ?? Objects.LastOrDefault()?.StartTime ?? double.MaxValue;
-
- // Perform some post processing of the timing changes
+ // Trim unwanted sequences of timing changes
timingChanges = timingChanges
// Collapse sections after the last hit object
.Where(s => s.StartTime <= lastObjectTime)
@@ -147,7 +173,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
controlPoints.AddRange(timingChanges);
- // If we have no control points, add a default one
if (controlPoints.Count == 0)
controlPoints.Add(new MultiplierControlPoint { Velocity = Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier });
}
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
index 1df8c8218f..bd1f496dfa 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.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.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
@@ -50,8 +51,13 @@ namespace osu.Game.Rulesets.UI.Scrolling
public override bool Remove(DrawableHitObject hitObject)
{
var result = base.Remove(hitObject);
+
if (result)
+ {
initialStateCache.Invalidate();
+ hitObjectInitialStateCache.Remove(hitObject);
+ }
+
return result;
}
@@ -86,13 +92,34 @@ namespace osu.Game.Rulesets.UI.Scrolling
scrollingInfo.Algorithm.Reset();
foreach (var obj in Objects)
+ {
+ computeLifetimeStartRecursive(obj);
computeInitialStateRecursive(obj);
+ }
+
initialStateCache.Validate();
}
}
- private void computeInitialStateRecursive(DrawableHitObject hitObject)
+ private void computeLifetimeStartRecursive(DrawableHitObject hitObject)
{
+ hitObject.LifetimeStart = scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, timeRange.Value);
+
+ foreach (var obj in hitObject.NestedHitObjects)
+ computeLifetimeStartRecursive(obj);
+ }
+
+ private readonly Dictionary hitObjectInitialStateCache = new Dictionary();
+
+ // Cant use AddOnce() since the delegate is re-constructed every invocation
+ private void computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() =>
+ {
+ if (!hitObjectInitialStateCache.TryGetValue(hitObject, out var cached))
+ cached = hitObjectInitialStateCache[hitObject] = new Cached();
+
+ if (cached.IsValid)
+ return;
+
double endTime = hitObject.HitObject.StartTime;
if (hitObject.HitObject is IHasEndTime e)
@@ -113,7 +140,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
}
}
- hitObject.LifetimeStart = scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, timeRange.Value);
hitObject.LifetimeEnd = scrollingInfo.Algorithm.TimeAt(scrollLength * safe_lifetime_end_multiplier, endTime, timeRange.Value, scrollLength);
foreach (var obj in hitObject.NestedHitObjects)
@@ -123,7 +149,9 @@ namespace osu.Game.Rulesets.UI.Scrolling
// Nested hitobjects don't need to scroll, but they do need accurate positions
updatePosition(obj, hitObject.HitObject.StartTime);
}
- }
+
+ cached.Validate();
+ });
protected override void UpdateAfterChildrenLife()
{
diff --git a/osu.Game/Screens/Edit/BindableBeatDivisor.cs b/osu.Game/Screens/Edit/BindableBeatDivisor.cs
index ea3b68e3bd..2aeb1ef04b 100644
--- a/osu.Game/Screens/Edit/BindableBeatDivisor.cs
+++ b/osu.Game/Screens/Edit/BindableBeatDivisor.cs
@@ -4,6 +4,9 @@
using System;
using System.Linq;
using osu.Framework.Bindables;
+using osu.Framework.Graphics.Colour;
+using osu.Game.Graphics;
+using osuTK.Graphics;
namespace osu.Game.Screens.Edit
{
@@ -35,5 +38,41 @@ namespace osu.Game.Screens.Edit
protected override int DefaultMinValue => VALID_DIVISORS.First();
protected override int DefaultMaxValue => VALID_DIVISORS.Last();
protected override int DefaultPrecision => 1;
+
+ ///
+ /// Retrieves the appropriate colour for a beat divisor.
+ ///
+ /// The beat divisor.
+ /// The set of colours.
+ /// The applicable colour from for .
+ public static ColourInfo GetColourFor(int beatDivisor, OsuColour colours)
+ {
+ switch (beatDivisor)
+ {
+ case 2:
+ return colours.BlueLight;
+
+ case 4:
+ return colours.Blue;
+
+ case 8:
+ return colours.BlueDarker;
+
+ case 16:
+ return colours.PurpleDark;
+
+ case 3:
+ return colours.YellowLight;
+
+ case 6:
+ return colours.Yellow;
+
+ case 12:
+ return colours.YellowDarker;
+
+ default:
+ return Color4.White;
+ }
+ }
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs
index 0d16d8474b..4d89e43ee5 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs
@@ -188,6 +188,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
private Marker marker;
+ [Resolved]
+ private OsuColour colours { get; set; }
+
private readonly BindableBeatDivisor beatDivisor;
private readonly int[] availableDivisors;
@@ -204,11 +207,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
foreach (var t in availableDivisors)
{
- AddInternal(new Tick(t)
+ AddInternal(new Tick
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopCentre,
RelativePositionAxes = Axes.X,
+ Colour = BindableBeatDivisor.GetColourFor(t, colours),
X = getMappedPosition(t)
});
}
@@ -284,11 +288,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
private class Tick : CompositeDrawable
{
- private readonly int divisor;
-
- public Tick(int divisor)
+ public Tick()
{
- this.divisor = divisor;
Size = new Vector2(2.5f, 10);
InternalChild = new Box { RelativeSizeAxes = Axes.Both };
@@ -296,42 +297,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
CornerRadius = 0.5f;
Masking = true;
}
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- Colour = getColourForDivisor(divisor, colours);
- }
-
- private ColourInfo getColourForDivisor(int divisor, OsuColour colours)
- {
- switch (divisor)
- {
- case 2:
- return colours.BlueLight;
-
- case 4:
- return colours.Blue;
-
- case 8:
- return colours.BlueDarker;
-
- case 16:
- return colours.PurpleDark;
-
- case 3:
- return colours.YellowLight;
-
- case 6:
- return colours.Yellow;
-
- case 12:
- return colours.YellowDarker;
-
- default:
- return Color4.White;
- }
- }
}
private class Marker : CompositeDrawable
diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
index a1e62cd38b..7d25fd5283 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
@@ -11,6 +11,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Input.States;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Screens.Edit.Compose.Components
@@ -29,6 +30,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
[Resolved]
private HitObjectComposer composer { get; set; }
+ [Resolved]
+ private IEditorBeatmap beatmap { get; set; }
+
public BlueprintContainer()
{
RelativeSizeAxes = Axes.Both;
@@ -53,7 +57,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
};
foreach (var obj in composer.HitObjects)
- AddBlueprintFor(obj);
+ addBlueprintFor(obj);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ beatmap.HitObjectAdded += addBlueprintFor;
+ beatmap.HitObjectRemoved += removeBlueprintFor;
}
private HitObjectCompositionTool currentTool;
@@ -75,11 +87,32 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
}
- ///
- /// Adds a blueprint for a which adds movement support.
- ///
- /// The to create a blueprint for.
- public void AddBlueprintFor(DrawableHitObject hitObject)
+ private void addBlueprintFor(HitObject hitObject)
+ {
+ var drawable = composer.HitObjects.FirstOrDefault(d => d.HitObject == hitObject);
+ if (drawable == null)
+ return;
+
+ addBlueprintFor(drawable);
+ }
+
+ private void removeBlueprintFor(HitObject hitObject)
+ {
+ var blueprint = selectionBlueprints.Single(m => m.HitObject.HitObject == hitObject);
+ if (blueprint == null)
+ return;
+
+ blueprint.Deselect();
+
+ blueprint.Selected -= onBlueprintSelected;
+ blueprint.Deselected -= onBlueprintDeselected;
+ blueprint.SelectionRequested -= onSelectionRequested;
+ blueprint.DragRequested -= onDragRequested;
+
+ selectionBlueprints.Remove(blueprint);
+ }
+
+ private void addBlueprintFor(DrawableHitObject hitObject)
{
refreshTool();
@@ -95,25 +128,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
selectionBlueprints.Add(blueprint);
}
- ///
- /// Removes a blueprint for a .
- ///
- /// The for which to remove the blueprint.
- public void RemoveBlueprintFor(DrawableHitObject hitObject)
- {
- var blueprint = selectionBlueprints.Single(m => m.HitObject == hitObject);
- if (blueprint == null)
- return;
-
- blueprint.Deselect();
-
- blueprint.Selected -= onBlueprintSelected;
- blueprint.Deselected -= onBlueprintDeselected;
- blueprint.SelectionRequested -= onSelectionRequested;
- blueprint.DragRequested -= onDragRequested;
-
- selectionBlueprints.Remove(blueprint);
- }
+ private void removeBlueprintFor(DrawableHitObject hitObject) => removeBlueprintFor(hitObject.HitObject);
protected override bool OnClick(ClickEvent e)
{
@@ -183,6 +198,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void onDragRequested(SelectionBlueprint blueprint, DragEvent dragEvent) => selectionHandler.HandleDrag(blueprint, dragEvent);
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (beatmap != null)
+ {
+ beatmap.HitObjectAdded -= addBlueprintFor;
+ beatmap.HitObjectRemoved -= removeBlueprintFor;
+ }
+ }
+
private class SelectionBlueprintContainer : Container
{
protected override int Compare(Drawable x, Drawable y)
diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
index 5699ef0a84..ec4dda5c23 100644
--- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
+++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs
@@ -9,15 +9,13 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Logging;
using osu.Game.Rulesets.Edit;
-using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Compose
{
- [Cached(Type = typeof(IPlacementHandler))]
- public class ComposeScreen : EditorScreen, IPlacementHandler
+ public class ComposeScreen : EditorScreen
{
private const float vertical_margins = 10;
private const float horizontal_margins = 20;
@@ -119,13 +117,5 @@ namespace osu.Game.Screens.Edit.Compose
composerContainer.Child = composer;
}
-
- public void BeginPlacement(HitObject hitObject)
- {
- }
-
- public void EndPlacement(HitObject hitObject) => composer.Add(hitObject);
-
- public void Delete(HitObject hitObject) => composer.Remove(hitObject);
}
}
diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs
new file mode 100644
index 0000000000..f0b6c62154
--- /dev/null
+++ b/osu.Game/Screens/Edit/EditorBeatmap.cs
@@ -0,0 +1,83 @@
+// 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.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps.Timing;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Screens.Edit
+{
+ public class EditorBeatmap : IEditorBeatmap
+ where T : HitObject
+ {
+ public event Action HitObjectAdded;
+ public event Action HitObjectRemoved;
+
+ private readonly Beatmap beatmap;
+
+ public EditorBeatmap(Beatmap beatmap)
+ {
+ this.beatmap = beatmap;
+ }
+
+ public BeatmapInfo BeatmapInfo
+ {
+ get => beatmap.BeatmapInfo;
+ set => beatmap.BeatmapInfo = value;
+ }
+
+ public BeatmapMetadata Metadata => beatmap.Metadata;
+
+ public ControlPointInfo ControlPointInfo => beatmap.ControlPointInfo;
+
+ public List Breaks => beatmap.Breaks;
+
+ public double TotalBreakTime => beatmap.TotalBreakTime;
+
+ IReadOnlyList IBeatmap.HitObjects => beatmap.HitObjects;
+
+ IReadOnlyList IBeatmap.HitObjects => beatmap.HitObjects;
+
+ public IEnumerable GetStatistics() => beatmap.GetStatistics();
+
+ public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone();
+
+ ///
+ /// Adds a to this .
+ ///
+ /// The to add.
+ public void Add(T hitObject)
+ {
+ // Preserve existing sorting order in the beatmap
+ var insertionIndex = beatmap.HitObjects.FindLastIndex(h => h.StartTime <= hitObject.StartTime);
+ beatmap.HitObjects.Insert(insertionIndex + 1, hitObject);
+
+ HitObjectAdded?.Invoke(hitObject);
+ }
+
+ ///
+ /// Removes a from this .
+ ///
+ /// The to add.
+ public void Remove(T hitObject)
+ {
+ if (beatmap.HitObjects.Remove(hitObject))
+ HitObjectRemoved?.Invoke(hitObject);
+ }
+
+ ///
+ /// Adds a to this .
+ ///
+ /// The to add.
+ public void Add(HitObject hitObject) => Add((T)hitObject);
+
+ ///
+ /// Removes a from this .
+ ///
+ /// The to add.
+ public void Remove(HitObject hitObject) => Remove((T)hitObject);
+ }
+}
diff --git a/osu.Game/Screens/Edit/EditorWorkingBeatmap.cs b/osu.Game/Screens/Edit/EditorWorkingBeatmap.cs
new file mode 100644
index 0000000000..299059407c
--- /dev/null
+++ b/osu.Game/Screens/Edit/EditorWorkingBeatmap.cs
@@ -0,0 +1,46 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Framework.Audio.Track;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Skinning;
+using osu.Game.Storyboards;
+
+namespace osu.Game.Screens.Edit
+{
+ ///
+ /// Encapsulates a while providing an overridden .
+ ///
+ ///
+ public class EditorWorkingBeatmap : IWorkingBeatmap
+ where TObject : HitObject
+ {
+ private readonly Beatmap playableBeatmap;
+ private readonly WorkingBeatmap workingBeatmap;
+
+ public EditorWorkingBeatmap(Beatmap playableBeatmap, WorkingBeatmap workingBeatmap)
+ {
+ this.playableBeatmap = playableBeatmap;
+ this.workingBeatmap = workingBeatmap;
+ }
+
+ public IBeatmap Beatmap => workingBeatmap.Beatmap;
+
+ public Texture Background => workingBeatmap.Background;
+
+ public Track Track => workingBeatmap.Track;
+
+ public Waveform Waveform => workingBeatmap.Waveform;
+
+ public Storyboard Storyboard => workingBeatmap.Storyboard;
+
+ public ISkin Skin => workingBeatmap.Skin;
+
+ public IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods) => playableBeatmap;
+ }
+}
diff --git a/osu.Game/Screens/Edit/IEditorBeatmap.cs b/osu.Game/Screens/Edit/IEditorBeatmap.cs
new file mode 100644
index 0000000000..2f250ba446
--- /dev/null
+++ b/osu.Game/Screens/Edit/IEditorBeatmap.cs
@@ -0,0 +1,36 @@
+// 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.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Screens.Edit
+{
+ ///
+ /// Interface for the contained by the see .
+ /// Children of may resolve the beatmap via or .
+ ///
+ public interface IEditorBeatmap : IBeatmap
+ {
+ ///
+ /// Invoked when a is added to this .
+ ///
+ event Action HitObjectAdded;
+
+ ///
+ /// Invoked when a is removed from this .
+ ///
+ event Action HitObjectRemoved;
+ }
+
+ ///
+ /// Interface for the contained by the see .
+ /// Children of may resolve the beatmap via or .
+ ///
+ public interface IEditorBeatmap : IEditorBeatmap, IBeatmap
+ where T : HitObject
+ {
+ }
+}
diff --git a/osu.Game/Screens/Menu/IntroCircles.cs b/osu.Game/Screens/Menu/IntroCircles.cs
index 4fa1a81123..c069f82134 100644
--- a/osu.Game/Screens/Menu/IntroCircles.cs
+++ b/osu.Game/Screens/Menu/IntroCircles.cs
@@ -77,7 +77,7 @@ namespace osu.Game.Screens.Menu
Scheduler.AddDelayed(delegate
{
- // Only start the current track if it is the menu music. A beatmap's track is started when entering the Main Manu.
+ // Only start the current track if it is the menu music. A beatmap's track is started when entering the Main Menu.
if (menuMusic.Value)
{
track.Restart();
diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs
index 9d0a5cd05b..6984959e9c 100644
--- a/osu.Game/Screens/Menu/LogoVisualisation.cs
+++ b/osu.Game/Screens/Menu/LogoVisualisation.cs
@@ -47,7 +47,7 @@ namespace osu.Game.Screens.Menu
private const float visualiser_rounds = 5;
///
- /// How much should each bar go down each milisecond (based on a full bar).
+ /// How much should each bar go down each millisecond (based on a full bar).
///
private const float decay_per_milisecond = 0.0024f;
@@ -161,7 +161,7 @@ namespace osu.Game.Screens.Menu
private IShader shader;
private Texture texture;
- //Asuming the logo is a circle, we don't need a second dimension.
+ //Assuming the logo is a circle, we don't need a second dimension.
private float size;
private Color4 colour;
diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs
index 0c5bf12bdb..534400e720 100644
--- a/osu.Game/Screens/Menu/OsuLogo.cs
+++ b/osu.Game/Screens/Menu/OsuLogo.cs
@@ -229,7 +229,7 @@ namespace osu.Game.Screens.Menu
}
///
- /// Schedule a new extenral animation. Handled queueing and finishing previous animations in a sane way.
+ /// Schedule a new external animation. Handled queueing and finishing previous animations in a sane way.
///
/// The animation to be performed
/// If true, the new animation is delayed until all previous transforms finish. If false, existing transformed are cleared.
diff --git a/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs b/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs
new file mode 100644
index 0000000000..cdfa0e993b
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs
@@ -0,0 +1,100 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Screens.Play.HUD.HitErrorMeters;
+
+namespace osu.Game.Screens.Play.HUD
+{
+ public class HitErrorDisplay : Container
+ {
+ private const int fade_duration = 200;
+ private const int margin = 10;
+
+ private readonly Bindable type = new Bindable();
+
+ private readonly HitWindows hitWindows;
+
+ private readonly ScoreProcessor processor;
+
+ public HitErrorDisplay(ScoreProcessor processor, HitWindows hitWindows)
+ {
+ this.processor = processor;
+ this.hitWindows = hitWindows;
+
+ RelativeSizeAxes = Axes.Both;
+
+ processor.NewJudgement += onNewJudgement;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ config.BindWith(OsuSetting.ScoreMeter, type);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ type.BindValueChanged(typeChanged, true);
+ }
+
+ private void onNewJudgement(JudgementResult result)
+ {
+ foreach (var c in Children)
+ c.OnNewJudgement(result);
+ }
+
+ private void typeChanged(ValueChangedEvent type)
+ {
+ Children.ForEach(c => c.FadeOut(fade_duration, Easing.OutQuint));
+
+ if (hitWindows == null)
+ return;
+
+ switch (type.NewValue)
+ {
+ case ScoreMeterType.HitErrorBoth:
+ createBar(false);
+ createBar(true);
+ break;
+
+ case ScoreMeterType.HitErrorLeft:
+ createBar(false);
+ break;
+
+ case ScoreMeterType.HitErrorRight:
+ createBar(true);
+ break;
+ }
+ }
+
+ private void createBar(bool rightAligned)
+ {
+ var display = new BarHitErrorMeter(hitWindows, rightAligned)
+ {
+ Margin = new MarginPadding(margin),
+ Anchor = rightAligned ? Anchor.CentreRight : Anchor.CentreLeft,
+ Origin = rightAligned ? Anchor.CentreRight : Anchor.CentreLeft,
+ Alpha = 0,
+ };
+
+ Add(display);
+ display.FadeInFromZero(fade_duration, Easing.OutQuint);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ processor.NewJudgement -= onNewJudgement;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs
new file mode 100644
index 0000000000..594dd64e52
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs
@@ -0,0 +1,284 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Screens.Play.HUD.HitErrorMeters
+{
+ public class BarHitErrorMeter : HitErrorMeter
+ {
+ private readonly Anchor alignment;
+
+ private const int arrow_move_duration = 400;
+
+ private const int judgement_line_width = 6;
+
+ private const int bar_height = 200;
+
+ private const int bar_width = 2;
+
+ private const int spacing = 2;
+
+ private const float chevron_size = 8;
+
+ private SpriteIcon arrow;
+
+ private Container colourBarsEarly;
+ private Container colourBarsLate;
+
+ private Container judgementsContainer;
+
+ private double maxHitWindow;
+
+ public BarHitErrorMeter(HitWindows hitWindows, bool rightAligned = false)
+ : base(hitWindows)
+ {
+ alignment = rightAligned ? Anchor.x0 : Anchor.x2;
+
+ AutoSizeAxes = Axes.Both;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ InternalChild = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.X,
+ Height = bar_height,
+ Direction = FillDirection.Horizontal,
+ Spacing = new Vector2(spacing, 0),
+ Margin = new MarginPadding(2),
+ Children = new Drawable[]
+ {
+ judgementsContainer = new Container
+ {
+ Anchor = Anchor.y1 | alignment,
+ Origin = Anchor.y1 | alignment,
+ Width = judgement_line_width,
+ RelativeSizeAxes = Axes.Y,
+ },
+ colourBars = new Container
+ {
+ Width = bar_width,
+ RelativeSizeAxes = Axes.Y,
+ Anchor = Anchor.y1 | alignment,
+ Origin = Anchor.y1 | alignment,
+ Children = new Drawable[]
+ {
+ colourBarsEarly = new Container
+ {
+ Anchor = Anchor.y1 | alignment,
+ Origin = alignment,
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.5f,
+ Scale = new Vector2(1, -1),
+ },
+ colourBarsLate = new Container
+ {
+ Anchor = Anchor.y1 | alignment,
+ Origin = alignment,
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.5f,
+ },
+ new SpriteIcon
+ {
+ Y = -10,
+ Size = new Vector2(10),
+ Icon = FontAwesome.Solid.ShippingFast,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ },
+ new SpriteIcon
+ {
+ Y = 10,
+ Size = new Vector2(10),
+ Icon = FontAwesome.Solid.Bicycle,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ }
+ }
+ },
+ new Container
+ {
+ Anchor = Anchor.y1 | alignment,
+ Origin = Anchor.y1 | alignment,
+ Width = chevron_size,
+ RelativeSizeAxes = Axes.Y,
+ Child = arrow = new SpriteIcon
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.Centre,
+ RelativePositionAxes = Axes.Y,
+ Y = 0.5f,
+ Icon = alignment == Anchor.x2 ? FontAwesome.Solid.ChevronRight : FontAwesome.Solid.ChevronLeft,
+ Size = new Vector2(chevron_size),
+ }
+ },
+ }
+ };
+
+ createColourBars(colours);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ colourBars.Height = 0;
+ colourBars.ResizeHeightTo(1, 800, Easing.OutQuint);
+
+ arrow.Alpha = 0;
+ arrow.Delay(200).FadeInFromZero(600);
+ }
+
+ private void createColourBars(OsuColour colours)
+ {
+ var windows = HitWindows.GetAllAvailableHalfWindows().ToArray();
+
+ maxHitWindow = windows.First().length;
+
+ for (var i = 0; i < windows.Length; i++)
+ {
+ var (result, length) = windows[i];
+
+ colourBarsEarly.Add(createColourBar(result, (float)(length / maxHitWindow), i == 0));
+ colourBarsLate.Add(createColourBar(result, (float)(length / maxHitWindow), i == 0));
+ }
+
+ // a little nub to mark the centre point.
+ var centre = createColourBar(windows.Last().result, 0.01f);
+ centre.Anchor = centre.Origin = Anchor.y1 | (alignment == Anchor.x2 ? Anchor.x0 : Anchor.x2);
+ centre.Width = 2.5f;
+ colourBars.Add(centre);
+
+ Color4 getColour(HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.Meh:
+ return colours.Yellow;
+
+ case HitResult.Ok:
+ return colours.Green;
+
+ case HitResult.Good:
+ return colours.GreenLight;
+
+ case HitResult.Great:
+ return colours.Blue;
+
+ default:
+ return colours.BlueLight;
+ }
+ }
+
+ Drawable createColourBar(HitResult result, float height, bool first = false)
+ {
+ var colour = getColour(result);
+
+ if (first)
+ {
+ // the first bar needs gradient rendering.
+ const float gradient_start = 0.8f;
+
+ return new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = getColour(result),
+ Height = height * gradient_start
+ },
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ RelativePositionAxes = Axes.Both,
+ Colour = ColourInfo.GradientVertical(colour, colour.Opacity(0)),
+ Y = gradient_start,
+ Height = height * (1 - gradient_start)
+ },
+ }
+ };
+ }
+
+ return new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colour,
+ Height = height
+ };
+ }
+ }
+
+ private double floatingAverage;
+ private Container colourBars;
+
+ public override void OnNewJudgement(JudgementResult judgement)
+ {
+ if (!judgement.IsHit)
+ return;
+
+ judgementsContainer.Add(new JudgementLine
+ {
+ Y = getRelativeJudgementPosition(judgement.TimeOffset),
+ Anchor = alignment == Anchor.x2 ? Anchor.x0 : Anchor.x2,
+ Origin = Anchor.y1 | (alignment == Anchor.x2 ? Anchor.x0 : Anchor.x2),
+ });
+
+ arrow.MoveToY(
+ getRelativeJudgementPosition(floatingAverage = floatingAverage * 0.9 + judgement.TimeOffset * 0.1)
+ , arrow_move_duration, Easing.Out);
+ }
+
+ private float getRelativeJudgementPosition(double value) => (float)((value / maxHitWindow) + 1) / 2;
+
+ private class JudgementLine : CompositeDrawable
+ {
+ private const int judgement_fade_duration = 10000;
+
+ public JudgementLine()
+ {
+ RelativeSizeAxes = Axes.X;
+ RelativePositionAxes = Axes.Y;
+ Height = 3;
+
+ InternalChild = new CircularContainer
+ {
+ Masking = true,
+ RelativeSizeAxes = Axes.Both,
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.White,
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Width = 0;
+
+ this.ResizeWidthTo(1, 200, Easing.OutElasticHalf);
+ this.FadeTo(0.8f, 150).Then().FadeOut(judgement_fade_duration, Easing.OutQuint).Expire();
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs
new file mode 100644
index 0000000000..da1d9fff0d
--- /dev/null
+++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/HitErrorMeter.cs
@@ -0,0 +1,21 @@
+// 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.Containers;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Screens.Play.HUD.HitErrorMeters
+{
+ public abstract class HitErrorMeter : CompositeDrawable
+ {
+ protected readonly HitWindows HitWindows;
+
+ protected HitErrorMeter(HitWindows hitWindows)
+ {
+ HitWindows = hitWindows;
+ }
+
+ public abstract void OnNewJudgement(JudgementResult judgement);
+ }
+}
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index 43b9491750..8e642ea552 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -33,6 +34,7 @@ namespace osu.Game.Screens.Play
public readonly HealthDisplay HealthDisplay;
public readonly SongProgress Progress;
public readonly ModDisplay ModDisplay;
+ public readonly HitErrorDisplay HitErrorDisplay;
public readonly HoldForMenuButton HoldToQuit;
public readonly PlayerSettingsOverlay PlayerSettingsOverlay;
@@ -84,6 +86,7 @@ namespace osu.Game.Screens.Play
HealthDisplay = CreateHealthDisplay(),
Progress = CreateProgress(),
ModDisplay = CreateModsContainer(),
+ HitErrorDisplay = CreateHitErrorDisplayOverlay(),
}
},
PlayerSettingsOverlay = CreatePlayerSettingsOverlay(),
@@ -256,6 +259,8 @@ namespace osu.Game.Screens.Play
Margin = new MarginPadding { Top = 20, Right = 10 },
};
+ protected virtual HitErrorDisplay CreateHitErrorDisplayOverlay() => new HitErrorDisplay(scoreProcessor, drawableRuleset.Objects.FirstOrDefault()?.HitWindows);
+
protected virtual PlayerSettingsOverlay CreatePlayerSettingsOverlay() => new PlayerSettingsOverlay();
protected virtual void BindProcessor(ScoreProcessor processor)
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index e7398be176..3f1603eabe 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -60,7 +60,9 @@ namespace osu.Game.Screens.Play
[Resolved]
private ScoreManager scoreManager { get; set; }
- private RulesetInfo ruleset;
+ private RulesetInfo rulesetInfo;
+
+ private Ruleset ruleset;
private IAPIProvider api;
@@ -121,21 +123,53 @@ namespace osu.Game.Screens.Play
InternalChild = GameplayClockContainer = new GameplayClockContainer(working, Mods.Value, DrawableRuleset.GameplayStartTime);
- GameplayClockContainer.Children = new[]
+ addUnderlayComponents(GameplayClockContainer);
+ addGameplayComponents(GameplayClockContainer, working);
+ addOverlayComponents(GameplayClockContainer, working);
+
+ DrawableRuleset.HasReplayLoaded.BindValueChanged(e => HUDOverlay.HoldToQuit.PauseOnFocusLost = !e.NewValue && PauseOnFocusLost, true);
+
+ // bind clock into components that require it
+ DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused);
+
+ // Bind ScoreProcessor to ourselves
+ ScoreProcessor.AllJudged += onCompletion;
+ ScoreProcessor.Failed += onFail;
+
+ foreach (var mod in Mods.Value.OfType())
+ mod.ApplyToScoreProcessor(ScoreProcessor);
+ }
+
+ private void addUnderlayComponents(Container target)
+ {
+ target.Add(DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both });
+ }
+
+ private void addGameplayComponents(Container target, WorkingBeatmap working)
+ {
+ var beatmapSkinProvider = new BeatmapSkinProvidingContainer(working.Skin);
+
+ // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation
+ // full access to all skin sources.
+ var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider));
+
+ // load the skinning hierarchy first.
+ // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.
+ target.Add(new ScalingContainer(ScalingMode.Gameplay)
+ .WithChild(beatmapSkinProvider
+ .WithChild(target = rulesetSkinProvider)));
+
+ target.AddRange(new Drawable[]
+ {
+ DrawableRuleset,
+ new ComboEffects(ScoreProcessor)
+ });
+ }
+
+ private void addOverlayComponents(Container target, WorkingBeatmap working)
+ {
+ target.AddRange(new[]
{
- DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both },
- new ScalingContainer(ScalingMode.Gameplay)
- {
- Child = new LocalSkinOverrideContainer(working.Skin)
- {
- RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
- {
- DrawableRuleset,
- new ComboEffects(ScoreProcessor)
- }
- }
- },
breakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor)
{
Anchor = Anchor.Centre,
@@ -144,6 +178,7 @@ namespace osu.Game.Screens.Play
},
// display the cursor above some HUD elements.
DrawableRuleset.Cursor?.CreateProxy() ?? new Container(),
+ DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(),
HUDOverlay = new HUDOverlay(ScoreProcessor, DrawableRuleset, Mods.Value)
{
HoldToQuit =
@@ -194,19 +229,7 @@ namespace osu.Game.Screens.Play
},
},
failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }
- };
-
- DrawableRuleset.HasReplayLoaded.BindValueChanged(e => HUDOverlay.HoldToQuit.PauseOnFocusLost = !e.NewValue && PauseOnFocusLost, true);
-
- // bind clock into components that require it
- DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused);
-
- // Bind ScoreProcessor to ourselves
- ScoreProcessor.AllJudged += onCompletion;
- ScoreProcessor.Failed += onFail;
-
- foreach (var mod in Mods.Value.OfType())
- mod.ApplyToScoreProcessor(ScoreProcessor);
+ });
}
private WorkingBeatmap loadBeatmap()
@@ -222,20 +245,20 @@ namespace osu.Game.Screens.Play
if (beatmap == null)
throw new InvalidOperationException("Beatmap was not loaded");
- ruleset = Ruleset.Value ?? beatmap.BeatmapInfo.Ruleset;
- var rulesetInstance = ruleset.CreateInstance();
+ rulesetInfo = Ruleset.Value ?? beatmap.BeatmapInfo.Ruleset;
+ ruleset = rulesetInfo.CreateInstance();
try
{
- DrawableRuleset = rulesetInstance.CreateDrawableRulesetWith(working, Mods.Value);
+ DrawableRuleset = ruleset.CreateDrawableRulesetWith(working, Mods.Value);
}
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, Mods.Value);
+ rulesetInfo = beatmap.BeatmapInfo.Ruleset;
+ ruleset = rulesetInfo.CreateInstance();
+ DrawableRuleset = ruleset.CreateDrawableRulesetWith(Beatmap.Value, Mods.Value);
}
if (!DrawableRuleset.Objects.Any())
@@ -313,7 +336,7 @@ namespace osu.Game.Screens.Play
var score = DrawableRuleset.ReplayScore?.ScoreInfo ?? new ScoreInfo
{
Beatmap = Beatmap.Value.BeatmapInfo,
- Ruleset = ruleset,
+ Ruleset = rulesetInfo,
Mods = Mods.Value.ToArray(),
User = api.LocalUser.Value,
};
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index edb0e6deb8..d0cb5986a8 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -414,7 +414,11 @@ namespace osu.Game.Screens.Select
{
Logger.Log($"beatmap changed from \"{Beatmap.Value.BeatmapInfo}\" to \"{beatmap}\"");
- Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, Beatmap.Value);
+ WorkingBeatmap previous = Beatmap.Value;
+ Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, previous);
+
+ if (this.IsCurrentScreen() && Beatmap.Value?.Track != previous?.Track)
+ ensurePlayingSelected();
if (beatmap != null)
{
@@ -425,8 +429,6 @@ namespace osu.Game.Screens.Select
}
}
- if (this.IsCurrentScreen())
- ensurePlayingSelected();
UpdateBeatmap(Beatmap.Value);
}
}
diff --git a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs
new file mode 100644
index 0000000000..345df35b12
--- /dev/null
+++ b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs
@@ -0,0 +1,39 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Game.Audio;
+using osu.Game.Configuration;
+
+namespace osu.Game.Skinning
+{
+ ///
+ /// A container which overrides existing skin options with beatmap-local values.
+ ///
+ public class BeatmapSkinProvidingContainer : SkinProvidingContainer
+ {
+ private readonly Bindable beatmapSkins = new Bindable();
+ private readonly Bindable beatmapHitsounds = new Bindable();
+
+ protected override bool AllowConfigurationLookup => beatmapSkins.Value;
+ protected override bool AllowDrawableLookup(string componentName) => beatmapSkins.Value;
+ protected override bool AllowTextureLookup(string componentName) => beatmapSkins.Value;
+ protected override bool AllowSampleLookup(ISampleInfo componentName) => beatmapHitsounds.Value;
+
+ public BeatmapSkinProvidingContainer(ISkin skin)
+ : base(skin)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config)
+ {
+ config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins);
+ config.BindWith(OsuSetting.BeatmapHitsounds, beatmapHitsounds);
+
+ beatmapSkins.BindValueChanged(_ => TriggerSourceChanged());
+ beatmapHitsounds.BindValueChanged(_ => TriggerSourceChanged());
+ }
+ }
+}
diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs
new file mode 100644
index 0000000000..b35c9c7b97
--- /dev/null
+++ b/osu.Game/Skinning/DefaultLegacySkin.cs
@@ -0,0 +1,23 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Audio;
+using osu.Framework.IO.Stores;
+
+namespace osu.Game.Skinning
+{
+ public class DefaultLegacySkin : LegacySkin
+ {
+ public DefaultLegacySkin(IResourceStore storage, AudioManager audioManager)
+ : base(Info, storage, audioManager, string.Empty)
+ {
+ }
+
+ public static SkinInfo Info { get; } = new SkinInfo
+ {
+ ID = -1, // this is temporary until database storage is decided upon.
+ Name = "osu!classic",
+ Creator = "team osu!"
+ };
+ }
+}
diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs
index 6072bb64ed..ec957566cb 100644
--- a/osu.Game/Skinning/DefaultSkin.cs
+++ b/osu.Game/Skinning/DefaultSkin.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Skinning
public DefaultSkin()
: base(SkinInfo.Default)
{
- Configuration = new SkinConfiguration();
+ Configuration = new DefaultSkinConfiguration();
}
public override Drawable GetDrawableComponent(string componentName) => null;
diff --git a/osu.Game/Skinning/DefaultSkinConfiguration.cs b/osu.Game/Skinning/DefaultSkinConfiguration.cs
new file mode 100644
index 0000000000..722b35f102
--- /dev/null
+++ b/osu.Game/Skinning/DefaultSkinConfiguration.cs
@@ -0,0 +1,28 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osuTK.Graphics;
+
+namespace osu.Game.Skinning
+{
+ ///
+ /// A skin configuration pre-populated with sane defaults.
+ ///
+ public class DefaultSkinConfiguration : SkinConfiguration
+ {
+ public DefaultSkinConfiguration()
+ {
+ HitCircleFont = "default";
+
+ ComboColours.AddRange(new[]
+ {
+ new Color4(17, 136, 170, 255),
+ new Color4(102, 136, 0, 255),
+ new Color4(204, 102, 0, 255),
+ new Color4(121, 9, 13, 255)
+ });
+
+ CursorExpand = true;
+ }
+ }
+}
diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index 570ba1ced7..56b8aa19c2 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -1,30 +1,14 @@
// 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.IO;
using System.Linq;
-using System.Threading.Tasks;
-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.Animations;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
-using osu.Framework.Text;
using osu.Game.Audio;
-using osu.Game.Database;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Objects.Types;
-using osu.Game.Rulesets.UI;
-using osuTK;
using osuTK.Graphics;
namespace osu.Game.Skinning
@@ -35,13 +19,6 @@ namespace osu.Game.Skinning
protected IResourceStore