diff --git a/osu.Android.props b/osu.Android.props
index f18400bf2f..8a9bf1b9cd 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,7 +52,7 @@
-
+
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
index 5aff4e200b..9ac223a0d7 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
@@ -6,7 +6,6 @@ using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Rulesets.Mania.UI;
@@ -29,11 +28,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
[Resolved(canBeNull: true)]
private ManiaPlayfield playfield { get; set; }
- ///
- /// Gets the samples that are played by this object during gameplay.
- ///
- public ISampleInfo[] GetGameplaySamples() => Samples.Samples;
-
protected override float SamplePlaybackPosition
{
get
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index 9b5893b268..f5e30efd91 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.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.Linq;
using osuTK.Graphics;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -19,6 +18,7 @@ using osuTK;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania.UI
{
@@ -28,12 +28,6 @@ namespace osu.Game.Rulesets.Mania.UI
public const float COLUMN_WIDTH = 80;
public const float SPECIAL_COLUMN_WIDTH = 70;
- ///
- /// For hitsounds played by this (i.e. not as a result of hitting a hitobject),
- /// a certain number of samples are allowed to be played concurrently so that it feels better when spam-pressing the key.
- ///
- private const int max_concurrent_hitsounds = OsuGameBase.SAMPLE_CONCURRENCY;
-
///
/// The index of this column as part of the whole playfield.
///
@@ -45,10 +39,10 @@ namespace osu.Game.Rulesets.Mania.UI
internal readonly Container TopLevelContainer;
private readonly DrawablePool hitExplosionPool;
private readonly OrderedHitPolicy hitPolicy;
- private readonly Container hitSounds;
-
public Container UnderlayElements => HitObjectArea.UnderlayElements;
+ private readonly GameplaySampleTriggerSource sampleTriggerSource;
+
public Column(int index)
{
Index = index;
@@ -64,6 +58,7 @@ namespace osu.Game.Rulesets.Mania.UI
InternalChildren = new[]
{
hitExplosionPool = new DrawablePool(5),
+ sampleTriggerSource = new GameplaySampleTriggerSource(HitObjectContainer),
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements
background.CreateProxy(),
HitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both },
@@ -72,12 +67,6 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Both
},
background,
- hitSounds = new Container
- {
- Name = "Column samples pool",
- RelativeSizeAxes = Axes.Both,
- Children = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new SkinnableSound()).ToArray()
- },
TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
};
@@ -133,29 +122,12 @@ namespace osu.Game.Rulesets.Mania.UI
HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result)));
}
- private int nextHitSoundIndex;
-
public bool OnPressed(ManiaAction action)
{
if (action != Action.Value)
return false;
- var nextObject =
- HitObjectContainer.AliveObjects.FirstOrDefault(h => h.HitObject.StartTime > Time.Current) ??
- // fallback to non-alive objects to find next off-screen object
- HitObjectContainer.Objects.FirstOrDefault(h => h.HitObject.StartTime > Time.Current) ??
- HitObjectContainer.Objects.LastOrDefault();
-
- if (nextObject is DrawableManiaHitObject maniaObject)
- {
- var hitSound = hitSounds[nextHitSoundIndex];
-
- hitSound.Samples = maniaObject.GetGameplaySamples();
- hitSound.Play();
-
- nextHitSoundIndex = (nextHitSoundIndex + 1) % max_concurrent_hitsounds;
- }
-
+ sampleTriggerSource.Play();
return true;
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs
index f9b8e9a985..269a855219 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableBarLine.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Origin = Anchor.Centre,
Children = new Drawable[]
{
- new TaikoPlayfield(new ControlPointInfo()),
+ new TaikoPlayfield(),
hoc = new ScrollingHitObjectContainer()
}
};
@@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Origin = Anchor.Centre,
Children = new Drawable[]
{
- new TaikoPlayfield(new ControlPointInfo()),
+ new TaikoPlayfield(),
hoc = new ScrollingHitObjectContainer()
}
};
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs
index 055a292fe8..24db046748 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneInputDrum.cs
@@ -5,7 +5,6 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Taiko.UI;
using osuTK;
@@ -17,6 +16,13 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
+ var playfield = new TaikoPlayfield();
+
+ var beatmap = CreateWorkingBeatmap(new TaikoRuleset().RulesetInfo).GetPlayableBeatmap(new TaikoRuleset().RulesetInfo);
+
+ foreach (var h in beatmap.HitObjects)
+ playfield.Add(h);
+
SetContents(_ => new TaikoInputManager(new TaikoRuleset().RulesetInfo)
{
RelativeSizeAxes = Axes.Both,
@@ -25,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200),
- Child = new InputDrum(new ControlPointInfo())
+ Child = new InputDrum(playfield.HitObjectContainer)
}
});
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs
index f96297a06d..6f2fcd08f1 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Beatmap.Value.Track.Start();
});
- AddStep("Load playfield", () => SetContents(_ => new TaikoPlayfield(new ControlPointInfo())
+ AddStep("Load playfield", () => SetContents(_ => new TaikoPlayfield
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs
deleted file mode 100644
index e4dc261363..0000000000
--- a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs
+++ /dev/null
@@ -1,104 +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.Collections.Generic;
-using System.Linq;
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Game.Audio;
-using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Skinning;
-
-namespace osu.Game.Rulesets.Taiko.Audio
-{
- ///
- /// Stores samples for the input drum.
- /// The lifetime of the samples is adjusted so that they are only alive during the appropriate sample control point.
- ///
- public class DrumSampleContainer : LifetimeManagementContainer
- {
- private readonly ControlPointInfo controlPoints;
- private readonly Dictionary mappings = new Dictionary();
-
- private readonly IBindableList samplePoints = new BindableList();
-
- public DrumSampleContainer(ControlPointInfo controlPoints)
- {
- this.controlPoints = controlPoints;
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- samplePoints.BindTo(controlPoints.SamplePoints);
- samplePoints.BindCollectionChanged((_, __) => recreateMappings(), true);
- }
-
- private void recreateMappings()
- {
- mappings.Clear();
- ClearInternal();
-
- SampleControlPoint[] points = samplePoints.Count == 0
- ? new[] { controlPoints.SamplePointAt(double.MinValue) }
- : samplePoints.ToArray();
-
- for (int i = 0; i < points.Length; i++)
- {
- var samplePoint = points[i];
-
- var lifetimeStart = i > 0 ? samplePoint.Time : double.MinValue;
- var lifetimeEnd = i + 1 < points.Length ? points[i + 1].Time : double.MaxValue;
-
- AddInternal(mappings[samplePoint.Time] = new DrumSample(samplePoint)
- {
- LifetimeStart = lifetimeStart,
- LifetimeEnd = lifetimeEnd
- });
- }
- }
-
- public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time).Time];
-
- public class DrumSample : CompositeDrawable
- {
- public override bool RemoveWhenNotAlive => false;
-
- public PausableSkinnableSound Centre { get; private set; }
- public PausableSkinnableSound Rim { get; private set; }
-
- private readonly SampleControlPoint samplePoint;
-
- private Bindable sampleBank;
- private BindableNumber sampleVolume;
-
- public DrumSample(SampleControlPoint samplePoint)
- {
- this.samplePoint = samplePoint;
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- sampleBank = samplePoint.SampleBankBindable.GetBoundCopy();
- sampleBank.BindValueChanged(_ => recreate());
-
- sampleVolume = samplePoint.SampleVolumeBindable.GetBoundCopy();
- sampleVolume.BindValueChanged(_ => recreate());
-
- recreate();
- }
-
- private void recreate()
- {
- InternalChildren = new Drawable[]
- {
- Centre = new PausableSkinnableSound(samplePoint.GetSampleInfo()),
- Rim = new PausableSkinnableSound(samplePoint.GetSampleInfo(HitSampleInfo.HIT_CLAP))
- };
- }
- }
- }
-}
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index 90c99316b1..9b73e644c5 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -18,12 +18,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{
internal class TaikoBeatmapConverter : BeatmapConverter
{
- ///
- /// osu! is generally slower than taiko, so a factor is added to increase
- /// speed. This must be used everywhere slider length or beat length is used.
- ///
- public const float LEGACY_VELOCITY_MULTIPLIER = 1.4f;
-
///
/// Because swells are easier in taiko than spinners are in osu!,
/// legacy taiko multiplies a factor when converting the number of required hits.
@@ -52,10 +46,12 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
{
- // Rewrite the beatmap info to add the slider velocity multiplier
- original.BeatmapInfo = original.BeatmapInfo.Clone();
- original.BeatmapInfo.BaseDifficulty = original.BeatmapInfo.BaseDifficulty.Clone();
- original.BeatmapInfo.BaseDifficulty.SliderMultiplier *= LEGACY_VELOCITY_MULTIPLIER;
+ if (!(original.BeatmapInfo.BaseDifficulty is TaikoMutliplierAppliedDifficulty))
+ {
+ // Rewrite the beatmap info to add the slider velocity multiplier
+ original.BeatmapInfo = original.BeatmapInfo.Clone();
+ original.BeatmapInfo.BaseDifficulty = new TaikoMutliplierAppliedDifficulty(original.BeatmapInfo.BaseDifficulty);
+ }
Beatmap converted = base.ConvertBeatmap(original, cancellationToken);
@@ -155,7 +151,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
// The true distance, accounting for any repeats. This ends up being the drum roll distance later
int spans = (obj as IHasRepeats)?.SpanCount() ?? 1;
- double distance = distanceData.Distance * spans * LEGACY_VELOCITY_MULTIPLIER;
+ double distance = distanceData.Distance * spans * LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime);
@@ -194,5 +190,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
}
protected override Beatmap CreateBeatmap() => new TaikoBeatmap();
+
+ private class TaikoMutliplierAppliedDifficulty : BeatmapDifficulty
+ {
+ public TaikoMutliplierAppliedDifficulty(BeatmapDifficulty difficulty)
+ {
+ difficulty.CopyTo(this);
+ SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
index c0377c67a5..b0634295d0 100644
--- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
@@ -6,10 +6,10 @@ using System;
using System.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps.Formats;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
-using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Judgements;
using osuTK;
@@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
double IHasDistance.Distance => Duration * Velocity;
SliderPath IHasPath.Path
- => new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / TaikoBeatmapConverter.LEGACY_VELOCITY_MULTIPLIER);
+ => new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER);
#endregion
}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs
index 795885d4b9..9d35093591 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs
@@ -7,7 +7,8 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
-using osu.Game.Rulesets.Taiko.Audio;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Skinning;
using osuTK;
@@ -111,7 +112,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
public readonly Sprite Centre;
[Resolved]
- private DrumSampleContainer sampleContainer { get; set; }
+ private DrumSampleTriggerSource sampleTriggerSource { get; set; }
public LegacyHalfDrum(bool flipped)
{
@@ -143,17 +144,16 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
public bool OnPressed(TaikoAction action)
{
Drawable target = null;
- var drumSample = sampleContainer.SampleAt(Time.Current);
if (action == CentreAction)
{
target = Centre;
- drumSample.Centre?.Play();
+ sampleTriggerSource.Play(HitType.Centre);
}
else if (action == RimAction)
{
target = Rim;
- drumSample.Rim?.Play();
+ sampleTriggerSource.Play(HitType.Rim);
}
if (target != null)
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
index 650ce1f5a3..6ddbf3c16b 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
@@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Taiko.UI
protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);
- protected override Playfield CreatePlayfield() => new TaikoPlayfield(Beatmap.ControlPointInfo);
+ protected override Playfield CreatePlayfield() => new TaikoPlayfield();
public override DrawableHitObject CreateDrawableRepresentation(TaikoHitObject h) => null;
diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs
new file mode 100644
index 0000000000..3279d128d3
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs
@@ -0,0 +1,30 @@
+// 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.Audio;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Taiko.UI
+{
+ public class DrumSampleTriggerSource : GameplaySampleTriggerSource
+ {
+ public DrumSampleTriggerSource(HitObjectContainer hitObjectContainer)
+ : base(hitObjectContainer)
+ {
+ }
+
+ public void Play(HitType hitType)
+ {
+ var hitObject = GetMostValidObject();
+
+ if (hitObject == null)
+ return;
+
+ PlaySamples(new ISampleInfo[] { hitObject.SampleControlPoint.GetSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL) });
+ }
+
+ public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead");
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
index 1ca1be1bdf..ddfaf64549 100644
--- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
@@ -2,18 +2,18 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using osuTK;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings;
-using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
-using osu.Game.Rulesets.Taiko.Audio;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
+using osuTK;
namespace osu.Game.Rulesets.Taiko.UI
{
@@ -25,11 +25,11 @@ namespace osu.Game.Rulesets.Taiko.UI
private const float middle_split = 0.025f;
[Cached]
- private DrumSampleContainer sampleContainer;
+ private DrumSampleTriggerSource sampleTriggerSource;
- public InputDrum(ControlPointInfo controlPoints)
+ public InputDrum(HitObjectContainer hitObjectContainer)
{
- sampleContainer = new DrumSampleContainer(controlPoints);
+ sampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer);
RelativeSizeAxes = Axes.Both;
}
@@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Taiko.UI
}
}
}),
- sampleContainer
+ sampleTriggerSource
};
}
@@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Taiko.UI
private readonly Sprite centreHit;
[Resolved]
- private DrumSampleContainer sampleContainer { get; set; }
+ private DrumSampleTriggerSource sampleTriggerSource { get; set; }
public TaikoHalfDrum(bool flipped)
{
@@ -156,21 +156,19 @@ namespace osu.Game.Rulesets.Taiko.UI
Drawable target = null;
Drawable back = null;
- var drumSample = sampleContainer.SampleAt(Time.Current);
-
if (action == CentreAction)
{
target = centreHit;
back = centre;
- drumSample.Centre?.Play();
+ sampleTriggerSource.Play(HitType.Centre);
}
else if (action == RimAction)
{
target = rimHit;
back = rim;
- drumSample.Rim?.Play();
+ sampleTriggerSource.Play(HitType.Rim);
}
if (target != null)
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
index 0d9e08b8b7..d650cab729 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
@@ -8,7 +8,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
-using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Judgements;
@@ -27,8 +26,6 @@ namespace osu.Game.Rulesets.Taiko.UI
{
public class TaikoPlayfield : ScrollingPlayfield
{
- private readonly ControlPointInfo controlPoints;
-
///
/// Default height of a when inside a .
///
@@ -56,11 +53,6 @@ namespace osu.Game.Rulesets.Taiko.UI
private Container hitTargetOffsetContent;
- public TaikoPlayfield(ControlPointInfo controlPoints)
- {
- this.controlPoints = controlPoints;
- }
-
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
@@ -131,7 +123,7 @@ namespace osu.Game.Rulesets.Taiko.UI
Children = new Drawable[]
{
new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundLeft), _ => new PlayfieldBackgroundLeft()),
- new InputDrum(controlPoints)
+ new InputDrum(HitObjectContainer)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
index 855a75117d..96986f06d5 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
@@ -49,6 +49,22 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration));
}
+ [TestCaseSource(nameof(allBeatmaps))]
+ public void TestEncodeDecodeStabilityDoubleConvert(string name)
+ {
+ var decoded = decodeFromLegacy(beatmaps_resource_store.GetStream(name), name);
+ var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded), name);
+
+ // run an extra convert. this is expected to be stable.
+ decodedAfterEncode.beatmap = convert(decodedAfterEncode.beatmap);
+
+ sort(decoded.beatmap);
+ sort(decodedAfterEncode.beatmap);
+
+ Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize()));
+ Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration));
+ }
+
[Test]
public void TestEncodeMultiSegmentSliderWithFloatingPointError()
{
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs
new file mode 100644
index 0000000000..fccc1a377c
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySampleTriggerSource.cs
@@ -0,0 +1,135 @@
+// 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 NUnit.Framework;
+using osu.Game.Audio;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.UI;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneGameplaySampleTriggerSource : PlayerTestScene
+ {
+ private TestGameplaySampleTriggerSource sampleTriggerSource;
+ protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
+
+ private Beatmap beatmap;
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
+ {
+ beatmap = new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ BaseDifficulty = new BeatmapDifficulty { CircleSize = 6, SliderMultiplier = 3 },
+ Ruleset = ruleset
+ }
+ };
+
+ const double start_offset = 8000;
+ const double spacing = 2000;
+
+ double t = start_offset;
+ beatmap.HitObjects.AddRange(new[]
+ {
+ new HitCircle
+ {
+ // intentionally start objects a bit late so we can test the case of no alive objects.
+ StartTime = t += spacing,
+ Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
+ },
+ new HitCircle
+ {
+ StartTime = t += spacing,
+ Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) }
+ },
+ new HitCircle
+ {
+ StartTime = t += spacing,
+ Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) },
+ SampleControlPoint = new SampleControlPoint { SampleBank = "soft" },
+ },
+ new HitCircle
+ {
+ StartTime = t + spacing,
+ Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) },
+ SampleControlPoint = new SampleControlPoint { SampleBank = "soft" },
+ },
+ });
+
+ return beatmap;
+ }
+
+ public override void SetUpSteps()
+ {
+ base.SetUpSteps();
+
+ AddStep("Add trigger source", () => Player.HUDOverlay.Add(sampleTriggerSource = new TestGameplaySampleTriggerSource(Player.DrawableRuleset.Playfield.HitObjectContainer)));
+ }
+
+ [Test]
+ public void TestCorrectHitObject()
+ {
+ HitObjectLifetimeEntry nextObjectEntry = null;
+
+ AddAssert("no alive objects", () => getNextAliveObject() == null);
+
+ AddAssert("check initially correct object", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[0]);
+
+ AddUntilStep("get next object", () =>
+ {
+ var nextDrawableObject = getNextAliveObject();
+
+ if (nextDrawableObject != null)
+ {
+ nextObjectEntry = nextDrawableObject.Entry;
+ InputManager.MoveMouseTo(nextDrawableObject.ScreenSpaceDrawQuad.Centre);
+ return true;
+ }
+
+ return false;
+ });
+
+ AddUntilStep("hit first hitobject", () =>
+ {
+ InputManager.Click(MouseButton.Left);
+ return nextObjectEntry.Result.HasResult;
+ });
+
+ AddAssert("check correct object after hit", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[1]);
+
+ AddUntilStep("check correct object after miss", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[2]);
+ AddUntilStep("check correct object after miss", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[3]);
+
+ AddUntilStep("no alive objects", () => getNextAliveObject() == null);
+ AddAssert("check correct object after none alive", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[3]);
+ }
+
+ private DrawableHitObject getNextAliveObject() =>
+ Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.FirstOrDefault();
+
+ [Test]
+ public void TestSampleTriggering()
+ {
+ AddRepeatStep("trigger sample", () => sampleTriggerSource.Play(), 10);
+ }
+
+ public class TestGameplaySampleTriggerSource : GameplaySampleTriggerSource
+ {
+ public TestGameplaySampleTriggerSource(HitObjectContainer hitObjectContainer)
+ : base(hitObjectContainer)
+ {
+ }
+
+ public new HitObject GetMostValidObject() => base.GetMostValidObject();
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
index 18e4a6c575..bfcb55ce33 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
@@ -19,6 +20,7 @@ using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Users;
+using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Multiplayer
{
@@ -78,7 +80,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestGeneral()
{
- int[] userIds = Enumerable.Range(0, 4).Select(i => PLAYER_1_ID + i).ToArray();
+ int[] userIds = getPlayerIds(4);
start(userIds);
loadSpectateScreen();
@@ -314,6 +316,36 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("player 2 playing from correct point in time", () => getPlayer(PLAYER_2_ID).ChildrenOfType().Single().FrameStableClock.CurrentTime > 30000);
}
+ [Test]
+ public void TestPlayersLeaveWhileSpectating()
+ {
+ start(getPlayerIds(4));
+ sendFrames(getPlayerIds(4), 300);
+
+ loadSpectateScreen();
+
+ for (int count = 3; count >= 0; count--)
+ {
+ var id = PLAYER_1_ID + count;
+
+ end(id);
+ AddUntilStep($"{id} area grayed", () => getInstance(id).Colour != Color4.White);
+ AddUntilStep($"{id} score quit set", () => getLeaderboardScore(id).HasQuit.Value);
+ sendFrames(getPlayerIds(count), 300);
+ }
+
+ Player player = null;
+
+ AddStep($"get {PLAYER_1_ID} player instance", () => player = getInstance(PLAYER_1_ID).ChildrenOfType().Single());
+
+ start(new[] { PLAYER_1_ID });
+ sendFrames(PLAYER_1_ID, 300);
+
+ AddAssert($"{PLAYER_1_ID} player instance still same", () => getInstance(PLAYER_1_ID).ChildrenOfType().Single() == player);
+ AddAssert($"{PLAYER_1_ID} area still grayed", () => getInstance(PLAYER_1_ID).Colour != Color4.White);
+ AddAssert($"{PLAYER_1_ID} score quit still set", () => getLeaderboardScore(PLAYER_1_ID).HasQuit.Value);
+ }
+
private void loadSpectateScreen(bool waitForPlayerLoad = true)
{
AddStep("load screen", () =>
@@ -333,14 +365,32 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
foreach (int id in userIds)
{
- OnlinePlayDependencies.Client.AddUser(new User { Id = id }, true);
+ var user = new MultiplayerRoomUser(id)
+ {
+ User = new User { Id = id },
+ };
+ OnlinePlayDependencies.Client.AddUser(user.User, true);
SpectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId);
- playingUsers.Add(new MultiplayerRoomUser(id));
+
+ playingUsers.Add(user);
}
});
}
+ private void end(int userId)
+ {
+ AddStep($"end play for {userId}", () =>
+ {
+ var user = playingUsers.Single(u => u.UserID == userId);
+
+ OnlinePlayDependencies.Client.RemoveUser(user.User.AsNonNull());
+ SpectatorClient.EndPlay(userId);
+
+ playingUsers.Remove(user);
+ });
+ }
+
private void sendFrames(int userId, int count = 10) => sendFrames(new[] { userId }, count);
private void sendFrames(int[] userIds, int count = 10)
@@ -374,5 +424,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
private Player getPlayer(int userId) => getInstance(userId).ChildrenOfType().Single();
private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType().Single(p => p.UserId == userId);
+
+ private GameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType().Single(s => s.User?.Id == userId);
+
+ private int[] getPlayerIds(int count) => Enumerable.Range(PLAYER_1_ID, count).ToArray();
}
}
diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs
index c56fec67aa..1844b193f2 100644
--- a/osu.Game/Beatmaps/BeatmapDifficulty.cs
+++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs
@@ -32,7 +32,23 @@ namespace osu.Game.Beatmaps
///
/// Returns a shallow-clone of this .
///
- public BeatmapDifficulty Clone() => (BeatmapDifficulty)MemberwiseClone();
+ public BeatmapDifficulty Clone()
+ {
+ var diff = new BeatmapDifficulty();
+ CopyTo(diff);
+ return diff;
+ }
+
+ public void CopyTo(BeatmapDifficulty difficulty)
+ {
+ difficulty.ApproachRate = ApproachRate;
+ difficulty.DrainRate = DrainRate;
+ difficulty.CircleSize = CircleSize;
+ difficulty.OverallDifficulty = OverallDifficulty;
+
+ difficulty.SliderMultiplier = SliderMultiplier;
+ difficulty.SliderTickRate = SliderTickRate;
+ }
///
/// Maps a difficulty value [0, 10] to a two-piece linear range of values.
diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs
index 3210ef0112..199f719893 100644
--- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs
+++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs
@@ -259,10 +259,10 @@ namespace osu.Game.Beatmaps.Drawables
private readonly IBindable starDifficulty = new Bindable();
- public bool SetContent(object content)
+ public void SetContent(object content)
{
if (!(content is DifficultyIconTooltipContent iconContent))
- return false;
+ return;
difficultyName.Text = iconContent.Beatmap.Version;
@@ -273,8 +273,6 @@ namespace osu.Game.Beatmaps.Drawables
starRating.Text = $"{difficulty.NewValue.Stars:0.##}";
difficultyFlow.Colour = colours.ForStarDifficulty(difficulty.NewValue.Stars);
}, true);
-
- return true;
}
public void Move(Vector2 pos) => Position = pos;
diff --git a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs
index c239fda455..dde7680989 100644
--- a/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs
+++ b/osu.Game/Beatmaps/Drawables/StarRatingDisplay.cs
@@ -4,12 +4,12 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
-using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
index 246dc991d5..fb6806a13d 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
@@ -24,6 +24,12 @@ namespace osu.Game.Beatmaps.Formats
{
public const int LATEST_VERSION = 128;
+ ///
+ /// osu! is generally slower than taiko, so a factor is added to increase
+ /// speed. This must be used everywhere slider length or beat length is used.
+ ///
+ public const float LEGACY_TAIKO_VELOCITY_MULTIPLIER = 1.4f;
+
private readonly IBeatmap beatmap;
[CanBeNull]
@@ -140,9 +146,9 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine(FormattableString.Invariant($"OverallDifficulty: {beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty}"));
writer.WriteLine(FormattableString.Invariant($"ApproachRate: {beatmap.BeatmapInfo.BaseDifficulty.ApproachRate}"));
- // Taiko adjusts the slider multiplier (see: TaikoBeatmapConverter.LEGACY_VELOCITY_MULTIPLIER)
+ // Taiko adjusts the slider multiplier (see: LEGACY_TAIKO_VELOCITY_MULTIPLIER)
writer.WriteLine(beatmap.BeatmapInfo.RulesetID == 1
- ? FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / 1.4f}")
+ ? FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / LEGACY_TAIKO_VELOCITY_MULTIPLIER}")
: FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier}"));
writer.WriteLine(FormattableString.Invariant($"SliderTickRate: {beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate}"));
diff --git a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs
index 81dca99ddd..35d7b4e795 100644
--- a/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs
+++ b/osu.Game/Graphics/Cursor/OsuTooltipContainer.cs
@@ -31,12 +31,9 @@ namespace osu.Game.Graphics.Cursor
private readonly OsuSpriteText text;
private bool instantMovement = true;
- public override bool SetContent(object content)
+ public override void SetContent(LocalisableString contentString)
{
- if (!(content is LocalisableString contentString))
- return false;
-
- if (contentString == text.Text) return true;
+ if (contentString == text.Text) return;
text.Text = contentString;
@@ -47,8 +44,6 @@ namespace osu.Game.Graphics.Cursor
}
else
AutoSizeDuration = 0;
-
- return true;
}
public OsuTooltip()
diff --git a/osu.Game/Graphics/DateTooltip.cs b/osu.Game/Graphics/DateTooltip.cs
index 67fcab43f7..3094f9cc2b 100644
--- a/osu.Game/Graphics/DateTooltip.cs
+++ b/osu.Game/Graphics/DateTooltip.cs
@@ -12,7 +12,7 @@ using osuTK;
namespace osu.Game.Graphics
{
- public class DateTooltip : VisibilityContainer, ITooltip
+ public class DateTooltip : VisibilityContainer, ITooltip
{
private readonly OsuSpriteText dateText, timeText;
private readonly Box background;
@@ -63,14 +63,10 @@ namespace osu.Game.Graphics
protected override void PopIn() => this.FadeIn(200, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(200, Easing.OutQuint);
- public bool SetContent(object content)
+ public void SetContent(DateTimeOffset date)
{
- if (!(content is DateTimeOffset date))
- return false;
-
dateText.Text = $"{date:d MMMM yyyy} ";
timeText.Text = $"{date:HH:mm:ss \"UTC\"z}";
- return true;
}
public void Move(Vector2 pos) => Position = pos;
diff --git a/osu.Game/Overlays/BeatmapSet/BasicStats.cs b/osu.Game/Overlays/BeatmapSet/BasicStats.cs
index 2dcb2f1777..5a6cde8229 100644
--- a/osu.Game/Overlays/BeatmapSet/BasicStats.cs
+++ b/osu.Game/Overlays/BeatmapSet/BasicStats.cs
@@ -4,6 +4,7 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs
index b16fb76ec3..60e341d2ac 100644
--- a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs
+++ b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs
@@ -6,12 +6,12 @@ using System.Linq;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
-using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
index a154016824..8fe1d35b62 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
@@ -4,6 +4,7 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
@@ -192,6 +193,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
if (showPerformancePoints)
{
+ Debug.Assert(score.PP != null);
+
content.Add(new OsuSpriteText
{
Text = score.PP.ToLocalisableString(@"N0"),
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs
index 23069eccdf..883e83ce6e 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs
@@ -116,7 +116,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
maxComboColumn.Text = value.MaxCombo.ToLocalisableString(@"0\x");
ppColumn.Alpha = value.Beatmap?.Status.GrantsPerformancePoints() == true ? 1 : 0;
- ppColumn.Text = value.PP.ToLocalisableString(@"N0");
+ ppColumn.Text = value.PP?.ToLocalisableString(@"N0");
statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(createStatisticsColumn);
modsColumn.Mods = value.Mods;
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs
index c934020059..5c3906cb39 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreUserSection.cs
@@ -3,6 +3,7 @@
using System;
using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
@@ -127,7 +128,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
public int? ScorePosition
{
- set => rankText.Text = value == null ? (LocalisableString)"-" : value.ToLocalisableString(@"\##");
+ set => rankText.Text = value?.ToLocalisableString(@"\##") ?? (LocalisableString)"-";
}
///
diff --git a/osu.Game/Overlays/BeatmapSet/SuccessRate.cs b/osu.Game/Overlays/BeatmapSet/SuccessRate.cs
index b1e9abe3aa..cde4589c98 100644
--- a/osu.Game/Overlays/BeatmapSet/SuccessRate.cs
+++ b/osu.Game/Overlays/BeatmapSet/SuccessRate.cs
@@ -2,9 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
diff --git a/osu.Game/Overlays/Mods/ModButtonTooltip.cs b/osu.Game/Overlays/Mods/ModButtonTooltip.cs
index 666ed07e28..89fcd61d76 100644
--- a/osu.Game/Overlays/Mods/ModButtonTooltip.cs
+++ b/osu.Game/Overlays/Mods/ModButtonTooltip.cs
@@ -18,7 +18,7 @@ using osuTK;
namespace osu.Game.Overlays.Mods
{
- public class ModButtonTooltip : VisibilityContainer, ITooltip
+ public class ModButtonTooltip : VisibilityContainer, ITooltip
{
private readonly OsuSpriteText descriptionText;
private readonly Box background;
@@ -82,12 +82,9 @@ namespace osu.Game.Overlays.Mods
private Mod lastMod;
- public bool SetContent(object content)
+ public void SetContent(Mod mod)
{
- if (!(content is Mod mod))
- return false;
-
- if (mod.Equals(lastMod)) return true;
+ if (mod.Equals(lastMod)) return;
lastMod = mod;
@@ -99,15 +96,7 @@ namespace osu.Game.Overlays.Mods
incompatibleMods.Value = allMods.Where(m => m.GetType() != mod.GetType() && incompatibleTypes.Any(t => t.IsInstanceOfType(m))).ToList();
- if (!incompatibleMods.Value.Any())
- {
- incompatibleText.Text = "Compatible with all mods";
- return true;
- }
-
- incompatibleText.Text = "Incompatible with:";
-
- return true;
+ incompatibleText.Text = !incompatibleMods.Value.Any() ? "Compatible with all mods" : "Incompatible with:";
}
public void Move(Vector2 pos) => Position = pos;
diff --git a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs
index f15fa2705a..180a288729 100644
--- a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
diff --git a/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs b/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs
index 877637be22..2c8c421eba 100644
--- a/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
diff --git a/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs b/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs
index 1235836aac..b098f9f840 100644
--- a/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs
@@ -1,10 +1,10 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
-using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
diff --git a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs
index 74a25591b4..7ba8ae7c80 100644
--- a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using Humanizer;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Graphics;
@@ -80,14 +81,13 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
}
- public override bool SetContent(object content)
+ public override void SetContent(object content)
{
if (!(content is TooltipDisplayContent info))
- return false;
+ return;
Counter.Text = info.Rank;
BottomText.Text = info.Time;
- return true;
}
}
diff --git a/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs
index 9e52751904..8ca6961950 100644
--- a/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs
index 438f52a2ce..cf930e985c 100644
--- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs
@@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
diff --git a/osu.Game/Overlays/Profile/Sections/CounterPill.cs b/osu.Game/Overlays/Profile/Sections/CounterPill.cs
index 34211b40b7..bd6cb4d09b 100644
--- a/osu.Game/Overlays/Profile/Sections/CounterPill.cs
+++ b/osu.Game/Overlays/Profile/Sections/CounterPill.cs
@@ -8,7 +8,7 @@ using osu.Game.Graphics;
using osu.Framework.Bindables;
using osu.Game.Graphics.Sprites;
using osu.Framework.Allocation;
-using osu.Framework.Localisation;
+using osu.Framework.Extensions.LocalisationExtensions;
namespace osu.Game.Overlays.Profile.Sections
{
diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs
index 449b1da35d..a75235359a 100644
--- a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs
+++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs
@@ -9,6 +9,7 @@ using System.Linq;
using osu.Game.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Game.Graphics;
using osu.Framework.Graphics.Shapes;
using osuTK;
diff --git a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs
index ac94f0fc87..85287d2325 100644
--- a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs
+++ b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Localisation;
using static osu.Game.Users.User;
@@ -49,14 +50,13 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
this.tooltipCounterName = tooltipCounterName;
}
- public override bool SetContent(object content)
+ public override void SetContent(object content)
{
if (!(content is TooltipDisplayContent info) || info.Name != tooltipCounterName)
- return false;
+ return;
Counter.Text = info.Count;
BottomText.Text = info.Date;
- return true;
}
}
diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs
index eb55a0a78d..762716efab 100644
--- a/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs
+++ b/osu.Game/Overlays/Profile/Sections/Kudosu/KudosuInfo.cs
@@ -12,6 +12,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Users;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Game.Resources.Localisation.Web;
using osu.Framework.Localisation;
diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileWeightedScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileWeightedScore.cs
index 4e4a665a60..f77464ecb9 100644
--- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileWeightedScore.cs
+++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileWeightedScore.cs
@@ -1,9 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
diff --git a/osu.Game/Overlays/Profile/UserGraph.cs b/osu.Game/Overlays/Profile/UserGraph.cs
index b7a08b6c5e..b88cc32ff7 100644
--- a/osu.Game/Overlays/Profile/UserGraph.cs
+++ b/osu.Game/Overlays/Profile/UserGraph.cs
@@ -268,7 +268,7 @@ namespace osu.Game.Overlays.Profile
background.Colour = colours.Gray1;
}
- public abstract bool SetContent(object content);
+ public abstract void SetContent(object content);
private bool instantMove = true;
diff --git a/osu.Game/Overlays/Rankings/SpotlightSelector.cs b/osu.Game/Overlays/Rankings/SpotlightSelector.cs
index 5309778a47..0f071883ca 100644
--- a/osu.Game/Overlays/Rankings/SpotlightSelector.cs
+++ b/osu.Game/Overlays/Rankings/SpotlightSelector.cs
@@ -16,6 +16,7 @@ using System.Collections.Generic;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Online.API.Requests;
using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
diff --git a/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs b/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs
index 85a317728f..a908380e95 100644
--- a/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs
+++ b/osu.Game/Overlays/Rankings/Tables/CountriesTable.cs
@@ -9,8 +9,8 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using System.Collections.Generic;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Game.Resources.Localisation.Web;
-using osu.Framework.Localisation;
namespace osu.Game.Overlays.Rankings.Tables
{
diff --git a/osu.Game/Overlays/Rankings/Tables/PerformanceTable.cs b/osu.Game/Overlays/Rankings/Tables/PerformanceTable.cs
index 6facf1e7a2..17c17b1f1a 100644
--- a/osu.Game/Overlays/Rankings/Tables/PerformanceTable.cs
+++ b/osu.Game/Overlays/Rankings/Tables/PerformanceTable.cs
@@ -2,9 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Users;
@@ -24,7 +24,7 @@ namespace osu.Game.Overlays.Rankings.Tables
protected override Drawable[] CreateUniqueContent(UserStatistics item) => new Drawable[]
{
- new RowText { Text = item.PP.ToLocalisableString(@"N0"), }
+ new RowText { Text = item.PP?.ToLocalisableString(@"N0"), }
};
}
}
diff --git a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs
index bc8eac16a9..6e6230f958 100644
--- a/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs
+++ b/osu.Game/Overlays/Rankings/Tables/RankingsTable.cs
@@ -10,6 +10,7 @@ using osu.Framework.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Game.Users;
using osu.Game.Users.Drawables;
using osuTK;
diff --git a/osu.Game/Overlays/Rankings/Tables/ScoresTable.cs b/osu.Game/Overlays/Rankings/Tables/ScoresTable.cs
index b6bb66e2c8..934da4501e 100644
--- a/osu.Game/Overlays/Rankings/Tables/ScoresTable.cs
+++ b/osu.Game/Overlays/Rankings/Tables/ScoresTable.cs
@@ -2,9 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Users;
diff --git a/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs b/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs
index b96ab556df..cc2ef55a2b 100644
--- a/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs
+++ b/osu.Game/Overlays/Rankings/Tables/UserBasedTable.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
diff --git a/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs
new file mode 100644
index 0000000000..c18698f77e
--- /dev/null
+++ b/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs
@@ -0,0 +1,104 @@
+// 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.Graphics.Containers;
+using osu.Game.Audio;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.UI
+{
+ ///
+ /// A component which can trigger the most appropriate hit sound for a given point in time, based on the state of a
+ ///
+ public class GameplaySampleTriggerSource : CompositeDrawable
+ {
+ ///
+ /// The number of concurrent samples allowed to be played concurrently so that it feels better when spam-pressing a key.
+ ///
+ private const int max_concurrent_hitsounds = OsuGameBase.SAMPLE_CONCURRENCY;
+
+ private readonly HitObjectContainer hitObjectContainer;
+
+ private int nextHitSoundIndex;
+
+ private readonly Container hitSounds;
+
+ public GameplaySampleTriggerSource(HitObjectContainer hitObjectContainer)
+ {
+ this.hitObjectContainer = hitObjectContainer;
+
+ InternalChild = hitSounds = new Container
+ {
+ Name = "concurrent sample pool",
+ ChildrenEnumerable = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new PausableSkinnableSound())
+ };
+ }
+
+ private HitObjectLifetimeEntry fallbackObject;
+
+ ///
+ /// Play the most appropriate hit sound for the current point in time.
+ ///
+ public virtual void Play()
+ {
+ var nextObject = GetMostValidObject();
+
+ if (nextObject == null)
+ return;
+
+ var samples = nextObject.Samples
+ .Select(s => nextObject.SampleControlPoint.ApplyTo(s))
+ .Cast()
+ .ToArray();
+
+ PlaySamples(samples);
+ }
+
+ protected void PlaySamples(ISampleInfo[] samples)
+ {
+ var hitSound = getNextSample();
+ hitSound.Samples = samples;
+ hitSound.Play();
+ }
+
+ protected HitObject GetMostValidObject()
+ {
+ // The most optimal lookup case we have is when an object is alive. There are usually very few alive objects so there's no drawbacks in attempting this lookup each time.
+ var hitObject = hitObjectContainer.AliveObjects.FirstOrDefault(h => h.Result?.HasResult != true)?.HitObject;
+
+ // In the case a next object isn't available in drawable form, we need to do a somewhat expensive traversal to get a valid sound to play.
+ if (hitObject == null)
+ {
+ // This lookup can be skipped if the last entry is still valid (in the future and not yet hit).
+ if (fallbackObject == null || fallbackObject.Result?.HasResult == true)
+ {
+ // We need to use lifetime entries to find the next object (we can't just use `hitObjectContainer.Objects` due to pooling - it may even be empty).
+ // If required, we can make this lookup more efficient by adding support to get next-future-entry in LifetimeEntryManager.
+ fallbackObject = hitObjectContainer.Entries
+ .Where(e => e.Result?.HasResult != true)
+ .OrderBy(e => e.HitObject.StartTime)
+ .FirstOrDefault();
+
+ // In the case there are no unjudged objects, the last hit object should be used instead.
+ fallbackObject ??= hitObjectContainer.Entries.LastOrDefault();
+ }
+
+ hitObject = fallbackObject?.HitObject;
+ }
+
+ return hitObject;
+ }
+
+ private SkinnableSound getNextSample()
+ {
+ SkinnableSound hitSound = hitSounds[nextHitSoundIndex];
+
+ // round robin over available samples to allow for concurrent playback.
+ nextHitSoundIndex = (nextHitSoundIndex + 1) % max_concurrent_hitsounds;
+
+ return hitSound;
+ }
+ }
+}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs
index cf0dfbb585..b8f47c16ff 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs
@@ -61,7 +61,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
playerClocks.Add(clock);
}
- public void RemovePlayerClock(ISpectatorPlayerClock clock) => playerClocks.Remove(clock);
+ public void RemovePlayerClock(ISpectatorPlayerClock clock)
+ {
+ playerClocks.Remove(clock);
+ clock.Stop();
+ }
protected override void Update()
{
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs
index d10917259d..bf7c738882 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs
@@ -8,6 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator;
using osu.Game.Screens.Play;
@@ -32,6 +33,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
///
public bool AllPlayersLoaded => instances.All(p => p?.PlayerLoaded == true);
+ [Resolved]
+ private OsuColour colours { get; set; }
+
[Resolved]
private SpectatorClient spectatorClient { get; set; }
@@ -215,6 +219,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
protected override void EndGameplay(int userId)
{
RemoveUser(userId);
+
+ var instance = instances.Single(i => i.UserId == userId);
+
+ instance.FadeColour(colours.Gray4, 400, Easing.OutQuint);
+ syncManager.RemovePlayerClock(instance.GameplayClock);
leaderboard.RemoveClock(userId);
}
diff --git a/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs b/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs
index 68e3f0df7d..d04e60a2ab 100644
--- a/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs
+++ b/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs
@@ -4,6 +4,7 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs
index 820d776e63..4520e2e825 100644
--- a/osu.Game/Screens/Play/SoloSpectator.cs
+++ b/osu.Game/Screens/Play/SoloSpectator.cs
@@ -211,7 +211,7 @@ namespace osu.Game.Screens.Play
Beatmap.Value = gameplayState.Beatmap;
Ruleset.Value = gameplayState.Ruleset.RulesetInfo;
- this.Push(new SpectatorPlayerLoader(gameplayState.Score));
+ this.Push(new SpectatorPlayerLoader(gameplayState.Score, () => new SoloSpectatorPlayer(gameplayState.Score)));
}
}
diff --git a/osu.Game/Screens/Play/SoloSpectatorPlayer.cs b/osu.Game/Screens/Play/SoloSpectatorPlayer.cs
new file mode 100644
index 0000000000..969a5bf2b4
--- /dev/null
+++ b/osu.Game/Screens/Play/SoloSpectatorPlayer.cs
@@ -0,0 +1,52 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Screens;
+using osu.Game.Online.Spectator;
+using osu.Game.Scoring;
+
+namespace osu.Game.Screens.Play
+{
+ public class SoloSpectatorPlayer : SpectatorPlayer
+ {
+ private readonly Score score;
+
+ public SoloSpectatorPlayer(Score score, PlayerConfiguration configuration = null)
+ : base(score, configuration)
+ {
+ this.score = score;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ SpectatorClient.OnUserBeganPlaying += userBeganPlaying;
+ }
+
+ public override bool OnExiting(IScreen next)
+ {
+ SpectatorClient.OnUserBeganPlaying -= userBeganPlaying;
+
+ return base.OnExiting(next);
+ }
+
+ private void userBeganPlaying(int userId, SpectatorState state)
+ {
+ if (userId != score.ScoreInfo.UserID) return;
+
+ Schedule(() =>
+ {
+ if (this.IsCurrentScreen()) this.Exit();
+ });
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (SpectatorClient != null)
+ SpectatorClient.OnUserBeganPlaying -= userBeganPlaying;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs
index 1dae28092a..d7e42a9cd1 100644
--- a/osu.Game/Screens/Play/SpectatorPlayer.cs
+++ b/osu.Game/Screens/Play/SpectatorPlayer.cs
@@ -14,16 +14,16 @@ using osu.Game.Screens.Ranking;
namespace osu.Game.Screens.Play
{
- public class SpectatorPlayer : Player
+ public abstract class SpectatorPlayer : Player
{
[Resolved]
- private SpectatorClient spectatorClient { get; set; }
+ protected SpectatorClient SpectatorClient { get; private set; }
private readonly Score score;
protected override bool CheckModsAllowFailure() => false; // todo: better support starting mid-way through beatmap
- public SpectatorPlayer(Score score, PlayerConfiguration configuration = null)
+ protected SpectatorPlayer(Score score, PlayerConfiguration configuration = null)
: base(configuration)
{
this.score = score;
@@ -32,8 +32,6 @@ namespace osu.Game.Screens.Play
[BackgroundDependencyLoader]
private void load()
{
- spectatorClient.OnUserBeganPlaying += userBeganPlaying;
-
AddInternal(new OsuSpriteText
{
Text = $"Watching {score.ScoreInfo.User.Username} playing live!",
@@ -50,7 +48,7 @@ namespace osu.Game.Screens.Play
// Start gameplay along with the very first arrival frame (the latest one).
score.Replay.Frames.Clear();
- spectatorClient.OnNewFrames += userSentFrames;
+ SpectatorClient.OnNewFrames += userSentFrames;
}
private void userSentFrames(int userId, FrameDataBundle bundle)
@@ -93,31 +91,17 @@ namespace osu.Game.Screens.Play
public override bool OnExiting(IScreen next)
{
- spectatorClient.OnUserBeganPlaying -= userBeganPlaying;
- spectatorClient.OnNewFrames -= userSentFrames;
+ SpectatorClient.OnNewFrames -= userSentFrames;
return base.OnExiting(next);
}
- private void userBeganPlaying(int userId, SpectatorState state)
- {
- if (userId != score.ScoreInfo.UserID) return;
-
- Schedule(() =>
- {
- if (this.IsCurrentScreen()) this.Exit();
- });
- }
-
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
- if (spectatorClient != null)
- {
- spectatorClient.OnUserBeganPlaying -= userBeganPlaying;
- spectatorClient.OnNewFrames -= userSentFrames;
- }
+ if (SpectatorClient != null)
+ SpectatorClient.OnNewFrames -= userSentFrames;
}
}
}
diff --git a/osu.Game/Screens/Play/SpectatorPlayerLoader.cs b/osu.Game/Screens/Play/SpectatorPlayerLoader.cs
index bdd23962dc..10cc36c9a9 100644
--- a/osu.Game/Screens/Play/SpectatorPlayerLoader.cs
+++ b/osu.Game/Screens/Play/SpectatorPlayerLoader.cs
@@ -11,12 +11,7 @@ namespace osu.Game.Screens.Play
{
public readonly ScoreInfo Score;
- public SpectatorPlayerLoader(Score score)
- : this(score, () => new SpectatorPlayer(score))
- {
- }
-
- public SpectatorPlayerLoader(Score score, Func createPlayer)
+ public SpectatorPlayerLoader(Score score, Func createPlayer)
: base(createPlayer)
{
if (score.Replay == null)
diff --git a/osu.Game/Screens/Select/Details/UserRatings.cs b/osu.Game/Screens/Select/Details/UserRatings.cs
index a7f28b932a..eabc476db9 100644
--- a/osu.Game/Screens/Select/Details/UserRatings.cs
+++ b/osu.Game/Screens/Select/Details/UserRatings.cs
@@ -8,8 +8,8 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using System.Linq;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Game.Beatmaps;
-using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Screens.Select.Details
diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs
index 72d10019b2..a882148392 100644
--- a/osu.Game/Screens/Select/FilterQueryParser.cs
+++ b/osu.Game/Screens/Select/FilterQueryParser.cs
@@ -3,8 +3,8 @@
using System;
using System.Globalization;
+using System.Linq;
using System.Text.RegularExpressions;
-using osu.Game.Beatmaps;
using osu.Game.Screens.Select.Filter;
namespace osu.Game.Screens.Select
@@ -64,8 +64,7 @@ namespace osu.Game.Screens.Select
return TryUpdateCriteriaRange(ref criteria.BeatDivisor, op, value, tryParseInt);
case "status":
- return TryUpdateCriteriaRange(ref criteria.OnlineStatus, op, value,
- (string s, out BeatmapSetOnlineStatus val) => Enum.TryParse(value, true, out val));
+ return TryUpdateCriteriaRange(ref criteria.OnlineStatus, op, value, tryParseEnum);
case "creator":
return TryUpdateCriteriaText(ref criteria.Creator, op, value);
@@ -120,6 +119,14 @@ namespace osu.Game.Screens.Select
private static bool tryParseInt(string value, out int result) =>
int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out result);
+ private static bool tryParseEnum(string value, out TEnum result) where TEnum : struct
+ {
+ if (Enum.TryParse(value, true, out result)) return true;
+
+ value = Enum.GetNames(typeof(TEnum)).FirstOrDefault(name => name.StartsWith(value, true, CultureInfo.InvariantCulture));
+ return Enum.TryParse(value, true, out result);
+ }
+
///
/// Attempts to parse a keyword filter with the specified and textual .
/// If the value indicates a valid textual filter, the function returns true and the resulting data is stored into
diff --git a/osu.Game/Utils/FormatUtils.cs b/osu.Game/Utils/FormatUtils.cs
index e763558647..d14dbb49f3 100644
--- a/osu.Game/Utils/FormatUtils.cs
+++ b/osu.Game/Utils/FormatUtils.cs
@@ -3,6 +3,7 @@
using System;
using Humanizer;
+using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Localisation;
namespace osu.Game.Utils
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index a89d57cd1f..b4d4aa3070 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,7 +36,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index bb4700a081..29e9b9fe20 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -93,7 +93,7 @@
-
+