diff --git a/osu.Android.props b/osu.Android.props
index bbe8426316..5078fee1cf 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
index e8c2472c3b..cecac38f70 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@@ -54,12 +55,77 @@ namespace osu.Game.Rulesets.Mania.Tests
}
}
+ [Test]
+ public void TestHoldNoteMissAfterNextObjectStartTime()
+ {
+ var objects = new List
+ {
+ new HoldNote
+ {
+ StartTime = 1000,
+ EndTime = 1010,
+ },
+ new HoldNote
+ {
+ StartTime = 1020,
+ EndTime = 1030
+ }
+ };
+
+ performTest(objects, new List());
+
+ addJudgementAssert(objects[0], HitResult.IgnoreHit);
+ addJudgementAssert(objects[1], HitResult.IgnoreHit);
+ }
+
+ [Test]
+ public void TestHoldNoteReleasedHitAfterNextObjectStartTime()
+ {
+ var objects = new List
+ {
+ new HoldNote
+ {
+ StartTime = 1000,
+ EndTime = 1010,
+ },
+ new HoldNote
+ {
+ StartTime = 1020,
+ EndTime = 1030
+ }
+ };
+
+ var frames = new List
+ {
+ new ManiaReplayFrame(1000, ManiaAction.Key1),
+ new ManiaReplayFrame(1030),
+ new ManiaReplayFrame(1040, ManiaAction.Key1),
+ new ManiaReplayFrame(1050)
+ };
+
+ performTest(objects, frames);
+
+ addJudgementAssert(objects[0], HitResult.IgnoreHit);
+ addJudgementAssert("first head", () => ((HoldNote)objects[0]).Head, HitResult.Perfect);
+ addJudgementAssert("first tail", () => ((HoldNote)objects[0]).Tail, HitResult.Perfect);
+
+ addJudgementAssert(objects[1], HitResult.IgnoreHit);
+ addJudgementAssert("second head", () => ((HoldNote)objects[1]).Head, HitResult.Great);
+ addJudgementAssert("second tail", () => ((HoldNote)objects[1]).Tail, HitResult.Perfect);
+ }
+
private void addJudgementAssert(ManiaHitObject hitObject, HitResult result)
{
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
() => judgementResults.Single(r => r.HitObject == hitObject).Type == result);
}
+ private void addJudgementAssert(string name, Func hitObject, HitResult result)
+ {
+ AddAssert($"{name} judgement is {result}",
+ () => judgementResults.Single(r => r.HitObject == hitObject()).Type == result);
+ }
+
private void addJudgementOffsetAssert(ManiaHitObject hitObject, double offset)
{
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}",
diff --git a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs
index 0f9cd48dd8..961858b62b 100644
--- a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs
+++ b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using System.Collections.Generic;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Mania.Objects.Drawables;
@@ -44,9 +43,6 @@ namespace osu.Game.Rulesets.Mania.UI
/// The that was hit.
public void HandleHit(DrawableHitObject hitObject)
{
- if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset))
- throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!");
-
foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
{
if (obj.Judged)
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
index 075bf314bc..856bfd7e80 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
@@ -125,6 +125,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{
if (!enabled) return null;
+ if (component is OsuSkinComponent osuComponent && osuComponent.Component == OsuSkinComponents.SliderBody)
+ return null;
+
return new OsuSpriteText
{
Text = identifier,
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
index 025e202666..cf7faca9b9 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
@@ -27,17 +27,23 @@ namespace osu.Game.Rulesets.Osu.Mods
public override void ApplyToDrawableHitObjects(IEnumerable drawables)
{
foreach (var d in drawables)
- d.ApplyCustomUpdateState += applyFadeInAdjustment;
+ {
+ d.HitObjectApplied += applyFadeInAdjustment;
+ applyFadeInAdjustment(d);
+ }
base.ApplyToDrawableHitObjects(drawables);
}
- private void applyFadeInAdjustment(DrawableHitObject hitObject, ArmedState state)
+ private void applyFadeInAdjustment(DrawableHitObject hitObject)
{
if (!(hitObject is DrawableOsuHitObject d))
return;
d.HitObject.TimeFadeIn = d.HitObject.TimePreempt * fade_in_duration_multiplier;
+
+ foreach (var nested in d.NestedHitObjects)
+ applyFadeInAdjustment(nested);
}
private double lastSliderHeadFadeOutStartTime;
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs
new file mode 100644
index 0000000000..b558709592
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs
@@ -0,0 +1,37 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.UI;
+
+namespace osu.Game.Rulesets.Taiko.Tests.Skinning
+{
+ [TestFixture]
+ public class TestSceneKiaiHitExplosion : TaikoSkinnableTestScene
+ {
+ [Test]
+ public void TestKiaiHits()
+ {
+ AddStep("rim hit", () => SetContents(() => getContentFor(createHit(HitType.Rim))));
+ AddStep("centre hit", () => SetContents(() => getContentFor(createHit(HitType.Centre))));
+ }
+
+ private Drawable getContentFor(DrawableTestHit hit)
+ {
+ return new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = new KiaiHitExplosion(hit, hit.HitObject.Type)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ };
+ }
+
+ private DrawableTestHit createHit(HitType type) => new DrawableTestHit(new Hit { StartTime = Time.Current, Type = type });
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
index c88480d18f..96fb065e79 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs
@@ -114,6 +114,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning
return null;
+ case TaikoSkinComponents.TaikoExplosionKiai:
+ // suppress the default kiai explosion if the skin brings its own sprites.
+ // the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield.
+ if (hasExplosion.Value)
+ return Drawable.Empty().With(d => d.LifetimeEnd = double.MinValue);
+
+ return null;
+
case TaikoSkinComponents.Scroller:
if (GetTexture("taiko-slider") != null)
return new LegacyTaikoScroller();
diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
index 132d8f8868..bf48898dd2 100644
--- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
@@ -18,6 +18,7 @@ namespace osu.Game.Rulesets.Taiko
TaikoExplosionMiss,
TaikoExplosionOk,
TaikoExplosionGreat,
+ TaikoExplosionKiai,
Scroller,
Mascot,
}
diff --git a/osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs
new file mode 100644
index 0000000000..7ce8b016d5
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs
@@ -0,0 +1,64 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osuTK;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Taiko.Objects;
+
+namespace osu.Game.Rulesets.Taiko.UI
+{
+ public class DefaultKiaiHitExplosion : CircularContainer
+ {
+ public override bool RemoveWhenNotAlive => true;
+
+ private readonly HitType type;
+
+ public DefaultKiaiHitExplosion(HitType type)
+ {
+ this.type = type;
+
+ RelativeSizeAxes = Axes.Both;
+
+ Blending = BlendingParameters.Additive;
+
+ Masking = true;
+ Alpha = 0.25f;
+
+ Children = new[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ AlwaysPresent = true
+ }
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = type == HitType.Rim ? colours.BlueDarker : colours.PinkDarker,
+ Radius = 60,
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ this.ScaleTo(new Vector2(1, 3f), 500, Easing.OutQuint);
+ this.FadeOut(250);
+
+ Expire(true);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs
index 067d390894..20900a9352 100644
--- a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs
@@ -1,71 +1,47 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osuTK;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Effects;
-using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Skinning;
+using osuTK;
namespace osu.Game.Rulesets.Taiko.UI
{
- public class KiaiHitExplosion : CircularContainer
+ public class KiaiHitExplosion : Container
{
public override bool RemoveWhenNotAlive => true;
+ [Cached(typeof(DrawableHitObject))]
public readonly DrawableHitObject JudgedObject;
- private readonly HitType type;
- public KiaiHitExplosion(DrawableHitObject judgedObject, HitType type)
+ private readonly HitType hitType;
+
+ private SkinnableDrawable skinnable;
+
+ public override double LifetimeStart => skinnable.Drawable.LifetimeStart;
+
+ public override double LifetimeEnd => skinnable.Drawable.LifetimeEnd;
+
+ public KiaiHitExplosion(DrawableHitObject judgedObject, HitType hitType)
{
JudgedObject = judgedObject;
- this.type = type;
+ this.hitType = hitType;
Anchor = Anchor.CentreLeft;
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
Size = new Vector2(TaikoHitObject.DEFAULT_SIZE, 1);
-
- Blending = BlendingParameters.Additive;
-
- Masking = true;
- Alpha = 0.25f;
-
- Children = new[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Alpha = 0,
- AlwaysPresent = true
- }
- };
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load()
{
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Colour = type == HitType.Rim ? colours.BlueDarker : colours.PinkDarker,
- Radius = 60,
- };
- }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- this.ScaleTo(new Vector2(1, 3f), 500, Easing.OutQuint);
- this.FadeOut(250);
-
- Expire(true);
+ Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoExplosionKiai), _ => new DefaultKiaiHitExplosion(hitType));
}
}
}
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index 442be6e837..37ab489da5 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -16,8 +16,6 @@ namespace osu.Game.Beatmaps.Formats
{
public class LegacyBeatmapDecoder : LegacyDecoder
{
- public const int LATEST_VERSION = 14;
-
private Beatmap beatmap;
private ConvertHitObjectParser parser;
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index 7b377e481f..de4dc8cdc8 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -16,6 +16,8 @@ namespace osu.Game.Beatmaps.Formats
public abstract class LegacyDecoder : Decoder
where T : new()
{
+ public const int LATEST_VERSION = 14;
+
protected readonly int FormatVersion;
protected LegacyDecoder(int version)
diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
index 8d8ca523d5..9a244c8bb2 100644
--- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Beatmaps.Legacy;
@@ -23,15 +24,15 @@ namespace osu.Game.Beatmaps.Formats
private readonly Dictionary variables = new Dictionary();
- public LegacyStoryboardDecoder()
- : base(0)
+ public LegacyStoryboardDecoder(int version = LATEST_VERSION)
+ : base(version)
{
}
public static void Register()
{
// note that this isn't completely correct
- AddDecoder(@"osu file format v", m => new LegacyStoryboardDecoder());
+ AddDecoder(@"osu file format v", m => new LegacyStoryboardDecoder(Parsing.ParseInt(m.Split('v').Last())));
AddDecoder(@"[Events]", m => new LegacyStoryboardDecoder());
SetFallbackDecoder(() => new LegacyStoryboardDecoder());
}
@@ -133,6 +134,11 @@ namespace osu.Game.Beatmaps.Formats
var y = Parsing.ParseFloat(split[5], Parsing.MAX_COORDINATE_VALUE);
var frameCount = Parsing.ParseInt(split[6]);
var frameDelay = Parsing.ParseDouble(split[7]);
+
+ if (FormatVersion < 6)
+ // this is random as hell but taken straight from osu-stable.
+ frameDelay = Math.Round(0.015 * frameDelay) * 1.186 * (1000 / 60f);
+
var loopType = split.Length > 8 ? (AnimationLoopType)Enum.Parse(typeof(AnimationLoopType), split[8]) : AnimationLoopType.LoopForever;
storyboardSprite = new StoryboardAnimation(path, origin, new Vector2(x, y), frameCount, frameDelay, loopType);
storyboard.GetLayer(layer).Add(storyboardSprite);
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 6b768cc8fc..1a1ebcc2d4 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -681,7 +681,6 @@ namespace osu.Game
loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true);
loadComponentSingleFile(new DialogOverlay(), topMostOverlayContent.Add, true);
- loadComponentSingleFile(externalLinkOpener = new ExternalLinkOpener(), topMostOverlayContent.Add);
chatOverlay.State.ValueChanged += state => channelManager.HighPollRate.Value = state.NewValue == Visibility.Visible;
diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs
index 8f19cd900c..556f3139dd 100644
--- a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs
+++ b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs
@@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
public class PaginatedMostPlayedBeatmapContainer : PaginatedContainer
{
public PaginatedMostPlayedBeatmapContainer(Bindable user)
- : base(user, "Most Played Beatmaps", "No records. :(")
+ : base(user, "Most Played Beatmaps", "No records. :(", CounterVisibilityState.AlwaysVisible)
{
ItemsPerPage = 5;
}
@@ -27,6 +27,8 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
ItemsContainer.Direction = FillDirection.Vertical;
}
+ protected override int GetCount(User user) => user.BeatmapPlaycountsCount;
+
protected override APIRequest> CreateRequest() =>
new GetUserMostPlayedBeatmapsRequest(User.Value.Id, VisiblePages++, ItemsPerPage);
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index 6af445939b..56b725da0e 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -27,8 +27,16 @@ namespace osu.Game.Rulesets.Objects.Drawables
[Cached(typeof(DrawableHitObject))]
public abstract class DrawableHitObject : SkinReloadableDrawable
{
+ ///
+ /// Invoked after this 's applied has had its defaults applied.
+ ///
public event Action DefaultsApplied;
+ ///
+ /// Invoked after a has been applied to this .
+ ///
+ public event Action HitObjectApplied;
+
///
/// The currently represented by this .
///
@@ -221,6 +229,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
HitObject.DefaultsApplied += onDefaultsApplied;
OnApply(hitObject);
+ HitObjectApplied?.Invoke(this);
// If not loaded, the state update happens in LoadComplete(). Otherwise, the update is scheduled to allow for lifetime updates.
if (IsLoaded)
diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs
index 2a76a963e1..d7e78d5b35 100644
--- a/osu.Game/Users/User.cs
+++ b/osu.Game/Users/User.cs
@@ -144,6 +144,9 @@ namespace osu.Game.Users
[JsonProperty(@"scores_first_count")]
public int ScoresFirstCount;
+ [JsonProperty(@"beatmap_playcounts_count")]
+ public int BeatmapPlaycountsCount;
+
[JsonProperty]
private string[] playstyle
{
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 8f0cc58594..405fb1a6ca 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -26,7 +26,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index f766e0ec03..099ecd8319 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -88,7 +88,7 @@
-
+