diff --git a/osu.Android.props b/osu.Android.props
index d7817cf4cf..3df894fbcc 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs
index 05c8e835ac..71f9fafe57 100644
--- a/osu.Desktop/Updater/SquirrelUpdateManager.cs
+++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs
@@ -29,6 +29,11 @@ namespace osu.Desktop.Updater
private static readonly Logger logger = Logger.GetLogger("updater");
+ ///
+ /// Whether an update has been downloaded but not yet applied.
+ ///
+ private bool updatePending;
+
[BackgroundDependencyLoader]
private void load(NotificationOverlay notification)
{
@@ -37,9 +42,9 @@ namespace osu.Desktop.Updater
Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger));
}
- protected override async Task PerformUpdateCheck() => await checkForUpdateAsync();
+ protected override async Task PerformUpdateCheck() => await checkForUpdateAsync();
- private async Task checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null)
+ private async Task checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null)
{
// should we schedule a retry on completion of this check?
bool scheduleRecheck = true;
@@ -49,9 +54,19 @@ namespace osu.Desktop.Updater
updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true);
var info = await updateManager.CheckForUpdate(!useDeltaPatching);
+
if (info.ReleasesToApply.Count == 0)
+ {
+ if (updatePending)
+ {
+ // the user may have dismissed the completion notice, so show it again.
+ notificationOverlay.Post(new UpdateCompleteNotification(this));
+ return true;
+ }
+
// no updates available. bail and retry later.
- return;
+ return false;
+ }
if (notification == null)
{
@@ -72,6 +87,7 @@ namespace osu.Desktop.Updater
await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f);
notification.State = ProgressNotificationState.Completed;
+ updatePending = true;
}
catch (Exception e)
{
@@ -103,6 +119,8 @@ namespace osu.Desktop.Updater
Scheduler.AddDelayed(async () => await checkForUpdateAsync(), 60000 * 30);
}
}
+
+ return true;
}
protected override void Dispose(bool isDisposing)
@@ -111,10 +129,27 @@ namespace osu.Desktop.Updater
updateManager?.Dispose();
}
+ private class UpdateCompleteNotification : ProgressCompletionNotification
+ {
+ [Resolved]
+ private OsuGame game { get; set; }
+
+ public UpdateCompleteNotification(SquirrelUpdateManager updateManager)
+ {
+ Text = @"Update ready to install. Click to restart!";
+
+ Activated = () =>
+ {
+ updateManager.PrepareUpdateAsync()
+ .ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit()));
+ return true;
+ };
+ }
+ }
+
private class UpdateProgressNotification : ProgressNotification
{
private readonly SquirrelUpdateManager updateManager;
- private OsuGame game;
public UpdateProgressNotification(SquirrelUpdateManager updateManager)
{
@@ -123,23 +158,12 @@ namespace osu.Desktop.Updater
protected override Notification CreateCompletionNotification()
{
- return new ProgressCompletionNotification
- {
- Text = @"Update ready to install. Click to restart!",
- Activated = () =>
- {
- updateManager.PrepareUpdateAsync()
- .ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit()));
- return true;
- }
- };
+ return new UpdateCompleteNotification(updateManager);
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours, OsuGame game)
+ private void load(OsuColour colours)
{
- this.game = game;
-
IconContent.AddRange(new Drawable[]
{
new Box
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index 1f27de3352..ad584d3f48 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -141,6 +141,35 @@ namespace osu.Game.Rulesets.Catch
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetCatch };
+ protected override IEnumerable GetValidHitResults()
+ {
+ return new[]
+ {
+ HitResult.Great,
+
+ HitResult.LargeTickHit,
+ HitResult.SmallTickHit,
+ HitResult.LargeBonus,
+ };
+ }
+
+ public override string GetDisplayNameForHitResult(HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.LargeTickHit:
+ return "large droplet";
+
+ case HitResult.SmallTickHit:
+ return "small droplet";
+
+ case HitResult.LargeBonus:
+ return "banana";
+ }
+
+ return base.GetDisplayNameForHitResult(result);
+ }
+
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap);
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source);
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index ecb09ebe85..b92e042686 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -319,6 +319,31 @@ namespace osu.Game.Rulesets.Mania
return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast().OrderByDescending(i => i).First(v => variant >= v);
}
+ protected override IEnumerable GetValidHitResults()
+ {
+ return new[]
+ {
+ HitResult.Perfect,
+ HitResult.Great,
+ HitResult.Good,
+ HitResult.Ok,
+ HitResult.Meh,
+
+ HitResult.LargeTickHit,
+ };
+ }
+
+ public override string GetDisplayNameForHitResult(HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.LargeTickHit:
+ return "hold tick";
+ }
+
+ return base.GetDisplayNameForHitResult(result);
+ }
+
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
{
new StatisticRow
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
index 9349ef7a18..5581ce4bfd 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
@@ -49,6 +49,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
OriginPosition = body.PathOffset;
}
+ public void RecyclePath() => body.RecyclePath();
+
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => body.ReceivePositionalInputAt(screenSpacePos);
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index f260c5a8fa..d3fb5defae 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -24,10 +24,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
public class SliderSelectionBlueprint : OsuSelectionBlueprint
{
- protected readonly SliderBodyPiece BodyPiece;
- protected readonly SliderCircleSelectionBlueprint HeadBlueprint;
- protected readonly SliderCircleSelectionBlueprint TailBlueprint;
- protected readonly PathControlPointVisualiser ControlPointVisualiser;
+ protected SliderBodyPiece BodyPiece { get; private set; }
+ protected SliderCircleSelectionBlueprint HeadBlueprint { get; private set; }
+ protected SliderCircleSelectionBlueprint TailBlueprint { get; private set; }
+ protected PathControlPointVisualiser ControlPointVisualiser { get; private set; }
+
+ private readonly DrawableSlider slider;
[Resolved(CanBeNull = true)]
private HitObjectComposer composer { get; set; }
@@ -44,17 +46,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
public SliderSelectionBlueprint(DrawableSlider slider)
: base(slider)
{
- var sliderObject = (Slider)slider.HitObject;
+ this.slider = slider;
+ }
+ [BackgroundDependencyLoader]
+ private void load()
+ {
InternalChildren = new Drawable[]
{
BodyPiece = new SliderBodyPiece(),
HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start),
TailBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.End),
- ControlPointVisualiser = new PathControlPointVisualiser(sliderObject, true)
- {
- RemoveControlPointsRequested = removeControlPoints
- }
};
}
@@ -66,13 +68,35 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
pathVersion = HitObject.Path.Version.GetBoundCopy();
pathVersion.BindValueChanged(_ => updatePath());
+
+ BodyPiece.UpdateFrom(HitObject);
}
protected override void Update()
{
base.Update();
- BodyPiece.UpdateFrom(HitObject);
+ if (IsSelected)
+ BodyPiece.UpdateFrom(HitObject);
+ }
+
+ protected override void OnSelected()
+ {
+ AddInternal(ControlPointVisualiser = new PathControlPointVisualiser((Slider)slider.HitObject, true)
+ {
+ RemoveControlPointsRequested = removeControlPoints
+ });
+
+ base.OnSelected();
+ }
+
+ protected override void OnDeselected()
+ {
+ base.OnDeselected();
+
+ // throw away frame buffers on deselection.
+ ControlPointVisualiser?.Expire();
+ BodyPiece.RecyclePath();
}
private Vector2 rightClickPosition;
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
index 7ae0730e39..762c4a04e7 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs
@@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
@@ -25,6 +26,7 @@ namespace osu.Game.Rulesets.Osu.Edit
SelectionBox.CanRotate = canOperate;
SelectionBox.CanScaleX = canOperate;
SelectionBox.CanScaleY = canOperate;
+ SelectionBox.CanReverse = canOperate;
}
protected override void OnOperationEnded()
@@ -41,6 +43,54 @@ namespace osu.Game.Rulesets.Osu.Edit
///
private Vector2? referenceOrigin;
+ public override bool HandleReverse()
+ {
+ var hitObjects = selectedMovableObjects;
+
+ double endTime = hitObjects.Max(h => h.GetEndTime());
+ double startTime = hitObjects.Min(h => h.StartTime);
+
+ bool moreThanOneObject = hitObjects.Length > 1;
+
+ foreach (var h in hitObjects)
+ {
+ if (moreThanOneObject)
+ h.StartTime = endTime - (h.GetEndTime() - startTime);
+
+ if (h is Slider slider)
+ {
+ var points = slider.Path.ControlPoints.ToArray();
+ Vector2 endPos = points.Last().Position.Value;
+
+ slider.Path.ControlPoints.Clear();
+
+ slider.Position += endPos;
+
+ PathType? lastType = null;
+
+ for (var i = 0; i < points.Length; i++)
+ {
+ var p = points[i];
+ p.Position.Value -= endPos;
+
+ // propagate types forwards to last null type
+ if (i == points.Length - 1)
+ p.Type.Value = lastType;
+ else if (p.Type.Value != null)
+ {
+ var newType = p.Type.Value;
+ p.Type.Value = lastType;
+ lastType = newType;
+ }
+
+ slider.Path.ControlPoints.Insert(0, p);
+ }
+ }
+ }
+
+ return true;
+ }
+
public override bool HandleFlip(Direction direction)
{
var hitObjects = selectedMovableObjects;
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
index 80e40af717..f69cacd432 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
@@ -42,7 +42,11 @@ namespace osu.Game.Rulesets.Osu.Mods
private double lastSliderHeadFadeOutStartTime;
private double lastSliderHeadFadeOutDuration;
- protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state)
+ protected override void ApplyFirstObjectIncreaseVisibilityState(DrawableHitObject drawable, ArmedState state) => applyState(drawable, true);
+
+ protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state) => applyState(drawable, false);
+
+ private void applyState(DrawableHitObject drawable, bool increaseVisibility)
{
if (!(drawable is DrawableOsuHitObject d))
return;
@@ -86,14 +90,23 @@ namespace osu.Game.Rulesets.Osu.Mods
lastSliderHeadFadeOutStartTime = fadeOutStartTime;
}
- // we don't want to see the approach circle
- using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
- circle.ApproachCircle.Hide();
+ Drawable fadeTarget = circle;
+
+ if (increaseVisibility)
+ {
+ // only fade the circle piece (not the approach circle) for the increased visibility object.
+ fadeTarget = circle.CirclePiece;
+ }
+ else
+ {
+ // we don't want to see the approach circle
+ using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
+ circle.ApproachCircle.Hide();
+ }
// fade out immediately after fade in.
using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true))
- circle.FadeOut(fadeOutDuration);
-
+ fadeTarget.FadeOut(fadeOutDuration);
break;
case DrawableSlider slider:
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index cc2eebdd36..678fb8aba6 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -191,6 +191,41 @@ namespace osu.Game.Rulesets.Osu
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
+ protected override IEnumerable GetValidHitResults()
+ {
+ return new[]
+ {
+ HitResult.Great,
+ HitResult.Ok,
+ HitResult.Meh,
+
+ HitResult.LargeTickHit,
+ HitResult.SmallTickHit,
+ HitResult.SmallBonus,
+ HitResult.LargeBonus,
+ };
+ }
+
+ public override string GetDisplayNameForHitResult(HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.LargeTickHit:
+ return "slider tick";
+
+ case HitResult.SmallTickHit:
+ return "slider end";
+
+ case HitResult.SmallBonus:
+ return "spinner spin";
+
+ case HitResult.LargeBonus:
+ return "spinner bonus";
+ }
+
+ return base.GetDisplayNameForHitResult(result);
+ }
+
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{
var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList();
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index ed7b8589ba..607eaf5dbd 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x =>
{
TaikoHitObject first = x.First();
- if (x.Skip(1).Any() && !(first is Swell))
+ if (x.Skip(1).Any() && first.CanBeStrong)
first.IsStrong = true;
return first;
}).ToList();
diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
index ee92936fc2..14c6cf31d0 100644
--- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
@@ -89,6 +89,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
yield return new TernaryStateMenuItem("Strong") { State = { BindTarget = selectionStrongState } };
}
+ public override bool HandleMovement(MoveSelectionEvent moveEvent) => true;
+
protected override void UpdateTernaryStates()
{
base.UpdateTernaryStates();
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
index 9cd23383c4..d8d75a7614 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
@@ -158,7 +158,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
base.LoadSamples();
- isStrong.Value = getStrongSamples().Any();
+ if (HitObject.CanBeStrong)
+ isStrong.Value = getStrongSamples().Any();
}
private void updateSamplesFromStrong()
diff --git a/osu.Game.Rulesets.Taiko/Objects/Swell.cs b/osu.Game.Rulesets.Taiko/Objects/Swell.cs
index eeae6e79f8..bf8b7bc178 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Swell.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Swell.cs
@@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
set => Duration = value - StartTime;
}
+ public override bool CanBeStrong => false;
+
public double Duration { get; set; }
///
diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs
index 2922010001..d2c37d965c 100644
--- a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.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.Threading;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Judgements;
@@ -30,6 +31,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
public readonly Bindable IsStrongBindable = new BindableBool();
+ ///
+ /// Whether this can be made a "strong" (large) hit.
+ ///
+ public virtual bool CanBeStrong => true;
+
///
/// Whether this HitObject is a "strong" type.
/// Strong hit objects give more points for hitting the hit object with both keys.
@@ -37,7 +43,13 @@ namespace osu.Game.Rulesets.Taiko.Objects
public bool IsStrong
{
get => IsStrongBindable.Value;
- set => IsStrongBindable.Value = value;
+ set
+ {
+ if (value && !CanBeStrong)
+ throw new InvalidOperationException($"Object of type {GetType()} cannot be strong");
+
+ IsStrongBindable.Value = value;
+ }
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
index 642eb0ddcc..73e9c16d07 100644
--- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
@@ -159,6 +159,33 @@ namespace osu.Game.Rulesets.Taiko
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
+ protected override IEnumerable GetValidHitResults()
+ {
+ return new[]
+ {
+ HitResult.Great,
+ HitResult.Ok,
+
+ HitResult.SmallTickHit,
+
+ HitResult.SmallBonus,
+ };
+ }
+
+ public override string GetDisplayNameForHitResult(HitResult result)
+ {
+ switch (result)
+ {
+ case HitResult.SmallTickHit:
+ return "drum tick";
+
+ case HitResult.SmallBonus:
+ return "strong bonus";
+ }
+
+ return base.GetDisplayNameForHitResult(result);
+ }
+
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{
var timedHitEvents = score.HitEvents.Where(e => e.HitObject is Hit).ToList();
diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
index 19294d12fc..528689e67c 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
@@ -193,6 +193,7 @@ namespace osu.Game.Tests.Visual.Background
AddStep("Transition to Results", () => player.Push(results = new FadeAccessibleResults(new ScoreInfo
{
+ Ruleset = new OsuRuleset().RulesetInfo,
User = new User { Username = "osu!" },
Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo
})));
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs
index e33040acdc..20e58c3d2a 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs
@@ -5,7 +5,6 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Screens.Edit.Compose.Components;
-using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK;
namespace osu.Game.Tests.Visual.Editing
@@ -13,7 +12,7 @@ namespace osu.Game.Tests.Visual.Editing
[TestFixture]
public class TestSceneTimelineTickDisplay : TimelineTestScene
{
- public override Drawable CreateTestComponent() => new TimelineTickDisplay();
+ public override Drawable CreateTestComponent() => Empty(); // tick display is implicitly inside the timeline.
[BackgroundDependencyLoader]
private void load()
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneWaveform.cs b/osu.Game.Tests/Visual/Editing/TestSceneWaveform.cs
index 0c1296b82c..c3a5a0e944 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneWaveform.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneWaveform.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Threading;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@@ -76,7 +77,7 @@ namespace osu.Game.Tests.Visual.Editing
};
});
- AddUntilStep("wait for load", () => graph.ResampledWaveform != null);
+ AddUntilStep("wait for load", () => graph.Loaded.IsSet);
}
[Test]
@@ -98,12 +99,18 @@ namespace osu.Game.Tests.Visual.Editing
};
});
- AddUntilStep("wait for load", () => graph.ResampledWaveform != null);
+ AddUntilStep("wait for load", () => graph.Loaded.IsSet);
}
public class TestWaveformGraph : WaveformGraph
{
- public new Waveform ResampledWaveform => base.ResampledWaveform;
+ public readonly ManualResetEventSlim Loaded = new ManualResetEventSlim();
+
+ protected override void OnWaveformRegenerated(Waveform waveform)
+ {
+ base.OnWaveformRegenerated(waveform);
+ Loaded.Set();
+ }
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs
index ce04b940e7..4fa4c00981 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs
@@ -3,6 +3,7 @@
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Bindables;
using osu.Game.Overlays;
using osu.Game.Rulesets;
@@ -23,33 +24,41 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestGameplayOverlayActivation()
{
+ AddAssert("local user playing", () => Player.LocalUserPlaying.Value);
AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled);
}
[Test]
public void TestGameplayOverlayActivationPaused()
{
+ AddAssert("local user playing", () => Player.LocalUserPlaying.Value);
AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled);
AddStep("pause gameplay", () => Player.Pause());
+ AddAssert("local user not playing", () => !Player.LocalUserPlaying.Value);
AddUntilStep("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered);
}
[Test]
public void TestGameplayOverlayActivationReplayLoaded()
{
+ AddAssert("local user playing", () => Player.LocalUserPlaying.Value);
AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled);
AddStep("load a replay", () => Player.DrawableRuleset.HasReplayLoaded.Value = true);
+ AddAssert("local user not playing", () => !Player.LocalUserPlaying.Value);
AddAssert("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered);
}
[Test]
public void TestGameplayOverlayActivationBreaks()
{
+ AddAssert("local user playing", () => Player.LocalUserPlaying.Value);
AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled);
AddStep("seek to break", () => Player.GameplayClockContainer.Seek(Beatmap.Value.Beatmap.Breaks.First().StartTime));
AddUntilStep("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered);
+ AddAssert("local user not playing", () => !Player.LocalUserPlaying.Value);
AddStep("seek to break end", () => Player.GameplayClockContainer.Seek(Beatmap.Value.Beatmap.Breaks.First().EndTime));
AddUntilStep("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled);
+ AddAssert("local user playing", () => Player.LocalUserPlaying.Value);
}
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new OverlayTestPlayer();
@@ -57,6 +66,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected class OverlayTestPlayer : TestPlayer
{
public new OverlayActivation OverlayActivationMode => base.OverlayActivationMode.Value;
+ public new Bindable LocalUserPlaying => base.LocalUserPlaying;
}
}
}
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs
index 144f8da2fa..4bc843096f 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs
@@ -23,6 +23,12 @@ namespace osu.Game.Tests.Visual.Ranking
createTest(CreateDistributedHitEvents());
}
+ [Test]
+ public void TestAroundCentre()
+ {
+ createTest(Enumerable.Range(-150, 300).Select(i => new HitEvent(i / 50f, HitResult.Perfect, new HitCircle(), new HitCircle(), null)).ToList());
+ }
+
[Test]
public void TestZeroTimeOffset()
{
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index 207a3f01d3..78179a781a 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -7,6 +7,7 @@ using osu.Framework.Configuration.Tracking;
using osu.Framework.Extensions;
using osu.Framework.Platform;
using osu.Framework.Testing;
+using osu.Game.Input;
using osu.Game.Overlays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Select;
@@ -69,6 +70,7 @@ namespace osu.Game.Configuration
Set(OsuSetting.MouseDisableButtons, false);
Set(OsuSetting.MouseDisableWheel, false);
+ Set(OsuSetting.ConfineMouseMode, OsuConfineMouseMode.DuringGameplay);
// Graphics
Set(OsuSetting.ShowFpsDisplay, false);
@@ -194,6 +196,7 @@ namespace osu.Game.Configuration
FadePlayfieldWhenHealthLow,
MouseDisableButtons,
MouseDisableWheel,
+ ConfineMouseMode,
AudioOffset,
VolumeInactive,
MenuMusic,
diff --git a/osu.Game/Input/ConfineMouseTracker.cs b/osu.Game/Input/ConfineMouseTracker.cs
new file mode 100644
index 0000000000..3dadae6317
--- /dev/null
+++ b/osu.Game/Input/ConfineMouseTracker.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 osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Configuration;
+using osu.Framework.Graphics;
+using osu.Framework.Input;
+using osu.Game.Configuration;
+
+namespace osu.Game.Input
+{
+ ///
+ /// Connects with .
+ /// If is true, we should also confine the mouse cursor if it has been
+ /// requested with .
+ ///
+ public class ConfineMouseTracker : Component
+ {
+ private Bindable frameworkConfineMode;
+ private Bindable osuConfineMode;
+ private IBindable localUserPlaying;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuGame game, FrameworkConfigManager frameworkConfigManager, OsuConfigManager osuConfigManager)
+ {
+ frameworkConfineMode = frameworkConfigManager.GetBindable(FrameworkSetting.ConfineMouseMode);
+ osuConfineMode = osuConfigManager.GetBindable(OsuSetting.ConfineMouseMode);
+ localUserPlaying = game.LocalUserPlaying.GetBoundCopy();
+
+ osuConfineMode.ValueChanged += _ => updateConfineMode();
+ localUserPlaying.BindValueChanged(_ => updateConfineMode(), true);
+ }
+
+ private void updateConfineMode()
+ {
+ // confine mode is unavailable on some platforms
+ if (frameworkConfineMode.Disabled)
+ return;
+
+ switch (osuConfineMode.Value)
+ {
+ case OsuConfineMouseMode.Never:
+ frameworkConfineMode.Value = ConfineMouseMode.Never;
+ break;
+
+ case OsuConfineMouseMode.Fullscreen:
+ frameworkConfineMode.Value = ConfineMouseMode.Fullscreen;
+ break;
+
+ case OsuConfineMouseMode.DuringGameplay:
+ frameworkConfineMode.Value = localUserPlaying.Value ? ConfineMouseMode.Always : ConfineMouseMode.Never;
+ break;
+
+ case OsuConfineMouseMode.Always:
+ frameworkConfineMode.Value = ConfineMouseMode.Always;
+ break;
+ }
+ }
+ }
+}
diff --git a/osu.Game/Input/OsuConfineMouseMode.cs b/osu.Game/Input/OsuConfineMouseMode.cs
new file mode 100644
index 0000000000..32b456395c
--- /dev/null
+++ b/osu.Game/Input/OsuConfineMouseMode.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 System.ComponentModel;
+using osu.Framework.Input;
+
+namespace osu.Game.Input
+{
+ ///
+ /// Determines the situations in which the mouse cursor should be confined to the window.
+ /// Expands upon by providing the option to confine during gameplay.
+ ///
+ public enum OsuConfineMouseMode
+ {
+ ///
+ /// The mouse cursor will be free to move outside the game window.
+ ///
+ Never,
+
+ ///
+ /// The mouse cursor will be locked to the window bounds while in fullscreen mode.
+ ///
+ Fullscreen,
+
+ ///
+ /// The mouse cursor will be locked to the window bounds during gameplay,
+ /// but may otherwise move freely.
+ ///
+ [Description("During Gameplay")]
+ DuringGameplay,
+
+ ///
+ /// The mouse cursor will always be locked to the window bounds while the game has focus.
+ ///
+ Always
+ }
+}
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 4a699dc82e..d315b213ab 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -95,6 +95,15 @@ namespace osu.Game
///
public readonly IBindable OverlayActivationMode = new Bindable();
+ ///
+ /// Whether the local user is currently interacting with the game in a way that should not be interrupted.
+ ///
+ ///
+ /// This is exclusively managed by . If other components are mutating this state, a more
+ /// resilient method should be used to ensure correct state.
+ ///
+ public Bindable LocalUserPlaying = new BindableBool();
+
protected OsuScreenStack ScreenStack;
protected BackButton BackButton;
@@ -577,7 +586,8 @@ namespace osu.Game
rightFloatingOverlayContent = new Container { RelativeSizeAxes = Axes.Both },
leftFloatingOverlayContent = new Container { RelativeSizeAxes = Axes.Both },
topMostOverlayContent = new Container { RelativeSizeAxes = Axes.Both },
- idleTracker
+ idleTracker,
+ new ConfineMouseTracker()
});
ScreenStack.ScreenPushed += screenPushed;
@@ -947,6 +957,9 @@ namespace osu.Game
break;
}
+ // reset on screen change for sanity.
+ LocalUserPlaying.Value = false;
+
if (current is IOsuScreen currentOsuScreen)
OverlayActivationMode.UnbindFrom(currentOsuScreen.OverlayActivationMode);
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
index 968355c377..324299ccba 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs
@@ -60,7 +60,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
///
/// The statistics that appear in the table, in order of appearance.
///
- private readonly List statisticResultTypes = new List();
+ private readonly List<(HitResult result, string displayName)> statisticResultTypes = new List<(HitResult, string)>();
private bool showPerformancePoints;
@@ -101,15 +101,24 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
};
// All statistics across all scores, unordered.
- var allScoreStatistics = scores.SelectMany(s => s.GetStatisticsForDisplay().Select(stat => stat.result)).ToHashSet();
+ var allScoreStatistics = scores.SelectMany(s => s.GetStatisticsForDisplay().Select(stat => stat.Result)).ToHashSet();
+
+ var ruleset = scores.First().Ruleset.CreateInstance();
foreach (var result in OrderAttributeUtils.GetValuesInOrder())
{
if (!allScoreStatistics.Contains(result))
continue;
- columns.Add(new TableColumn(result.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60)));
- statisticResultTypes.Add(result);
+ // for the time being ignore bonus result types.
+ // this is not being sent from the API and will be empty in all cases.
+ if (result.IsBonus())
+ continue;
+
+ string displayName = ruleset.GetDisplayNameForHitResult(result);
+
+ columns.Add(new TableColumn(displayName, Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60)));
+ statisticResultTypes.Add((result, displayName));
}
if (showPerformancePoints)
@@ -163,18 +172,18 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
}
};
- var availableStatistics = score.GetStatisticsForDisplay().ToDictionary(tuple => tuple.result);
+ var availableStatistics = score.GetStatisticsForDisplay().ToDictionary(tuple => tuple.Result);
foreach (var result in statisticResultTypes)
{
- if (!availableStatistics.TryGetValue(result, out var stat))
- stat = (result, 0, null);
+ if (!availableStatistics.TryGetValue(result.result, out var stat))
+ stat = new HitResultDisplayStatistic(result.result, 0, null, result.displayName);
content.Add(new OsuSpriteText
{
- Text = stat.maxCount == null ? $"{stat.count}" : $"{stat.count}/{stat.maxCount}",
+ Text = stat.MaxCount == null ? $"{stat.Count}" : $"{stat.Count}/{stat.MaxCount}",
Font = OsuFont.GetFont(size: text_size),
- Colour = stat.count == 0 ? Color4.Gray : Color4.White
+ Colour = stat.Count == 0 ? Color4.Gray : Color4.White
});
}
diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs
index 05789e1fc0..93744dd6a3 100644
--- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs
+++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs
@@ -5,7 +5,6 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -15,7 +14,6 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osuTK;
@@ -117,7 +115,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
ppColumn.Alpha = value.Beatmap?.Status == BeatmapSetOnlineStatus.Ranked ? 1 : 0;
ppColumn.Text = $@"{value.PP:N0}";
- statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(s => createStatisticsColumn(s.result, s.count, s.maxCount));
+ statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(createStatisticsColumn);
modsColumn.Mods = value.Mods;
if (scoreManager != null)
@@ -125,9 +123,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
}
}
- private TextColumn createStatisticsColumn(HitResult hitResult, int count, int? maxCount) => new TextColumn(hitResult.GetDescription(), smallFont, bottom_columns_min_width)
+ private TextColumn createStatisticsColumn(HitResultDisplayStatistic stat) => new TextColumn(stat.DisplayName, smallFont, bottom_columns_min_width)
{
- Text = maxCount == null ? $"{count}" : $"{count}/{maxCount}"
+ Text = stat.MaxCount == null ? $"{stat.Count}" : $"{stat.Count}/{stat.MaxCount}"
};
private class InfoColumn : CompositeDrawable
diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs
index 0764f34697..12caf98021 100644
--- a/osu.Game/Overlays/MusicController.cs
+++ b/osu.Game/Overlays/MusicController.cs
@@ -150,7 +150,7 @@ namespace osu.Game.Overlays
{
if (IsUserPaused) return;
- if (CurrentTrack.IsDummyDevice)
+ if (CurrentTrack.IsDummyDevice || beatmap.Value.BeatmapSetInfo.DeletePending)
{
if (beatmap.Disabled)
return;
diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs
index a59a6b00b9..c213313559 100644
--- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs
@@ -4,9 +4,11 @@
using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Graphics.Sprites;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Game.Configuration;
+using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.Settings.Sections.Maintenance;
using osu.Game.Updater;
@@ -21,6 +23,9 @@ namespace osu.Game.Overlays.Settings.Sections.General
private SettingsButton checkForUpdatesButton;
+ [Resolved(CanBeNull = true)]
+ private NotificationOverlay notifications { get; set; }
+
[BackgroundDependencyLoader(true)]
private void load(Storage storage, OsuConfigManager config, OsuGame game)
{
@@ -38,7 +43,19 @@ namespace osu.Game.Overlays.Settings.Sections.General
Action = () =>
{
checkForUpdatesButton.Enabled.Value = false;
- Task.Run(updateManager.CheckForUpdateAsync).ContinueWith(t => Schedule(() => checkForUpdatesButton.Enabled.Value = true));
+ Task.Run(updateManager.CheckForUpdateAsync).ContinueWith(t => Schedule(() =>
+ {
+ if (!t.Result)
+ {
+ notifications?.Post(new SimpleNotification
+ {
+ Text = $"You are running the latest release ({game.Version})",
+ Icon = FontAwesome.Solid.CheckCircle,
+ });
+ }
+
+ checkForUpdatesButton.Enabled.Value = true;
+ }));
}
});
}
diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
index 5227e328ec..f0d51a0d37 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs
@@ -6,9 +6,9 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
-using osu.Framework.Input;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Input;
namespace osu.Game.Overlays.Settings.Sections.Input
{
@@ -47,10 +47,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input
LabelText = "Map absolute input to window",
Current = config.GetBindable(FrameworkSetting.MapAbsoluteInputToWindow)
},
- new SettingsEnumDropdown
+ new SettingsEnumDropdown
{
LabelText = "Confine mouse cursor to window",
- Current = config.GetBindable(FrameworkSetting.ConfineMouseMode),
+ Current = osuConfig.GetBindable(OsuSetting.ConfineMouseMode)
},
new SettingsCheckbox
{
diff --git a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
index 71256093d5..4abdbfc244 100644
--- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
+++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
@@ -89,9 +89,23 @@ namespace osu.Game.Rulesets.Edit
}
}
- protected virtual void OnDeselected() => Hide();
+ protected virtual void OnDeselected()
+ {
+ // selection blueprints are AlwaysPresent while the related DrawableHitObject is visible
+ // set the body piece's alpha directly to avoid arbitrarily rendering frame buffers etc. of children.
+ foreach (var d in InternalChildren)
+ d.Hide();
- protected virtual void OnSelected() => Show();
+ Hide();
+ }
+
+ protected virtual void OnSelected()
+ {
+ foreach (var d in InternalChildren)
+ d.Show();
+
+ Show();
+ }
// When not selected, input is only required for the blueprint itself to receive IsHovering
protected override bool ShouldBeConsideredForInput(Drawable child) => State == SelectionState.Selected;
diff --git a/osu.Game/Rulesets/Mods/ModHidden.cs b/osu.Game/Rulesets/Mods/ModHidden.cs
index a1915b974c..ad01bf036c 100644
--- a/osu.Game/Rulesets/Mods/ModHidden.cs
+++ b/osu.Game/Rulesets/Mods/ModHidden.cs
@@ -38,7 +38,15 @@ namespace osu.Game.Rulesets.Mods
public virtual void ApplyToDrawableHitObjects(IEnumerable drawables)
{
if (IncreaseFirstObjectVisibility.Value)
- drawables = drawables.SkipWhile(h => !IsFirstHideableObject(h)).Skip(1);
+ {
+ drawables = drawables.SkipWhile(h => !IsFirstHideableObject(h));
+
+ var firstObject = drawables.FirstOrDefault();
+ if (firstObject != null)
+ firstObject.ApplyCustomUpdateState += ApplyFirstObjectIncreaseVisibilityState;
+
+ drawables = drawables.Skip(1);
+ }
foreach (var dho in drawables)
dho.ApplyCustomUpdateState += ApplyHiddenState;
@@ -65,6 +73,20 @@ namespace osu.Game.Rulesets.Mods
}
}
+ ///
+ /// Apply a special visibility state to the first object in a beatmap, if the user chooses to turn on the "increase first object visibility" setting.
+ ///
+ /// The hit object to apply the state change to.
+ /// The state of the hit object.
+ protected virtual void ApplyFirstObjectIncreaseVisibilityState(DrawableHitObject hitObject, ArmedState state)
+ {
+ }
+
+ ///
+ /// Apply a hidden state to the provided object.
+ ///
+ /// The hit object to apply the state change to.
+ /// The state of the hit object.
protected virtual void ApplyHiddenState(DrawableHitObject hitObject, ArmedState state)
{
}
diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs
index d577e8fdda..3083fcfccb 100644
--- a/osu.Game/Rulesets/Objects/SliderPath.cs
+++ b/osu.Game/Rulesets/Objects/SliderPath.cs
@@ -57,6 +57,7 @@ namespace osu.Game.Rulesets.Objects
c.Changed += invalidate;
break;
+ case NotifyCollectionChangedAction.Reset:
case NotifyCollectionChangedAction.Remove:
foreach (var c in args.OldItems.Cast())
c.Changed -= invalidate;
diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs
index fef36ef16a..8caadffd1d 100644
--- a/osu.Game/Rulesets/Ruleset.cs
+++ b/osu.Game/Rulesets/Ruleset.cs
@@ -23,8 +23,10 @@ using osu.Game.Scoring;
using osu.Game.Skinning;
using osu.Game.Users;
using JetBrains.Annotations;
+using osu.Framework.Extensions;
using osu.Framework.Testing;
using osu.Game.Screens.Ranking.Statistics;
+using osu.Game.Utils;
namespace osu.Game.Rulesets
{
@@ -241,5 +243,52 @@ namespace osu.Game.Rulesets
/// The s to display. Each may contain 0 or more .
[NotNull]
public virtual StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => Array.Empty();
+
+ ///
+ /// Get all valid s for this ruleset.
+ /// Generally used for results display purposes, where it can't be determined if zero-count means the user has not achieved any or the type is not used by this ruleset.
+ ///
+ ///
+ /// All valid s along with a display-friendly name.
+ ///
+ public IEnumerable<(HitResult result, string displayName)> GetHitResults()
+ {
+ var validResults = GetValidHitResults();
+
+ // enumerate over ordered list to guarantee return order is stable.
+ foreach (var result in OrderAttributeUtils.GetValuesInOrder())
+ {
+ switch (result)
+ {
+ // hard blocked types, should never be displayed even if the ruleset tells us to.
+ case HitResult.None:
+ case HitResult.IgnoreHit:
+ case HitResult.IgnoreMiss:
+ // display is handled as a completion count with corresponding "hit" type.
+ case HitResult.LargeTickMiss:
+ case HitResult.SmallTickMiss:
+ continue;
+ }
+
+ if (result == HitResult.Miss || validResults.Contains(result))
+ yield return (result, GetDisplayNameForHitResult(result));
+ }
+ }
+
+ ///
+ /// Get all valid s for this ruleset.
+ /// Generally used for results display purposes, where it can't be determined if zero-count means the user has not achieved any or the type is not used by this ruleset.
+ ///
+ ///
+ /// is implicitly included. Special types like are ignored even when specified.
+ ///
+ protected virtual IEnumerable GetValidHitResults() => OrderAttributeUtils.GetValuesInOrder();
+
+ ///
+ /// Get a display friendly name for the specified result type.
+ ///
+ /// The result type to get the name for.
+ /// The display name.
+ public virtual string GetDisplayNameForHitResult(HitResult result) => result.GetDescription();
}
}
diff --git a/osu.Game/Scoring/HitResultDisplayStatistic.cs b/osu.Game/Scoring/HitResultDisplayStatistic.cs
new file mode 100644
index 0000000000..d43d8bf0ba
--- /dev/null
+++ b/osu.Game/Scoring/HitResultDisplayStatistic.cs
@@ -0,0 +1,41 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Scoring
+{
+ ///
+ /// Compiled result data for a specific in a score.
+ ///
+ public class HitResultDisplayStatistic
+ {
+ ///
+ /// The associated result type.
+ ///
+ public HitResult Result { get; }
+
+ ///
+ /// The count of successful hits of this type.
+ ///
+ public int Count { get; }
+
+ ///
+ /// The maximum achievable hits of this type. May be null if undetermined.
+ ///
+ public int? MaxCount { get; }
+
+ ///
+ /// A custom display name for the result type. May be provided by rulesets to give better clarity.
+ ///
+ public string DisplayName { get; }
+
+ public HitResultDisplayStatistic(HitResult result, int count, int? maxCount, string displayName)
+ {
+ Result = result;
+ Count = count;
+ MaxCount = maxCount;
+ DisplayName = displayName;
+ }
+ }
+}
diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs
index 0206989231..596e98a6bd 100644
--- a/osu.Game/Scoring/ScoreInfo.cs
+++ b/osu.Game/Scoring/ScoreInfo.cs
@@ -213,22 +213,19 @@ namespace osu.Game.Scoring
set => isLegacyScore = value;
}
- public IEnumerable<(HitResult result, int count, int? maxCount)> GetStatisticsForDisplay()
+ public IEnumerable GetStatisticsForDisplay()
{
- foreach (var key in OrderAttributeUtils.GetValuesInOrder())
+ foreach (var r in Ruleset.CreateInstance().GetHitResults())
{
- if (key.IsBonus())
- continue;
+ int value = Statistics.GetOrDefault(r.result);
- int value = Statistics.GetOrDefault(key);
-
- switch (key)
+ switch (r.result)
{
case HitResult.SmallTickHit:
{
int total = value + Statistics.GetOrDefault(HitResult.SmallTickMiss);
if (total > 0)
- yield return (key, value, total);
+ yield return new HitResultDisplayStatistic(r.result, value, total, r.displayName);
break;
}
@@ -237,7 +234,7 @@ namespace osu.Game.Scoring
{
int total = value + Statistics.GetOrDefault(HitResult.LargeTickMiss);
if (total > 0)
- yield return (key, value, total);
+ yield return new HitResultDisplayStatistic(r.result, value, total, r.displayName);
break;
}
@@ -247,8 +244,7 @@ namespace osu.Game.Scoring
break;
default:
- if (value > 0 || key == HitResult.Miss)
- yield return (key, value, null);
+ yield return new HitResultDisplayStatistic(r.result, value, null, r.displayName);
break;
}
diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs
index 1ac960039e..b0ecffdd24 100644
--- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs
+++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.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 osuTK;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
+using osuTK;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
{
@@ -12,16 +12,23 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
///
public class PointVisualisation : Box
{
+ public const float WIDTH = 1;
+
public PointVisualisation(double startTime)
+ : this()
+ {
+ X = (float)startTime;
+ }
+
+ public PointVisualisation()
{
Origin = Anchor.TopCentre;
- RelativeSizeAxes = Axes.Y;
- Width = 1;
- EdgeSmoothness = new Vector2(1, 0);
-
RelativePositionAxes = Axes.X;
- X = (float)startTime;
+ RelativeSizeAxes = Axes.Y;
+
+ Width = WIDTH;
+ EdgeSmoothness = new Vector2(WIDTH, 0);
}
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
index 9b3314e2ad..0336c74386 100644
--- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
@@ -201,7 +201,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override void AddBlueprintFor(HitObject hitObject)
{
refreshTool();
+
base.AddBlueprintFor(hitObject);
+
+ // on successful placement, the new combo button should be reset as this is the most common user interaction.
+ if (Beatmap.SelectedHitObjects.Count == 0)
+ NewCombo.Value = TernaryState.False;
}
private void createPlacement()
diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs
index 64191e48e2..b753c45cca 100644
--- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs
@@ -17,10 +17,28 @@ namespace osu.Game.Screens.Edit.Compose.Components
public Action OnRotation;
public Action OnScale;
public Action OnFlip;
+ public Action OnReverse;
public Action OperationStarted;
public Action OperationEnded;
+ private bool canReverse;
+
+ ///
+ /// Whether pattern reversing support should be enabled.
+ ///
+ public bool CanReverse
+ {
+ get => canReverse;
+ set
+ {
+ if (canReverse == value) return;
+
+ canReverse = value;
+ recreate();
+ }
+ }
+
private bool canRotate;
///
@@ -125,6 +143,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (CanScaleX && CanScaleY) addFullScaleComponents();
if (CanScaleY) addYScaleComponents();
if (CanRotate) addRotationComponents();
+ if (CanReverse) addButton(FontAwesome.Solid.Backward, "Reverse pattern", () => OnReverse?.Invoke());
}
private void addRotationComponents()
diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
index e8ab09df85..a5fb73832f 100644
--- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
@@ -103,6 +103,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
OnRotation = angle => HandleRotation(angle),
OnScale = (amount, anchor) => HandleScale(amount, anchor),
OnFlip = direction => HandleFlip(direction),
+ OnReverse = () => HandleReverse(),
};
///
@@ -141,7 +142,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// Handles the selected s being rotated.
///
/// The delta angle to apply to the selection.
- /// Whether any s could be moved.
+ /// Whether any s could be rotated.
public virtual bool HandleRotation(float angle) => false;
///
@@ -149,16 +150,22 @@ namespace osu.Game.Screens.Edit.Compose.Components
///
/// The delta scale to apply, in playfield local coordinates.
/// The point of reference where the scale is originating from.
- /// Whether any s could be moved.
+ /// Whether any s could be scaled.
public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false;
///
- /// Handled the selected s being flipped.
+ /// Handles the selected s being flipped.
///
/// The direction to flip
- /// Whether any s could be moved.
+ /// Whether any s could be flipped.
public virtual bool HandleFlip(Direction direction) => false;
+ ///
+ /// Handles the selected s being reversed pattern-wise.
+ ///
+ /// Whether any s could be reversed.
+ public virtual bool HandleReverse() => false;
+
public bool OnPressed(PlatformAction action)
{
switch (action.ActionMethod)
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs
index 36ee976bf7..724256af8b 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs
@@ -1,9 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
@@ -12,7 +14,7 @@ using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
- public class TimelineTickDisplay : TimelinePart
+ public class TimelineTickDisplay : TimelinePart
{
[Resolved]
private EditorBeatmap beatmap { get; set; }
@@ -31,15 +33,63 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
RelativeSizeAxes = Axes.Both;
}
+ private readonly Cached tickCache = new Cached();
+
[BackgroundDependencyLoader]
private void load()
{
- beatDivisor.BindValueChanged(_ => createLines(), true);
+ beatDivisor.BindValueChanged(_ => tickCache.Invalidate());
}
- private void createLines()
+ ///
+ /// The visible time/position range of the timeline.
+ ///
+ private (float min, float max) visibleRange = (float.MinValue, float.MaxValue);
+
+ ///
+ /// The next time/position value to the left of the display when tick regeneration needs to be run.
+ ///
+ private float? nextMinTick;
+
+ ///
+ /// The next time/position value to the right of the display when tick regeneration needs to be run.
+ ///
+ private float? nextMaxTick;
+
+ [Resolved(canBeNull: true)]
+ private Timeline timeline { get; set; }
+
+ protected override void Update()
{
- Clear();
+ base.Update();
+
+ if (timeline != null)
+ {
+ var newRange = (
+ (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - PointVisualisation.WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X,
+ (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + PointVisualisation.WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X);
+
+ if (visibleRange != newRange)
+ {
+ visibleRange = newRange;
+
+ // actual regeneration only needs to occur if we've passed one of the known next min/max tick boundaries.
+ if (nextMinTick == null || nextMaxTick == null || (visibleRange.min < nextMinTick || visibleRange.max > nextMaxTick))
+ tickCache.Invalidate();
+ }
+ }
+
+ if (!tickCache.IsValid)
+ createTicks();
+ }
+
+ private void createTicks()
+ {
+ int drawableIndex = 0;
+ int highestDivisor = BindableBeatDivisor.VALID_DIVISORS.Last();
+
+ nextMinTick = null;
+ nextMaxTick = null;
for (var i = 0; i < beatmap.ControlPointInfo.TimingPoints.Count; i++)
{
@@ -50,41 +100,70 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
for (double t = point.Time; t < until; t += point.BeatLength / beatDivisor.Value)
{
- var indexInBeat = beat % beatDivisor.Value;
+ float xPos = (float)t;
- if (indexInBeat == 0)
- {
- Add(new PointVisualisation(t)
- {
- Colour = BindableBeatDivisor.GetColourFor(1, colours),
- Origin = Anchor.TopCentre,
- });
- }
+ if (t < visibleRange.min)
+ nextMinTick = xPos;
+ else if (t > visibleRange.max)
+ nextMaxTick ??= xPos;
else
{
+ // if this is the first beat in the beatmap, there is no next min tick
+ if (beat == 0 && i == 0)
+ nextMinTick = float.MinValue;
+
+ var indexInBar = beat % ((int)point.TimeSignature * beatDivisor.Value);
+
var divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value);
var colour = BindableBeatDivisor.GetColourFor(divisor, colours);
- var height = 0.1f - (float)divisor / BindableBeatDivisor.VALID_DIVISORS.Last() * 0.08f;
- Add(new PointVisualisation(t)
- {
- Colour = colour,
- Height = height,
- Origin = Anchor.TopCentre,
- });
+ // even though "bar lines" take up the full vertical space, we render them in two pieces because it allows for less anchor/origin churn.
+ var height = indexInBar == 0 ? 0.5f : 0.1f - (float)divisor / highestDivisor * 0.08f;
- Add(new PointVisualisation(t)
- {
- Colour = colour,
- Anchor = Anchor.BottomLeft,
- Origin = Anchor.BottomCentre,
- Height = height,
- });
+ var topPoint = getNextUsablePoint();
+ topPoint.X = xPos;
+ topPoint.Colour = colour;
+ topPoint.Height = height;
+ topPoint.Anchor = Anchor.TopLeft;
+ topPoint.Origin = Anchor.TopCentre;
+
+ var bottomPoint = getNextUsablePoint();
+ bottomPoint.X = xPos;
+ bottomPoint.Colour = colour;
+ bottomPoint.Anchor = Anchor.BottomLeft;
+ bottomPoint.Origin = Anchor.BottomCentre;
+ bottomPoint.Height = height;
}
beat++;
}
}
+
+ int usedDrawables = drawableIndex;
+
+ // save a few drawables beyond the currently used for edge cases.
+ while (drawableIndex < Math.Min(usedDrawables + 16, Count))
+ Children[drawableIndex++].Hide();
+
+ // expire any excess
+ while (drawableIndex < Count)
+ Children[drawableIndex++].Expire();
+
+ tickCache.Validate();
+
+ Drawable getNextUsablePoint()
+ {
+ PointVisualisation point;
+ if (drawableIndex >= Count)
+ Add(point = new PointVisualisation());
+ else
+ point = Children[drawableIndex];
+
+ drawableIndex++;
+ point.Show();
+
+ return point;
+ }
}
}
}
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 74f324364a..7444369e84 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -469,10 +469,17 @@ namespace osu.Game.Screens.Edit
private void confirmExit()
{
+ // stop the track if playing to allow the parent screen to choose a suitable playback mode.
+ Beatmap.Value.Track.Stop();
+
if (isNewBeatmap)
{
// confirming exit without save means we should delete the new beatmap completely.
beatmapManager.Delete(playableBeatmap.BeatmapInfo.BeatmapSet);
+
+ // in theory this shouldn't be required but due to EF core not sharing instance states 100%
+ // MusicController is unaware of the changed DeletePending state.
+ Beatmap.SetDefault();
}
exitConfirmed = true;
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 90a0eb0027..74714e7e59 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -68,6 +68,8 @@ namespace osu.Game.Screens.Play
private readonly Bindable storyboardReplacesBackground = new Bindable();
+ protected readonly Bindable LocalUserPlaying = new Bindable();
+
public int RestartCount;
[Resolved]
@@ -155,8 +157,8 @@ namespace osu.Game.Screens.Play
DrawableRuleset.SetRecordTarget(recordingReplay = new Replay());
}
- [BackgroundDependencyLoader]
- private void load(AudioManager audio, OsuConfigManager config)
+ [BackgroundDependencyLoader(true)]
+ private void load(AudioManager audio, OsuConfigManager config, OsuGame game)
{
Mods.Value = base.Mods.Value.Select(m => m.CreateCopy()).ToArray();
@@ -172,6 +174,9 @@ namespace osu.Game.Screens.Play
mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel);
+ if (game != null)
+ LocalUserPlaying.BindTo(game.LocalUserPlaying);
+
DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value);
ScoreProcessor = ruleset.CreateScoreProcessor();
@@ -219,9 +224,9 @@ namespace osu.Game.Screens.Play
skipOverlay.Hide();
}
- DrawableRuleset.IsPaused.BindValueChanged(_ => updateOverlayActivationMode());
- DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateOverlayActivationMode());
- breakTracker.IsBreakTime.BindValueChanged(_ => updateOverlayActivationMode());
+ DrawableRuleset.IsPaused.BindValueChanged(_ => updateGameplayState());
+ DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateGameplayState());
+ breakTracker.IsBreakTime.BindValueChanged(_ => updateGameplayState());
DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true);
@@ -353,14 +358,11 @@ namespace osu.Game.Screens.Play
HUDOverlay.KeyCounter.IsCounting = !isBreakTime.NewValue;
}
- private void updateOverlayActivationMode()
+ private void updateGameplayState()
{
- bool canTriggerOverlays = DrawableRuleset.IsPaused.Value || breakTracker.IsBreakTime.Value;
-
- if (DrawableRuleset.HasReplayLoaded.Value || canTriggerOverlays)
- OverlayActivationMode.Value = OverlayActivation.UserTriggered;
- else
- OverlayActivationMode.Value = OverlayActivation.Disabled;
+ bool inGameplay = !DrawableRuleset.HasReplayLoaded.Value && !DrawableRuleset.IsPaused.Value && !breakTracker.IsBreakTime.Value;
+ OverlayActivationMode.Value = inGameplay ? OverlayActivation.Disabled : OverlayActivation.UserTriggered;
+ LocalUserPlaying.Value = inGameplay;
}
private void updatePauseOnFocusLostState() =>
@@ -661,7 +663,7 @@ namespace osu.Game.Screens.Play
foreach (var mod in Mods.Value.OfType())
mod.ApplyToTrack(musicController.CurrentTrack);
- updateOverlayActivationMode();
+ updateGameplayState();
}
public override void OnSuspending(IScreen next)
diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs
index 95ece1a9fb..24f1116d0e 100644
--- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs
+++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs
@@ -3,7 +3,6 @@
using System.Linq;
using osu.Framework.Allocation;
-using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
@@ -117,7 +116,7 @@ namespace osu.Game.Screens.Ranking.Contracted
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
- ChildrenEnumerable = score.GetStatisticsForDisplay().Select(s => createStatistic(s.result, s.count, s.maxCount))
+ ChildrenEnumerable = score.GetStatisticsForDisplay().Where(s => !s.Result.IsBonus()).Select(createStatistic)
},
new FillFlowContainer
{
@@ -199,8 +198,8 @@ namespace osu.Game.Screens.Ranking.Contracted
};
}
- private Drawable createStatistic(HitResult result, int count, int? maxCount)
- => createStatistic(result.GetDescription(), maxCount == null ? $"{count}" : $"{count}/{maxCount}");
+ private Drawable createStatistic(HitResultDisplayStatistic result)
+ => createStatistic(result.DisplayName, result.MaxCount == null ? $"{result.Count}" : $"{result.Count}/{result.MaxCount}");
private Drawable createStatistic(string key, string value) => new Container
{
diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
index ebab8c88f6..5aac449adb 100644
--- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
+++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs
@@ -64,155 +64,168 @@ namespace osu.Game.Screens.Ranking.Expanded
new CounterStatistic("pp", (int)(score.PP ?? 0)),
};
- var bottomStatistics = new List();
+ var bottomStatistics = new List();
- foreach (var (key, value, maxCount) in score.GetStatisticsForDisplay())
- bottomStatistics.Add(new HitResultStatistic(key, value, maxCount));
+ foreach (var result in score.GetStatisticsForDisplay())
+ bottomStatistics.Add(new HitResultStatistic(result));
statisticDisplays.AddRange(topStatistics);
statisticDisplays.AddRange(bottomStatistics);
- InternalChild = new FillFlowContainer
+ InternalChildren = new Drawable[]
{
- RelativeSizeAxes = Axes.Both,
- Direction = FillDirection.Vertical,
- Spacing = new Vector2(20),
- Children = new Drawable[]
+ new FillFlowContainer
{
- new FillFlowContainer
+ RelativeSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(20),
+ Children = new Drawable[]
{
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Children = new Drawable[]
+ new FillFlowContainer
{
- new OsuSpriteText
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
{
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)),
- Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold),
- MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
- Truncate = true,
- },
- new OsuSpriteText
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)),
- Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
- MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
- Truncate = true,
- },
- new Container
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Margin = new MarginPadding { Top = 40 },
- RelativeSizeAxes = Axes.X,
- Height = 230,
- Child = new AccuracyCircle(score)
+ new OsuSpriteText
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- FillMode = FillMode.Fit,
- }
- },
- scoreCounter = new TotalScoreCounter
- {
- Margin = new MarginPadding { Top = 0, Bottom = 5 },
- Current = { Value = 0 },
- Alpha = 0,
- AlwaysPresent = true
- },
- starAndModDisplay = new FillFlowContainer
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- AutoSizeAxes = Axes.Both,
- Spacing = new Vector2(5, 0),
- Children = new Drawable[]
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)),
+ Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold),
+ MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
+ Truncate = true,
+ },
+ new OsuSpriteText
{
- new StarRatingDisplay(beatmap)
- {
- Anchor = Anchor.CentreLeft,
- Origin = Anchor.CentreLeft
- },
- }
- },
- new FillFlowContainer
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Direction = FillDirection.Vertical,
- AutoSizeAxes = Axes.Both,
- Children = new Drawable[]
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)),
+ Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
+ MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
+ Truncate = true,
+ },
+ new Container
{
- new OsuSpriteText
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Margin = new MarginPadding { Top = 40 },
+ RelativeSizeAxes = Axes.X,
+ Height = 230,
+ Child = new AccuracyCircle(score)
{
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Text = beatmap.Version,
- Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold),
- },
- new OsuTextFlowContainer(s => s.Font = OsuFont.Torus.With(size: 12))
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fit,
+ }
+ },
+ scoreCounter = new TotalScoreCounter
+ {
+ Margin = new MarginPadding { Top = 0, Bottom = 5 },
+ Current = { Value = 0 },
+ Alpha = 0,
+ AlwaysPresent = true
+ },
+ starAndModDisplay = new FillFlowContainer
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ AutoSizeAxes = Axes.Both,
+ Spacing = new Vector2(5, 0),
+ Children = new Drawable[]
{
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Horizontal,
- }.With(t =>
- {
- if (!string.IsNullOrEmpty(creator))
+ new StarRatingDisplay(beatmap)
{
- t.AddText("mapped by ");
- t.AddText(creator, s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
- }
- })
- }
- },
- }
- },
- new FillFlowContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Spacing = new Vector2(0, 5),
- Children = new Drawable[]
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft
+ },
+ }
+ },
+ new FillFlowContainer
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Direction = FillDirection.Vertical,
+ AutoSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Text = beatmap.Version,
+ Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold),
+ },
+ new OsuTextFlowContainer(s => s.Font = OsuFont.Torus.With(size: 12))
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ }.With(t =>
+ {
+ if (!string.IsNullOrEmpty(creator))
+ {
+ t.AddText("mapped by ");
+ t.AddText(creator, s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
+ }
+ })
+ }
+ },
+ }
+ },
+ new FillFlowContainer
{
- new GridContainer
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 5),
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Content = new[] { topStatistics.Cast().ToArray() },
- RowDimensions = new[]
+ new GridContainer
{
- new Dimension(GridSizeMode.AutoSize),
- }
- },
- new GridContainer
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Content = new[] { bottomStatistics.Cast().ToArray() },
- RowDimensions = new[]
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Content = new[] { topStatistics.Cast().ToArray() },
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize),
+ }
+ },
+ new GridContainer
{
- new Dimension(GridSizeMode.AutoSize),
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Content = new[] { bottomStatistics.Where(s => s.Result <= HitResult.Perfect).ToArray() },
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize),
+ }
+ },
+ new GridContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Content = new[] { bottomStatistics.Where(s => s.Result > HitResult.Perfect).ToArray() },
+ RowDimensions = new[]
+ {
+ new Dimension(GridSizeMode.AutoSize),
+ }
}
}
}
- },
- new OsuSpriteText
- {
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
- Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold),
- Text = $"Played on {score.Date.ToLocalTime():d MMMM yyyy HH:mm}"
}
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold),
+ Text = $"Played on {score.Date.ToLocalTime():d MMMM yyyy HH:mm}"
}
};
diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs
index a86033713f..ada8dfabf0 100644
--- a/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs
+++ b/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs
@@ -2,26 +2,26 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
-using osu.Framework.Extensions;
using osu.Game.Graphics;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
namespace osu.Game.Screens.Ranking.Expanded.Statistics
{
public class HitResultStatistic : CounterStatistic
{
- private readonly HitResult result;
+ public readonly HitResult Result;
- public HitResultStatistic(HitResult result, int count, int? maxCount = null)
- : base(result.GetDescription(), count, maxCount)
+ public HitResultStatistic(HitResultDisplayStatistic result)
+ : base(result.DisplayName, result.Count, result.MaxCount)
{
- this.result = result;
+ Result = result.Result;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
- HeaderText.Colour = colours.ForHitResult(result);
+ HeaderText.Colour = colours.ForHitResult(Result);
}
}
}
diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs
index 1904da7094..ee97ee55eb 100644
--- a/osu.Game/Screens/Ranking/ScorePanel.cs
+++ b/osu.Game/Screens/Ranking/ScorePanel.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Screens.Ranking
///
/// Height of the panel when contracted.
///
- private const float contracted_height = 355;
+ private const float contracted_height = 385;
///
/// Width of the panel when expanded.
@@ -39,7 +39,7 @@ namespace osu.Game.Screens.Ranking
///
/// Height of the panel when expanded.
///
- private const float expanded_height = 560;
+ private const float expanded_height = 586;
///
/// Height of the top layer when the panel is expanded.
@@ -105,11 +105,16 @@ namespace osu.Game.Screens.Ranking
[BackgroundDependencyLoader]
private void load()
{
+ // ScorePanel doesn't include the top extruding area in its own size.
+ // Adding a manual offset here allows the expanded version to take on an "acceptable" vertical centre when at 100% UI scale.
+ const float vertical_fudge = 20;
+
InternalChild = content = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(40),
+ Y = vertical_fudge,
Children = new Drawable[]
{
topLayerContainer = new Container
diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs
index aa2a83774e..93885b6e02 100644
--- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs
+++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs
@@ -66,7 +66,7 @@ namespace osu.Game.Screens.Ranking.Statistics
foreach (var e in hitEvents)
{
- int binOffset = (int)(e.TimeOffset / binSize);
+ int binOffset = (int)Math.Round(e.TimeOffset / binSize, MidpointRounding.AwayFromZero);
bins[timing_distribution_centre_bin_index + binOffset]++;
}
diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs
index 48c6722bd9..b5fcb56c06 100644
--- a/osu.Game/Updater/SimpleUpdateManager.cs
+++ b/osu.Game/Updater/SimpleUpdateManager.cs
@@ -30,7 +30,7 @@ namespace osu.Game.Updater
version = game.Version;
}
- protected override async Task PerformUpdateCheck()
+ protected override async Task PerformUpdateCheck()
{
try
{
@@ -53,12 +53,17 @@ namespace osu.Game.Updater
return true;
}
});
+
+ return true;
}
}
catch
{
// we shouldn't crash on a web failure. or any failure for the matter.
+ return true;
}
+
+ return false;
}
private string getBestUrl(GitHubRelease release)
diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs
index 61775a26b7..f772c6d282 100644
--- a/osu.Game/Updater/UpdateManager.cs
+++ b/osu.Game/Updater/UpdateManager.cs
@@ -57,25 +57,31 @@ namespace osu.Game.Updater
private readonly object updateTaskLock = new object();
- private Task updateCheckTask;
+ private Task updateCheckTask;
- public async Task CheckForUpdateAsync()
+ public async Task CheckForUpdateAsync()
{
if (!CanCheckForUpdate)
- return;
+ return false;
- Task waitTask;
+ Task waitTask;
lock (updateTaskLock)
waitTask = (updateCheckTask ??= PerformUpdateCheck());
- await waitTask;
+ bool hasUpdates = await waitTask;
lock (updateTaskLock)
updateCheckTask = null;
+
+ return hasUpdates;
}
- protected virtual Task PerformUpdateCheck() => Task.CompletedTask;
+ ///
+ /// Performs an asynchronous check for application updates.
+ ///
+ /// Whether any update is waiting. May return true if an error occured (there is potentially an update available).
+ protected virtual Task PerformUpdateCheck() => Task.FromResult(false);
private class UpdateCompleteNotification : SimpleNotification
{
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index fa2135580d..8b10f0a7f7 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -24,7 +24,7 @@
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 20a51e5feb..88abbca73d 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -80,7 +80,7 @@
-
+