diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 47a6a4c3d3..ec57232126 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -2,7 +2,7 @@ blank_issues_enabled: false
contact_links:
- name: Help
url: https://github.com/ppy/osu/discussions/categories/q-a
- about: osu! not working as you'd expect? Not sure it's a bug? Check the Q&A section!
+ about: osu! not working or performing as you'd expect? Not sure it's a bug? Check the Q&A section!
- name: Suggestions or feature request
url: https://github.com/ppy/osu/discussions/categories/ideas
about: Got something you think should change or be added? Search for or start a new discussion!
diff --git a/osu.Android.props b/osu.Android.props
index 6aebae665d..f4d08e443c 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -11,7 +11,7 @@
manifestmerger.jar
-
+
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchBeatSnapGrid.cs b/osu.Game.Rulesets.Catch/Edit/CatchBeatSnapGrid.cs
new file mode 100644
index 0000000000..6862696b3a
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/CatchBeatSnapGrid.cs
@@ -0,0 +1,180 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Caching;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Screens.Edit;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ ///
+ /// A grid which displays coloured beat divisor lines in proximity to the selection or placement cursor.
+ ///
+ ///
+ /// This class heavily borrows from osu!mania's implementation (ManiaBeatSnapGrid).
+ /// If further changes are to be made, they should also be applied there.
+ /// If the scale of the changes are large enough, abstracting may be a good path.
+ ///
+ public partial class CatchBeatSnapGrid : Component
+ {
+ private const double visible_range = 750;
+
+ ///
+ /// The range of time values of the current selection.
+ ///
+ public (double start, double end)? SelectionTimeRange
+ {
+ set
+ {
+ if (value == selectionTimeRange)
+ return;
+
+ selectionTimeRange = value;
+ lineCache.Invalidate();
+ }
+ }
+
+ [Resolved]
+ private EditorBeatmap beatmap { get; set; } = null!;
+
+ [Resolved]
+ private OsuColour colours { get; set; } = null!;
+
+ [Resolved]
+ private BindableBeatDivisor beatDivisor { get; set; } = null!;
+
+ private readonly Cached lineCache = new Cached();
+
+ private (double start, double end)? selectionTimeRange;
+
+ private ScrollingHitObjectContainer lineContainer = null!;
+
+ [BackgroundDependencyLoader]
+ private void load(HitObjectComposer composer)
+ {
+ lineContainer = new ScrollingHitObjectContainer();
+
+ ((CatchPlayfield)composer.Playfield).UnderlayElements.Add(lineContainer);
+
+ beatDivisor.BindValueChanged(_ => createLines(), true);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!lineCache.IsValid)
+ {
+ lineCache.Validate();
+ createLines();
+ }
+ }
+
+ private readonly Stack availableLines = new Stack();
+
+ private void createLines()
+ {
+ foreach (var line in lineContainer.Objects.OfType())
+ availableLines.Push(line);
+
+ lineContainer.Clear();
+
+ if (selectionTimeRange == null)
+ return;
+
+ var range = selectionTimeRange.Value;
+
+ var timingPoint = beatmap.ControlPointInfo.TimingPointAt(range.start - visible_range);
+
+ double time = timingPoint.Time;
+ int beat = 0;
+
+ // progress time until in the visible range.
+ while (time < range.start - visible_range)
+ {
+ time += timingPoint.BeatLength / beatDivisor.Value;
+ beat++;
+ }
+
+ while (time < range.end + visible_range)
+ {
+ var nextTimingPoint = beatmap.ControlPointInfo.TimingPointAt(time);
+
+ // switch to the next timing point if we have reached it.
+ if (nextTimingPoint.Time > timingPoint.Time)
+ {
+ beat = 0;
+ time = nextTimingPoint.Time;
+ timingPoint = nextTimingPoint;
+ }
+
+ Color4 colour = BindableBeatDivisor.GetColourFor(
+ BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value), colours);
+
+ if (!availableLines.TryPop(out var line))
+ line = new DrawableGridLine();
+
+ line.HitObject.StartTime = time;
+ line.Colour = colour;
+
+ lineContainer.Add(line);
+
+ beat++;
+ time += timingPoint.BeatLength / beatDivisor.Value;
+ }
+
+ // required to update ScrollingHitObjectContainer's cache.
+ lineContainer.UpdateSubTree();
+
+ foreach (var line in lineContainer.Objects.OfType())
+ {
+ time = line.HitObject.StartTime;
+
+ if (time >= range.start && time <= range.end)
+ line.Alpha = 1;
+ else
+ {
+ double timeSeparation = time < range.start ? range.start - time : time - range.end;
+ line.Alpha = (float)Math.Max(0, 1 - timeSeparation / visible_range);
+ }
+ }
+ }
+
+ private partial class DrawableGridLine : DrawableHitObject
+ {
+ public DrawableGridLine()
+ : base(new HitObject())
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = 2;
+
+ AddInternal(new Box { RelativeSizeAxes = Axes.Both });
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Origin = Anchor.BottomLeft;
+ Anchor = Anchor.BottomLeft;
+ }
+
+ protected override void UpdateInitialTransforms()
+ {
+ // don't perform any fading – we are handling that ourselves.
+ LifetimeEnd = HitObject.StartTime + visible_range;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
index 8afeca3e51..f2877572e8 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
@@ -33,6 +33,8 @@ namespace osu.Game.Rulesets.Catch.Edit
private InputManager inputManager = null!;
+ private CatchBeatSnapGrid beatSnapGrid = null!;
+
private readonly BindableDouble timeRangeMultiplier = new BindableDouble(1)
{
MinValue = 1,
@@ -65,6 +67,8 @@ namespace osu.Game.Rulesets.Catch.Edit
Catcher.BASE_DASH_SPEED, -Catcher.BASE_DASH_SPEED,
Catcher.BASE_WALK_SPEED, -Catcher.BASE_WALK_SPEED,
}));
+
+ AddInternal(beatSnapGrid = new CatchBeatSnapGrid());
}
protected override void LoadComplete()
@@ -74,6 +78,29 @@ namespace osu.Game.Rulesets.Catch.Edit
inputManager = GetContainingInputManager();
}
+ protected override void UpdateAfterChildren()
+ {
+ base.UpdateAfterChildren();
+
+ if (BlueprintContainer.CurrentTool is SelectTool)
+ {
+ if (EditorBeatmap.SelectedHitObjects.Any())
+ {
+ beatSnapGrid.SelectionTimeRange = (EditorBeatmap.SelectedHitObjects.Min(h => h.StartTime), EditorBeatmap.SelectedHitObjects.Max(h => h.GetEndTime()));
+ }
+ else
+ beatSnapGrid.SelectionTimeRange = null;
+ }
+ else
+ {
+ var result = FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position);
+ if (result.Time is double time)
+ beatSnapGrid.SelectionTimeRange = (time, time);
+ else
+ beatSnapGrid.SelectionTimeRange = null;
+ }
+ }
+
protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
{
// osu!catch's distance snap implementation is limited, in that a custom spacing cannot be specified.
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
index cf7337fd0d..f091dee845 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
@@ -41,6 +42,8 @@ namespace osu.Game.Rulesets.Catch.UI
internal CatcherArea CatcherArea { get; private set; } = null!;
+ public Container UnderlayElements { get; private set; } = null!;
+
private readonly IBeatmapDifficultyInfo difficulty;
public CatchPlayfield(IBeatmapDifficultyInfo difficulty)
@@ -62,6 +65,10 @@ namespace osu.Game.Rulesets.Catch.UI
AddRangeInternal(new[]
{
+ UnderlayElements = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
droppedObjectContainer,
Catcher.CreateProxiedContent(),
HitObjectContainer.CreateProxy(),
diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
index 99a80ef28d..f975c7f1d4 100644
--- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
+++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
@@ -21,18 +21,29 @@ namespace osu.Game.Rulesets.Mania.Configuration
{
base.InitialiseDefaults();
- SetDefault(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 5);
+ SetDefault(ManiaRulesetSetting.ScrollSpeed, 8, 1, 40);
SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false);
+
+#pragma warning disable CS0618
+ // Although obsolete, this is still required to populate the bindable from the database in case migration is required.
+ SetDefault(ManiaRulesetSetting.ScrollTime, null);
+
+ if (Get(ManiaRulesetSetting.ScrollTime) is double scrollTime)
+ {
+ SetValue(ManiaRulesetSetting.ScrollSpeed, (int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime));
+ SetValue(ManiaRulesetSetting.ScrollTime, null);
+ }
+#pragma warning restore CS0618
}
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
{
- new TrackedSetting(ManiaRulesetSetting.ScrollTime,
- scrollTime => new SettingDescription(
- rawValue: scrollTime,
+ new TrackedSetting(ManiaRulesetSetting.ScrollSpeed,
+ speed => new SettingDescription(
+ rawValue: speed,
name: RulesetSettingsStrings.ScrollSpeed,
- value: RulesetSettingsStrings.ScrollSpeedTooltip(scrollTime, (int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime))
+ value: RulesetSettingsStrings.ScrollSpeedTooltip((int)DrawableManiaRuleset.ComputeScrollTime(speed), speed)
)
)
};
@@ -40,7 +51,9 @@ namespace osu.Game.Rulesets.Mania.Configuration
public enum ManiaRulesetSetting
{
+ [Obsolete("Use ScrollSpeed instead.")] // Can be removed 2023-11-30
ScrollTime,
+ ScrollSpeed,
ScrollDirection,
TimingBasedNoteColouring
}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs
index d1d5492b7a..2d4b5f718c 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs
@@ -129,7 +129,7 @@ namespace osu.Game.Rulesets.Mania.Edit
}
Color4 colour = BindableBeatDivisor.GetColourFor(
- BindableBeatDivisor.GetDivisorForBeatIndex(Math.Max(1, beat), beatDivisor.Value), colours);
+ BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value), colours);
foreach (var grid in grids)
{
diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
index d324682989..e8fda3ec80 100644
--- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs
@@ -389,41 +389,23 @@ namespace osu.Game.Rulesets.Mania
return base.GetDisplayNameForHitResult(result);
}
- public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
+ public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
{
- new StatisticRow
+ new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
{
- Columns = new[]
- {
- new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y
- }),
- }
- },
- new StatisticRow
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y
+ }),
+ new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(score.HitEvents)
{
- Columns = new[]
- {
- new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(score.HitEvents)
- {
- RelativeSizeAxes = Axes.X,
- Height = 250
- }, true),
- }
- },
- new StatisticRow
+ RelativeSizeAxes = Axes.X,
+ Height = 250
+ }, true),
+ new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
- Columns = new[]
- {
- new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
- {
- new AverageHitError(score.HitEvents),
- new UnstableRate(score.HitEvents)
- }), true)
- }
- }
+ new AverageHitError(score.HitEvents),
+ new UnstableRate(score.HitEvents)
+ }), true)
};
public override IRulesetFilterCriteria CreateRulesetFilterCriteria()
diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
index fc0b4a9ed9..065534eec4 100644
--- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
+++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
@@ -34,10 +33,10 @@ namespace osu.Game.Rulesets.Mania
LabelText = RulesetSettingsStrings.ScrollingDirection,
Current = config.GetBindable(ManiaRulesetSetting.ScrollDirection)
},
- new SettingsSlider
+ new SettingsSlider
{
LabelText = RulesetSettingsStrings.ScrollSpeed,
- Current = config.GetBindable(ManiaRulesetSetting.ScrollTime),
+ Current = config.GetBindable(ManiaRulesetSetting.ScrollSpeed),
KeyboardStep = 5
},
new SettingsCheckbox
@@ -48,9 +47,9 @@ namespace osu.Game.Rulesets.Mania
};
}
- private partial class ManiaScrollSlider : RoundedSliderBar
+ private partial class ManiaScrollSlider : RoundedSliderBar
{
- public override LocalisableString TooltipText => RulesetSettingsStrings.ScrollSpeedTooltip(Current.Value, (int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / Current.Value));
+ public override LocalisableString TooltipText => RulesetSettingsStrings.ScrollSpeedTooltip((int)DrawableManiaRuleset.ComputeScrollTime(Current.Value), Current.Value);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/BarLine.cs b/osu.Game.Rulesets.Mania/Objects/BarLine.cs
index 3f04a4fafe..09a746042b 100644
--- a/osu.Game.Rulesets.Mania/Objects/BarLine.cs
+++ b/osu.Game.Rulesets.Mania/Objects/BarLine.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index ce34addeff..3f91328128 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -245,7 +245,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
// As the note is being held, adjust the size of the sizing container. This has two effects:
// 1. The contained masking container will mask the body and ticks.
// 2. The head note will move along with the new "head position" in the container.
- if (Head.IsHit && releaseTime == null && DrawHeight > 0)
+ //
+ // As per stable, this should not apply for early hits, waiting until the object starts to touch the
+ // judgement area first.
+ if (Head.IsHit && releaseTime == null && DrawHeight > 0 && Time.Current >= HitObject.StartTime)
{
// How far past the hit target this hold note is.
float yOffset = Direction.Value == ScrollingDirection.Up ? -Y : Y;
diff --git a/osu.Game.Rulesets.Mania/Objects/HeadNote.cs b/osu.Game.Rulesets.Mania/Objects/HeadNote.cs
index fb5c7b4ddd..e69cc62aed 100644
--- a/osu.Game.Rulesets.Mania/Objects/HeadNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/HeadNote.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Rulesets.Mania.Objects
{
public class HeadNote : Note
diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
index 3341f834dd..6292ed75cd 100644
--- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
+++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs
@@ -18,8 +18,8 @@ namespace osu.Game.Rulesets.Mania.Scoring
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
{
- return 200000 * comboProgress
- + 800000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress
+ return 10000 * comboProgress
+ + 990000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress
+ bonusPortion;
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs
index 007d02400a..ddd6365c25 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ManiaArgonSkinTransformer.cs
@@ -139,11 +139,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case 3:
switch (columnIndex)
{
- case 0: return colour_pink;
+ case 0: return colour_green;
- case 1: return colour_orange;
+ case 1: return colour_special_column;
- case 2: return colour_yellow;
+ case 2: return colour_cyan;
default: throw new ArgumentOutOfRangeException();
}
@@ -185,11 +185,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case 1: return colour_orange;
- case 2: return colour_yellow;
+ case 2: return colour_green;
case 3: return colour_cyan;
- case 4: return colour_purple;
+ case 4: return colour_orange;
case 5: return colour_pink;
@@ -201,17 +201,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
case 0: return colour_pink;
- case 1: return colour_cyan;
+ case 1: return colour_orange;
case 2: return colour_pink;
case 3: return colour_special_column;
- case 4: return colour_green;
+ case 4: return colour_pink;
- case 5: return colour_cyan;
+ case 5: return colour_orange;
- case 6: return colour_green;
+ case 6: return colour_pink;
default: throw new ArgumentOutOfRangeException();
}
@@ -225,9 +225,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case 2: return colour_orange;
- case 3: return colour_yellow;
+ case 3: return colour_green;
- case 4: return colour_yellow;
+ case 4: return colour_cyan;
case 5: return colour_orange;
@@ -273,9 +273,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case 3: return colour_yellow;
- case 4: return colour_cyan;
+ case 4: return colour_green;
- case 5: return colour_green;
+ case 5: return colour_cyan;
case 6: return colour_yellow;
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index 6ca830a82f..f38571a6d3 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -11,6 +11,7 @@ using osu.Framework.Graphics.Pooling;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Platform;
+using osu.Game.Extensions;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
@@ -39,7 +40,11 @@ namespace osu.Game.Rulesets.Mania.UI
public readonly Bindable Action = new Bindable();
public readonly ColumnHitObjectArea HitObjectArea;
+
+ internal readonly Container BackgroundContainer = new Container { RelativeSizeAxes = Axes.Both };
+
internal readonly Container TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both };
+
private DrawablePool hitExplosionPool;
private readonly OrderedHitPolicy hitPolicy;
public Container UnderlayElements => HitObjectArea.UnderlayElements;
@@ -76,30 +81,31 @@ namespace osu.Game.Rulesets.Mania.UI
skin.SourceChanged += onSourceChanged;
onSourceChanged();
- Drawable background = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
- {
- RelativeSizeAxes = Axes.Both,
- };
-
- InternalChildren = new[]
+ InternalChildren = new Drawable[]
{
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,
keyArea = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
{
RelativeSizeAxes = Axes.Both,
},
- background,
+ // For input purposes, the background is added at the highest depth, but is then proxied back below all other elements externally
+ // (see `Stage.columnBackgrounds`).
+ BackgroundContainer,
TopLevelContainer,
new ColumnTouchInputArea(this)
};
- applyGameWideClock(background);
- applyGameWideClock(keyArea);
+ var background = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
+ {
+ RelativeSizeAxes = Axes.Both,
+ };
+ background.ApplyGameWideClock(host);
+ keyArea.ApplyGameWideClock(host);
+
+ BackgroundContainer.Add(background);
TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
RegisterPool(10, 50);
@@ -107,18 +113,6 @@ namespace osu.Game.Rulesets.Mania.UI
RegisterPool(10, 50);
RegisterPool(10, 50);
RegisterPool(50, 250);
-
- // Some elements don't handle rewind correctly and fixing them is non-trivial.
- // In the future we need a better solution to this, but as a temporary work-around, give these components the game-wide
- // clock so they don't need to worry about rewind.
- // This only works because they handle OnPressed/OnReleased which results in a correct state while rewinding.
- //
- // This is kinda dodgy (and will cause weirdness when pausing gameplay) but is better than completely broken rewind.
- void applyGameWideClock(Drawable drawable)
- {
- drawable.Clock = host.UpdateThread.Clock;
- drawable.ProcessCustomClock = false;
- }
}
private void onSourceChanged()
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
index af8758fb5e..2d373c0471 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
@@ -33,12 +33,12 @@ namespace osu.Game.Rulesets.Mania.UI
public partial class DrawableManiaRuleset : DrawableScrollingRuleset
{
///
- /// The minimum time range. This occurs at a of 40.
+ /// The minimum time range. This occurs at a of 40.
///
public const double MIN_TIME_RANGE = 290;
///
- /// The maximum time range. This occurs at a of 1.
+ /// The maximum time range. This occurs with a of 1.
///
public const double MAX_TIME_RANGE = 11485;
@@ -69,7 +69,8 @@ namespace osu.Game.Rulesets.Mania.UI
protected override ScrollVisualisationMethod VisualisationMethod => scrollMethod;
private readonly Bindable configDirection = new Bindable();
- private readonly BindableDouble configTimeRange = new BindableDouble();
+ private readonly BindableInt configScrollSpeed = new BindableInt();
+ private double smoothTimeRange;
// Stores the current speed adjustment active in gameplay.
private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
@@ -78,6 +79,9 @@ namespace osu.Game.Rulesets.Mania.UI
: base(ruleset, beatmap, mods)
{
BarLines = new BarLineGenerator(Beatmap).BarLines;
+
+ TimeRange.MinValue = 1;
+ TimeRange.MaxValue = MAX_TIME_RANGE;
}
[BackgroundDependencyLoader]
@@ -104,30 +108,28 @@ namespace osu.Game.Rulesets.Mania.UI
Config.BindWith(ManiaRulesetSetting.ScrollDirection, configDirection);
configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true);
- Config.BindWith(ManiaRulesetSetting.ScrollTime, configTimeRange);
- TimeRange.MinValue = configTimeRange.MinValue;
- TimeRange.MaxValue = configTimeRange.MaxValue;
+ Config.BindWith(ManiaRulesetSetting.ScrollSpeed, configScrollSpeed);
+ configScrollSpeed.BindValueChanged(speed => this.TransformTo(nameof(smoothTimeRange), ComputeScrollTime(speed.NewValue), 200, Easing.OutQuint));
+
+ TimeRange.Value = smoothTimeRange = ComputeScrollTime(configScrollSpeed.Value);
}
- protected override void AdjustScrollSpeed(int amount)
- {
- this.TransformTo(nameof(relativeTimeRange), relativeTimeRange + amount, 200, Easing.OutQuint);
- }
-
- private double relativeTimeRange
- {
- get => MAX_TIME_RANGE / configTimeRange.Value;
- set => configTimeRange.Value = MAX_TIME_RANGE / value;
- }
+ protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount;
protected override void Update()
{
base.Update();
-
updateTimeRange();
}
- private void updateTimeRange() => TimeRange.Value = configTimeRange.Value * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value;
+ private void updateTimeRange() => TimeRange.Value = smoothTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value;
+
+ ///
+ /// Computes a scroll time (in milliseconds) from a scroll speed in the range of 1-40.
+ ///
+ /// The scroll speed.
+ /// The scroll time.
+ public static double ComputeScrollTime(int scrollSpeed) => MAX_TIME_RANGE / scrollSpeed;
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer();
diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs
index c1d3e85bf1..879c704450 100644
--- a/osu.Game.Rulesets.Mania/UI/Stage.cs
+++ b/osu.Game.Rulesets.Mania/UI/Stage.cs
@@ -60,6 +60,7 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Y;
AutoSizeAxes = Axes.X;
+ Container columnBackgrounds;
Container topLevelContainer;
InternalChildren = new Drawable[]
@@ -77,9 +78,10 @@ namespace osu.Game.Rulesets.Mania.UI
{
RelativeSizeAxes = Axes.Both
},
- columnFlow = new ColumnFlow(definition)
+ columnBackgrounds = new Container
{
- RelativeSizeAxes = Axes.Y,
+ Name = "Column backgrounds",
+ RelativeSizeAxes = Axes.Both,
},
new Container
{
@@ -98,6 +100,10 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Y,
}
},
+ columnFlow = new ColumnFlow(definition)
+ {
+ RelativeSizeAxes = Axes.Y,
+ },
new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.StageForeground), _ => null)
{
RelativeSizeAxes = Axes.Both
@@ -126,6 +132,7 @@ namespace osu.Game.Rulesets.Mania.UI
};
topLevelContainer.Add(column.TopLevelContainer.CreateProxy());
+ columnBackgrounds.Add(column.BackgroundContainer.CreateProxy());
columnFlow.SetContentForColumn(i, column);
AddNested(column);
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs
index 3b8a5a90a5..9af1855167 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs
@@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("place first object", () => InputManager.Click(MouseButton.Left));
- AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.02f, 0)));
+ AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.01f, 0)));
AddStep("place second object", () => InputManager.Click(MouseButton.Left));
@@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("enter circle placement mode", () => InputManager.Key(Key.Number2));
- AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.235f, 0)));
+ AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.205f, 0)));
AddStep("place second object", () => InputManager.Click(MouseButton.Left));
@@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left));
- AddStep("move mouse slightly off centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.02f, 0)));
+ AddStep("move mouse slightly off centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.01f, 0)));
AddAssert("object 3 snapped to 1", () =>
{
@@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
return Precision.AlmostEquals(first.EndPosition, third.Position);
});
- AddStep("move mouse slightly off centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * -0.22f, playfield.ScreenSpaceDrawQuad.Width * 0.21f)));
+ AddStep("move mouse slightly off centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * -0.21f, playfield.ScreenSpaceDrawQuad.Width * 0.205f)));
AddAssert("object 2 snapped to 1", () =>
{
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
index 8fdab9f1f9..32028823ae 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
@@ -1,22 +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 System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Osu.UI;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModAutoplay : OsuModTestScene
{
+ protected override bool AllowFail => true;
+
+ [Test]
+ public void TestCursorPositionStoredToJudgement()
+ {
+ CreateModTest(new ModTestData
+ {
+ Autoplay = true,
+ PassCondition = () =>
+ Player.ScoreProcessor.JudgedHits >= 1
+ && Player.ScoreProcessor.HitEvents.Any(e => e.Position != null)
+ });
+ }
+
[Test]
public void TestSpmUnaffectedByRateAdjust()
=> runSpmTest(new OsuModDaycore
@@ -32,6 +51,36 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
FinalRate = { Value = 1.3 }
});
+ [Test]
+ public void TestPerfectScoreOnShortSliderWithRepeat()
+ {
+ AddStep("set score to standardised", () => LocalConfig.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised));
+
+ CreateModTest(new ModTestData
+ {
+ Autoplay = true,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Slider
+ {
+ StartTime = 500,
+ Position = new Vector2(256, 192),
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(),
+ new PathControlPoint(new Vector2(0, 6.25f))
+ }),
+ RepeatCount = 1,
+ SliderVelocity = 10
+ }
+ }
+ },
+ PassCondition = () => Player.ScoreProcessor.TotalScore.Value == 1_000_000
+ });
+ }
+
private void runSpmTest(Mod mod)
{
SpinnerSpmCalculator? spmCalculator = null;
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPosition.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPosition.cs
index 92071d4a57..616bb17e05 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPosition.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPosition.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
public enum SliderPosition
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index ad6af6d74e..aac5f6ffb1 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -187,7 +187,7 @@ namespace osu.Game.Rulesets.Osu.Edit
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
float snapRadius =
- playfield.GamefieldToScreenSpace(new Vector2(OsuHitObject.OBJECT_RADIUS / 5)).X -
+ playfield.GamefieldToScreenSpace(new Vector2(OsuHitObject.OBJECT_RADIUS * 0.10f)).X -
playfield.GamefieldToScreenSpace(Vector2.Zero).X;
foreach (var b in blueprints)
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs
index 12e2090f89..b74b722bad 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModBubbles.cs
@@ -42,14 +42,14 @@ namespace osu.Game.Rulesets.Osu.Mods
private PlayfieldAdjustmentContainer bubbleContainer = null!;
+ private DrawablePool bubblePool = null!;
+
private readonly Bindable currentCombo = new BindableInt();
private float maxSize;
private float bubbleSize;
private double bubbleFade;
- private readonly DrawablePool bubblePool = new DrawablePool(100);
-
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
@@ -72,6 +72,7 @@ namespace osu.Game.Rulesets.Osu.Mods
bubbleContainer = drawableRuleset.CreatePlayfieldAdjustmentContainer();
drawableRuleset.Overlays.Add(bubbleContainer);
+ drawableRuleset.Overlays.Add(bubblePool = new DrawablePool(100));
}
public void ApplyToDrawableHitObject(DrawableHitObject drawableObject)
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
index 32ffb545e0..aaa7c70a8d 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
@@ -18,7 +18,7 @@ using static osu.Game.Input.Handlers.ReplayInputHandler;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset, IApplicableToPlayer
+ public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset, IApplicableToPlayer, IHasNoTimedInputs
{
public override LocalisableString Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 664a8146e7..01174d4d61 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -75,18 +75,24 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
[BackgroundDependencyLoader]
private void load()
{
+ tailContainer = new Container { RelativeSizeAxes = Axes.Both };
+
AddRangeInternal(new Drawable[]
{
shakeContainer = new ShakeContainer
{
ShakeDuration = 30,
RelativeSizeAxes = Axes.Both,
- Children = new Drawable[]
+ Children = new[]
{
Body = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling),
- tailContainer = new Container { RelativeSizeAxes = Axes.Both },
+ // proxied here so that the tail is drawn under repeats/ticks - legacy skins rely on this
+ tailContainer.CreateProxy(),
tickContainer = new Container { RelativeSizeAxes = Axes.Both },
repeatContainer = new Container { RelativeSizeAxes = Axes.Both },
+ // actual tail container is placed here to ensure that tail hitobjects are processed after ticks/repeats.
+ // this is required for the correct operation of Score V2.
+ tailContainer,
}
},
// slider head is not included in shake as it handles hit detection, and handles its own shaking.
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/IRequireTracking.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/IRequireTracking.cs
index 55de5a0e8d..b1815b23c9 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/IRequireTracking.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/IRequireTracking.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public interface IRequireTracking
diff --git a/osu.Game.Rulesets.Osu/Objects/ISliderProgress.cs b/osu.Game.Rulesets.Osu/Objects/ISliderProgress.cs
index eddd251bda..7594f7c2e0 100644
--- a/osu.Game.Rulesets.Osu/Objects/ISliderProgress.cs
+++ b/osu.Game.Rulesets.Osu/Objects/ISliderProgress.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Rulesets.Osu.Objects
{
public interface ISliderProgress
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index 922594a93a..8ce55d78dd 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -291,56 +291,32 @@ namespace osu.Game.Rulesets.Osu
return base.GetDisplayNameForHitResult(result);
}
- public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
+ public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{
var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList();
return new[]
{
- new StatisticRow
+ new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
{
- Columns = new[]
- {
- new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y
- }),
- }
- },
- new StatisticRow
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y
+ }),
+ new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents)
{
- Columns = new[]
- {
- new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents)
- {
- RelativeSizeAxes = Axes.X,
- Height = 250
- }, true),
- }
- },
- new StatisticRow
+ RelativeSizeAxes = Axes.X,
+ Height = 250
+ }, true),
+ new StatisticItem("Accuracy Heatmap", () => new AccuracyHeatmap(score, playableBeatmap)
{
- Columns = new[]
- {
- new StatisticItem("Accuracy Heatmap", () => new AccuracyHeatmap(score, playableBeatmap)
- {
- RelativeSizeAxes = Axes.X,
- Height = 250
- }, true),
- }
- },
- new StatisticRow
+ RelativeSizeAxes = Axes.X,
+ Height = 250
+ }, true),
+ new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
- Columns = new[]
- {
- new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
- {
- new AverageHitError(timedHitEvents),
- new UnstableRate(timedHitEvents)
- }), true)
- }
- }
+ new AverageHitError(timedHitEvents),
+ new UnstableRate(timedHitEvents)
+ }), true)
};
}
diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
index ab07ac3e9d..f97be0d7ff 100644
--- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
+++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
@@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring
@@ -13,6 +15,9 @@ namespace osu.Game.Rulesets.Osu.Scoring
{
}
+ protected override HitEvent CreateHitEvent(JudgementResult result)
+ => base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit);
+
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
{
return 700000 * comboProgress
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
index 1c5cf49625..3427031dc8 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
@@ -48,21 +48,26 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
private Bindable configHitLighting = null!;
+ private static readonly Vector2 circle_size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
+
[Resolved]
private DrawableHitObject drawableObject { get; set; } = null!;
public ArgonMainCirclePiece(bool withOuterFill)
{
- Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
+ Size = circle_size;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
InternalChildren = new Drawable[]
{
- outerFill = new Circle // renders white outer border and dark fill
+ outerFill = new Circle // renders dark fill
{
- Size = Size,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ // Slightly inset to prevent bleeding outside the ring
+ Size = circle_size - new Vector2(1),
Alpha = withOuterFill ? 1 : 0,
},
outerGradient = new Circle // renders the outer bright gradient
@@ -88,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
Masking = true,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Size = Size,
+ Size = circle_size,
Child = new KiaiFlash
{
RelativeSizeAxes = Axes.Both,
diff --git a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneDrumRollJudgements.cs b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneDrumRollJudgements.cs
index a9231b4783..21f2b8f1be 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneDrumRollJudgements.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Judgements/TestSceneDrumRollJudgements.cs
@@ -15,187 +15,175 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
[Test]
public void TestHitAllDrumRoll()
{
- const double hit_time = 1000;
-
PerformTest(new List
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
new TaikoReplayFrame(1001),
+ new TaikoReplayFrame(1250, TaikoAction.LeftCentre),
+ new TaikoReplayFrame(1251),
+ new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
+ new TaikoReplayFrame(1501),
+ new TaikoReplayFrame(1750, TaikoAction.LeftCentre),
+ new TaikoReplayFrame(1751),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2001),
- }, CreateBeatmap(new DrumRoll
- {
- StartTime = hit_time,
- Duration = 1000
- }));
+ }, CreateBeatmap(createDrumRoll(false)));
- AssertJudgementCount(3);
+ AssertJudgementCount(6);
AssertResult(0, HitResult.SmallBonus);
AssertResult(1, HitResult.SmallBonus);
+ AssertResult(2, HitResult.SmallBonus);
+ AssertResult(3, HitResult.SmallBonus);
+ AssertResult(4, HitResult.SmallBonus);
AssertResult(0, HitResult.IgnoreHit);
}
[Test]
public void TestHitSomeDrumRoll()
{
- const double hit_time = 1000;
-
PerformTest(new List
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2001),
- }, CreateBeatmap(new DrumRoll
- {
- StartTime = hit_time,
- Duration = 1000
- }));
+ }, CreateBeatmap(createDrumRoll(false)));
- AssertJudgementCount(3);
+ AssertJudgementCount(6);
AssertResult(0, HitResult.IgnoreMiss);
- AssertResult(1, HitResult.SmallBonus);
+ AssertResult(1, HitResult.IgnoreMiss);
+ AssertResult(2, HitResult.IgnoreMiss);
+ AssertResult(3, HitResult.IgnoreMiss);
+ AssertResult(4, HitResult.SmallBonus);
AssertResult(0, HitResult.IgnoreHit);
}
[Test]
public void TestHitNoneDrumRoll()
{
- const double hit_time = 1000;
-
PerformTest(new List
{
new TaikoReplayFrame(0),
- }, CreateBeatmap(new DrumRoll
- {
- StartTime = hit_time,
- Duration = 1000
- }));
+ }, CreateBeatmap(createDrumRoll(false)));
- AssertJudgementCount(3);
+ AssertJudgementCount(6);
AssertResult(0, HitResult.IgnoreMiss);
AssertResult(1, HitResult.IgnoreMiss);
+ AssertResult(2, HitResult.IgnoreMiss);
+ AssertResult(3, HitResult.IgnoreMiss);
+ AssertResult(4, HitResult.IgnoreMiss);
AssertResult(0, HitResult.IgnoreHit);
}
[Test]
public void TestHitAllStrongDrumRollWithOneKey()
{
- const double hit_time = 1000;
-
PerformTest(new List
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
new TaikoReplayFrame(1001),
+ new TaikoReplayFrame(1250, TaikoAction.LeftCentre),
+ new TaikoReplayFrame(1251),
+ new TaikoReplayFrame(1500, TaikoAction.LeftCentre),
+ new TaikoReplayFrame(1501),
+ new TaikoReplayFrame(1750, TaikoAction.LeftCentre),
+ new TaikoReplayFrame(1751),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2001),
- }, CreateBeatmap(new DrumRoll
+ }, CreateBeatmap(createDrumRoll(true)));
+
+ AssertJudgementCount(12);
+
+ for (int i = 0; i < 5; i++)
{
- StartTime = hit_time,
- Duration = 1000,
- IsStrong = true
- }));
-
- AssertJudgementCount(6);
-
- AssertResult(0, HitResult.SmallBonus);
- AssertResult(0, HitResult.LargeBonus);
-
- AssertResult(1, HitResult.SmallBonus);
- AssertResult(1, HitResult.LargeBonus);
+ AssertResult(i, HitResult.SmallBonus);
+ AssertResult(i, HitResult.LargeBonus);
+ }
AssertResult(0, HitResult.IgnoreHit);
- AssertResult(2, HitResult.IgnoreHit);
+ AssertResult(5, HitResult.IgnoreHit);
}
[Test]
public void TestHitSomeStrongDrumRollWithOneKey()
{
- const double hit_time = 1000;
-
PerformTest(new List
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2001),
- }, CreateBeatmap(new DrumRoll
- {
- StartTime = hit_time,
- Duration = 1000,
- IsStrong = true
- }));
+ }, CreateBeatmap(createDrumRoll(true)));
- AssertJudgementCount(6);
+ AssertJudgementCount(12);
AssertResult(0, HitResult.IgnoreMiss);
AssertResult(0, HitResult.IgnoreMiss);
- AssertResult(1, HitResult.SmallBonus);
- AssertResult(1, HitResult.LargeBonus);
+ AssertResult(4, HitResult.SmallBonus);
+ AssertResult(4, HitResult.LargeBonus);
AssertResult(0, HitResult.IgnoreHit);
- AssertResult(2, HitResult.IgnoreHit);
+ AssertResult(5, HitResult.IgnoreHit);
}
[Test]
public void TestHitAllStrongDrumRollWithBothKeys()
{
- const double hit_time = 1000;
-
PerformTest(new List
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(1000, TaikoAction.LeftCentre, TaikoAction.RightCentre),
new TaikoReplayFrame(1001),
+ new TaikoReplayFrame(1250, TaikoAction.LeftCentre, TaikoAction.RightCentre),
+ new TaikoReplayFrame(1251),
+ new TaikoReplayFrame(1500, TaikoAction.LeftCentre, TaikoAction.RightCentre),
+ new TaikoReplayFrame(1501),
+ new TaikoReplayFrame(1750, TaikoAction.LeftCentre, TaikoAction.RightCentre),
+ new TaikoReplayFrame(1751),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre, TaikoAction.RightCentre),
new TaikoReplayFrame(2001),
- }, CreateBeatmap(new DrumRoll
+ }, CreateBeatmap(createDrumRoll(true)));
+
+ AssertJudgementCount(12);
+
+ for (int i = 0; i < 5; i++)
{
- StartTime = hit_time,
- Duration = 1000,
- IsStrong = true
- }));
-
- AssertJudgementCount(6);
-
- AssertResult(0, HitResult.SmallBonus);
- AssertResult(0, HitResult.LargeBonus);
-
- AssertResult(1, HitResult.SmallBonus);
- AssertResult(1, HitResult.LargeBonus);
+ AssertResult(i, HitResult.SmallBonus);
+ AssertResult(i, HitResult.LargeBonus);
+ }
AssertResult(0, HitResult.IgnoreHit);
- AssertResult(2, HitResult.IgnoreHit);
+ AssertResult(5, HitResult.IgnoreHit);
}
[Test]
public void TestHitSomeStrongDrumRollWithBothKeys()
{
- const double hit_time = 1000;
-
PerformTest(new List
{
new TaikoReplayFrame(0),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre, TaikoAction.RightCentre),
new TaikoReplayFrame(2001),
- }, CreateBeatmap(new DrumRoll
- {
- StartTime = hit_time,
- Duration = 1000,
- IsStrong = true
- }));
+ }, CreateBeatmap(createDrumRoll(true)));
- AssertJudgementCount(6);
+ AssertJudgementCount(12);
AssertResult(0, HitResult.IgnoreMiss);
AssertResult(0, HitResult.IgnoreMiss);
- AssertResult(1, HitResult.SmallBonus);
- AssertResult(1, HitResult.LargeBonus);
+ AssertResult(4, HitResult.SmallBonus);
+ AssertResult(4, HitResult.LargeBonus);
AssertResult(0, HitResult.IgnoreHit);
- AssertResult(2, HitResult.IgnoreHit);
+ AssertResult(5, HitResult.IgnoreHit);
}
+
+ private DrumRoll createDrumRoll(bool strong) => new DrumRoll
+ {
+ StartTime = 1000,
+ Duration = 1000,
+ IsStrong = strong
+ };
}
}
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index e298e313df..1c2e7abafe 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -92,6 +92,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
}).ToList();
}
+ // TODO: stable makes the last tick of a drumroll non-required when the next object is too close.
+ // This probably needs to be reimplemented:
+ //
+ // List hitobjects = hitObjectManager.hitObjects;
+ // int ind = hitobjects.IndexOf(this);
+ // if (i < hitobjects.Count - 1 && hitobjects[i + 1].HittableStartTime - (EndTime + (int)TickSpacing) <= (int)TickSpacing)
+ // lastTickHittable = false;
+
return converted;
}
@@ -133,7 +141,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
StartTime = obj.StartTime,
Samples = obj.Samples,
Duration = taikoDuration,
- TickRate = beatmap.Difficulty.SliderTickRate == 3 ? 3 : 4,
SliderVelocity = obj is IHasSliderVelocity velocityData ? velocityData.SliderVelocity : 1
};
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
index 76d1a58506..083b8cc547 100644
--- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Objects.Types;
using System.Threading;
using osu.Framework.Bindables;
@@ -69,6 +67,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
double scoringDistance = base_distance * difficulty.SliderMultiplier * SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength;
+ TickRate = difficulty.SliderTickRate == 3 ? 3 : 4;
+
tickSpacing = timingPoint.BeatLength / TickRate;
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/HitType.cs b/osu.Game.Rulesets.Taiko/Objects/HitType.cs
index eae7fa683a..17b3fdbd04 100644
--- a/osu.Game.Rulesets.Taiko/Objects/HitType.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/HitType.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Rulesets.Taiko.Objects
{
///
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs
index d7e37899ce..cecb99c690 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonCirclePiece.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
private const double pre_beat_transition_time = 80;
- private const float flash_opacity = 0.3f;
+ private const float kiai_flash_opacity = 0.15f;
private ColourInfo accentColour;
@@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
if (drawableHitObject.State.Value == ArmedState.Idle)
{
flash
- .FadeTo(flash_opacity)
+ .FadeTo(kiai_flash_opacity)
.Then()
.FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine);
}
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonHitExplosion.cs
index a47fd7e62e..cddae7f05b 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonHitExplosion.cs
@@ -1,9 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
@@ -18,7 +16,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
public partial class ArgonHitExplosion : CompositeDrawable, IAnimatableHitExplosion
{
private readonly TaikoSkinComponents component;
+
private readonly Circle outer;
+ private readonly Circle inner;
public ArgonHitExplosion(TaikoSkinComponents component)
{
@@ -34,13 +34,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
- Colour = ColourInfo.GradientVertical(
- new Color4(255, 227, 236, 255),
- new Color4(255, 198, 211, 255)
- ),
Masking = true,
},
- new Circle
+ inner = new Circle
{
Name = "Inner circle",
Anchor = Anchor.Centre,
@@ -48,12 +44,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
RelativeSizeAxes = Axes.Both,
Colour = Color4.White,
Size = new Vector2(0.85f),
- EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Glow,
- Colour = new Color4(255, 132, 191, 255).Opacity(0.5f),
- Radius = 45,
- },
Masking = true,
},
};
@@ -63,6 +53,16 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
{
this.FadeOut();
+ bool isRim = (drawableHitObject.HitObject as Hit)?.Type == HitType.Rim;
+
+ outer.Colour = isRim ? ArgonInputDrum.RIM_HIT_GRADIENT : ArgonInputDrum.CENTRE_HIT_GRADIENT;
+ inner.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = (isRim ? ArgonInputDrum.RIM_HIT_GLOW : ArgonInputDrum.CENTRE_HIT_GLOW).Opacity(0.5f),
+ Radius = 45,
+ };
+
switch (component)
{
case TaikoSkinComponents.TaikoExplosionGreat:
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonInputDrum.cs
index e7b0a5537a..f7b7105bdc 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonInputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Argon/ArgonInputDrum.cs
@@ -19,6 +19,20 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
{
public partial class ArgonInputDrum : AspectContainer
{
+ public static readonly ColourInfo RIM_HIT_GRADIENT = ColourInfo.GradientHorizontal(
+ new Color4(227, 248, 255, 255),
+ new Color4(198, 245, 255, 255)
+ );
+
+ public static readonly Colour4 RIM_HIT_GLOW = new Color4(126, 215, 253, 255);
+
+ public static readonly ColourInfo CENTRE_HIT_GRADIENT = ColourInfo.GradientHorizontal(
+ new Color4(255, 227, 236, 255),
+ new Color4(255, 198, 211, 255)
+ );
+
+ public static readonly Colour4 CENTRE_HIT_GLOW = new Color4(255, 147, 199, 255);
+
private const float rim_size = 0.3f;
public ArgonInputDrum()
@@ -141,14 +155,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
Anchor = anchor,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
- Colour = ColourInfo.GradientHorizontal(
- new Color4(227, 248, 255, 255),
- new Color4(198, 245, 255, 255)
- ),
+ Colour = RIM_HIT_GRADIENT,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
- Colour = new Color4(126, 215, 253, 170),
+ Colour = RIM_HIT_GLOW.Opacity(0.66f),
Radius = 50,
},
Alpha = 0,
@@ -166,14 +177,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
Anchor = anchor,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
- Colour = ColourInfo.GradientHorizontal(
- new Color4(255, 227, 236, 255),
- new Color4(255, 198, 211, 255)
- ),
+ Colour = CENTRE_HIT_GRADIENT,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
- Colour = new Color4(255, 147, 199, 255),
+ Colour = CENTRE_HIT_GLOW,
Radius = 50,
},
Size = new Vector2(1 - rim_size),
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs
index bde502bbed..b3833d372c 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
private const double pre_beat_transition_time = 80;
- private const float flash_opacity = 0.3f;
+ private const float kiai_flash_opacity = 0.15f;
[Resolved]
private DrawableHitObject drawableHitObject { get; set; } = null!;
@@ -187,7 +187,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
if (drawableHitObject.State.Value == ArmedState.Idle)
{
flashBox
- .FadeTo(flash_opacity)
+ .FadeTo(kiai_flash_opacity)
.Then()
.FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine);
}
diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
index a35fdb890d..d6824109b3 100644
--- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs
@@ -229,45 +229,27 @@ namespace osu.Game.Rulesets.Taiko
return base.GetDisplayNameForHitResult(result);
}
- public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
+ public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{
var timedHitEvents = score.HitEvents.Where(e => e.HitObject is Hit).ToList();
return new[]
{
- new StatisticRow
+ new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
{
- Columns = new[]
- {
- new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
- {
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y
- }),
- }
- },
- new StatisticRow
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y
+ }),
+ new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents)
{
- Columns = new[]
- {
- new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents)
- {
- RelativeSizeAxes = Axes.X,
- Height = 250
- }, true),
- }
- },
- new StatisticRow
+ RelativeSizeAxes = Axes.X,
+ Height = 250
+ }, true),
+ new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
- Columns = new[]
- {
- new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
- {
- new AverageHitError(timedHitEvents),
- new UnstableRate(timedHitEvents)
- }), true)
- }
- }
+ new AverageHitError(timedHitEvents),
+ new UnstableRate(timedHitEvents)
+ }), true)
};
}
}
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
index 09130ac57d..fac5e098b9 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
@@ -231,7 +231,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
protected override IBeatmap GetBeatmap() => beatmap;
- protected override Texture GetBackground() => throw new NotImplementedException();
+ public override Texture GetBackground() => throw new NotImplementedException();
protected override Track GetBeatmapTrack() => throw new NotImplementedException();
diff --git a/osu.Game.Tests/Editing/Checks/CheckBackgroundQualityTest.cs b/osu.Game.Tests/Editing/Checks/CheckBackgroundQualityTest.cs
index 295a10ba5b..3d1f7c5b17 100644
--- a/osu.Game.Tests/Editing/Checks/CheckBackgroundQualityTest.cs
+++ b/osu.Game.Tests/Editing/Checks/CheckBackgroundQualityTest.cs
@@ -131,7 +131,7 @@ namespace osu.Game.Tests.Editing.Checks
var mock = new Mock();
mock.SetupGet(w => w.Beatmap).Returns(beatmap);
- mock.SetupGet(w => w.Background).Returns(background);
+ mock.Setup(w => w.GetBackground()).Returns(background);
mock.Setup(w => w.GetStream(It.IsAny())).Returns(stream);
return mock;
diff --git a/osu.Game.Tests/Rulesets/TestSceneBrokenRulesetHandling.cs b/osu.Game.Tests/Rulesets/TestSceneBrokenRulesetHandling.cs
index c3a6b7c474..dac6beea65 100644
--- a/osu.Game.Tests/Rulesets/TestSceneBrokenRulesetHandling.cs
+++ b/osu.Game.Tests/Rulesets/TestSceneBrokenRulesetHandling.cs
@@ -73,7 +73,5 @@ namespace osu.Game.Tests.Rulesets
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null;
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null;
}
-
-#nullable enable
}
}
diff --git a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
index 8f4250799e..1523ae7027 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneBackgroundScreenDefault.cs
@@ -286,7 +286,7 @@ namespace osu.Game.Tests.Visual.Background
this.renderer = renderer;
}
- protected override Texture GetBackground() => renderer.CreateTexture(1, 1);
+ public override Texture GetBackground() => renderer.CreateTexture(1, 1);
}
private partial class TestWorkingBeatmapWithStoryboard : TestWorkingBeatmap
diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs
index 1e9982f8d4..cfa45ec6ef 100644
--- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs
+++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs
@@ -264,8 +264,9 @@ namespace osu.Game.Tests.Visual.Collections
assertCollectionName(1, "First");
}
- [Test]
- public void TestCollectionRenamedOnTextChange()
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestCollectionRenamedOnTextChange(bool commitWithEnter)
{
BeatmapCollection first = null!;
DrawableCollectionListItem firstItem = null!;
@@ -293,9 +294,19 @@ namespace osu.Game.Tests.Visual.Collections
AddStep("change first collection name", () =>
{
firstItem.ChildrenOfType().First().Text = "First";
- InputManager.Key(Key.Enter);
});
+ if (commitWithEnter)
+ AddStep("commit via enter", () => InputManager.Key(Key.Enter));
+ else
+ {
+ AddStep("commit via click away", () =>
+ {
+ InputManager.MoveMouseTo(firstItem.ScreenSpaceDrawQuad.TopLeft - new Vector2(10));
+ InputManager.Click(MouseButton.Left);
+ });
+ }
+
AddUntilStep("collection has new name", () => first.Name == "First");
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs
index 353acfa4ba..88b959a2a0 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Diagnostics;
using System.Linq;
@@ -23,8 +21,8 @@ namespace osu.Game.Tests.Visual.Editing
{
public partial class TestSceneBeatDivisorControl : OsuManualInputManagerTestScene
{
- private BeatDivisorControl beatDivisorControl;
- private BindableBeatDivisor bindableBeatDivisor;
+ private BeatDivisorControl beatDivisorControl = null!;
+ private BindableBeatDivisor bindableBeatDivisor = null!;
private SliderBar tickSliderBar => beatDivisorControl.ChildrenOfType>().Single();
private Triangle tickMarkerHead => tickSliderBar.ChildrenOfType().Single();
@@ -51,9 +49,9 @@ namespace osu.Game.Tests.Visual.Editing
[Test]
public void TestBindableBeatDivisor()
{
- AddRepeatStep("move previous", () => bindableBeatDivisor.Previous(), 2);
+ AddRepeatStep("move previous", () => bindableBeatDivisor.SelectPrevious(), 2);
AddAssert("divisor is 4", () => bindableBeatDivisor.Value == 4);
- AddRepeatStep("move next", () => bindableBeatDivisor.Next(), 1);
+ AddRepeatStep("move next", () => bindableBeatDivisor.SelectNext(), 1);
AddAssert("divisor is 12", () => bindableBeatDivisor.Value == 8);
}
@@ -101,16 +99,22 @@ namespace osu.Game.Tests.Visual.Editing
public void TestBeatChevronNavigation()
{
switchBeatSnap(1);
+ assertBeatSnap(16);
+
+ switchBeatSnap(-4);
assertBeatSnap(1);
switchBeatSnap(3);
assertBeatSnap(8);
- switchBeatSnap(-1);
+ switchBeatSnap(3);
+ assertBeatSnap(16);
+
+ switchBeatSnap(-2);
assertBeatSnap(4);
switchBeatSnap(-3);
- assertBeatSnap(16);
+ assertBeatSnap(1);
}
[Test]
@@ -163,9 +167,11 @@ namespace osu.Game.Tests.Visual.Editing
switchPresets(1);
assertPreset(BeatDivisorType.Triplets);
+ assertBeatSnap(6);
switchPresets(1);
assertPreset(BeatDivisorType.Common);
+ assertBeatSnap(4);
switchPresets(-1);
assertPreset(BeatDivisorType.Triplets);
@@ -181,6 +187,7 @@ namespace osu.Game.Tests.Visual.Editing
setDivisorViaInput(15);
assertPreset(BeatDivisorType.Custom, 15);
+ assertBeatSnap(15);
switchBeatSnap(-1);
assertBeatSnap(5);
@@ -190,12 +197,14 @@ namespace osu.Game.Tests.Visual.Editing
setDivisorViaInput(5);
assertPreset(BeatDivisorType.Custom, 15);
+ assertBeatSnap(5);
switchPresets(1);
assertPreset(BeatDivisorType.Common);
switchPresets(-1);
- assertPreset(BeatDivisorType.Triplets);
+ assertPreset(BeatDivisorType.Custom, 15);
+ assertBeatSnap(15);
}
private void switchBeatSnap(int direction) => AddRepeatStep($"move snap {(direction > 0 ? "forward" : "backward")}", () =>
@@ -207,7 +216,7 @@ namespace osu.Game.Tests.Visual.Editing
}, Math.Abs(direction));
private void assertBeatSnap(int expected) => AddAssert($"beat snap is {expected}",
- () => bindableBeatDivisor.Value == expected);
+ () => bindableBeatDivisor.Value, () => Is.EqualTo(expected));
private void switchPresets(int direction) => AddRepeatStep($"move presets {(direction > 0 ? "forward" : "backward")}", () =>
{
@@ -219,7 +228,7 @@ namespace osu.Game.Tests.Visual.Editing
private void assertPreset(BeatDivisorType type, int? maxDivisor = null)
{
- AddAssert($"preset is {type}", () => bindableBeatDivisor.ValidDivisors.Value.Type == type);
+ AddAssert($"preset is {type}", () => bindableBeatDivisor.ValidDivisors.Value.Type, () => Is.EqualTo(type));
if (type == BeatDivisorType.Custom)
{
@@ -237,7 +246,7 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.Click(MouseButton.Left);
});
- BeatDivisorControl.CustomDivisorPopover popover = null;
+ BeatDivisorControl.CustomDivisorPopover? popover = null;
AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType().SingleOrDefault()) != null && popover.IsLoaded);
AddStep($"set divisor to {divisor}", () =>
{
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
index 2250868a39..007716bd6c 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorTestGameplay.cs
@@ -209,10 +209,14 @@ namespace osu.Game.Tests.Visual.Editing
public override void TearDownSteps()
{
base.TearDownSteps();
- AddStep("delete imported", () =>
+ AddStep("delete imported", () => Realm.Write(r =>
{
- beatmaps.Delete(importedBeatmapSet);
- });
+ // delete from realm directly rather than via `BeatmapManager` to avoid cross-test pollution
+ // (`BeatmapManager.Delete()` uses soft deletion, which can lead to beatmap reuse between test cases).
+ r.RemoveAll();
+ r.RemoveAll();
+ r.RemoveAll();
+ }));
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs
index 6eae795630..f3701b664c 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapOffsetControl.cs
@@ -8,6 +8,8 @@ using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Overlays.Settings;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Tests.Visual.Ranking;
@@ -49,6 +51,21 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any());
}
+ [Test]
+ public void TestModRemovingTimedInputs()
+ {
+ AddStep("Set score with mod removing timed inputs", () =>
+ {
+ offsetControl.ReferenceScore.Value = new ScoreInfo
+ {
+ HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(10),
+ Mods = new Mod[] { new OsuModRelax() }
+ };
+ });
+
+ AddAssert("No calibration button", () => !offsetControl.ChildrenOfType().Any());
+ }
+
[Test]
public void TestCalibrationFromZero()
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs
index 3efc7fbd30..6d309078e6 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs
@@ -129,7 +129,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
Playlist =
{
- new MultiplayerPlaylistItem(playlistItem),
+ TestMultiplayerClient.CreateMultiplayerPlaylistItem(playlistItem),
},
Users = { localUser },
Host = localUser,
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index d747d23229..09624f63b7 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -906,7 +906,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
enterGameplay();
AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 }));
- AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(
+ AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, TestMultiplayerClient.CreateMultiplayerPlaylistItem(
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
@@ -938,7 +938,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
enterGameplay();
AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 }));
- AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(
+ AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, TestMultiplayerClient.CreateMultiplayerPlaylistItem(
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs
index d7578b4114..2100f82886 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs
@@ -215,7 +215,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
///
private void addItemStep(bool expired = false, int? userId = null) => AddStep("add item", () =>
{
- MultiplayerClient.AddUserPlaylistItem(userId ?? API.LocalUser.Value.OnlineID, new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)
+ MultiplayerClient.AddUserPlaylistItem(userId ?? API.LocalUser.Value.OnlineID, TestMultiplayerClient.CreateMultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)
{
Expired = expired,
PlayedAt = DateTimeOffset.Now
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs
index bb37f1a5a7..47fb4e06ea 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs
@@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("add playlist item", () =>
{
- MultiplayerPlaylistItem item = new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap));
+ MultiplayerPlaylistItem item = TestMultiplayerClient.CreateMultiplayerPlaylistItem(new PlaylistItem(importedBeatmap));
MultiplayerClient.AddUserPlaylistItem(userId(), item).WaitSafely();
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs
index 1b2bb57b84..5483be5676 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs
@@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
- AddStep("test gameplay", () => ((Editor)Game.ScreenStack.CurrentScreen).TestGameplay());
+ AddStep("test gameplay", () => getEditor().TestGameplay());
AddUntilStep("wait for player", () =>
{
@@ -141,6 +141,37 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for editor exit", () => Game.ScreenStack.CurrentScreen is not Editor);
}
- private EditorBeatmap getEditorBeatmap() => ((Editor)Game.ScreenStack.CurrentScreen).ChildrenOfType().Single();
+ [Test]
+ public void TestLastTimestampRememberedOnExit()
+ {
+ BeatmapSetInfo beatmapSet = null!;
+
+ AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
+ AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
+
+ AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
+ AddUntilStep("wait for song select",
+ () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
+ && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
+ && songSelect.IsLoaded);
+
+ AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
+ AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
+
+ AddStep("seek to arbitrary time", () => getEditor().ChildrenOfType().First().Seek(1234));
+ AddUntilStep("time is correct", () => getEditor().ChildrenOfType().First().CurrentTime, () => Is.EqualTo(1234));
+
+ AddStep("exit editor", () => InputManager.Key(Key.Escape));
+ AddUntilStep("wait for editor exit", () => Game.ScreenStack.CurrentScreen is not Editor);
+
+ AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit());
+
+ AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
+ AddUntilStep("time is correct", () => getEditor().ChildrenOfType().First().CurrentTime, () => Is.EqualTo(1234));
+ }
+
+ private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType().Single();
+
+ private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen;
}
}
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
index 193cec8907..18aef99ccd 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs
@@ -17,6 +17,7 @@ using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.API;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
@@ -539,6 +540,11 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("open room", () => multiplayerComponents.ChildrenOfType().Single().Open());
AddStep("press back button", () => Game.ChildrenOfType().First().Action());
AddWaitStep("wait two frames", 2);
+
+ AddStep("exit lounge", () => Game.ScreenStack.Exit());
+ // `TestMultiplayerComponents` registers a request handler in its BDL, but never unregisters it.
+ // to prevent the handler living for longer than it should be, clean up manually.
+ AddStep("clean up multiplayer request handler", () => ((DummyAPIAccess)API).HandleRequest = null);
}
[Test]
diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs
index 4f825e1191..885c00be80 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Linq;
using System.Threading;
@@ -24,8 +22,8 @@ namespace osu.Game.Tests.Visual.Online
{
private readonly APIUser streamingUser = new APIUser { Id = 2, Username = "Test user" };
- private TestSpectatorClient spectatorClient;
- private CurrentlyPlayingDisplay currentlyPlaying;
+ private TestSpectatorClient spectatorClient = null!;
+ private CurrentlyPlayingDisplay currentlyPlaying = null!;
[SetUpSteps]
public void SetUpSteps()
@@ -88,13 +86,13 @@ namespace osu.Game.Tests.Visual.Online
"pishifat"
};
- protected override Task ComputeValueAsync(int lookup, CancellationToken token = default)
+ protected override Task ComputeValueAsync(int lookup, CancellationToken token = default)
{
// tests against failed lookups
if (lookup == 13)
- return Task.FromResult(null);
+ return Task.FromResult(null);
- return Task.FromResult(new APIUser
+ return Task.FromResult(new APIUser
{
Id = lookup,
Username = usernames[lookup % usernames.Length],
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs
index fcd5f97fcc..67211a3b72 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs
@@ -174,78 +174,33 @@ namespace osu.Game.Tests.Visual.Ranking
private class TestRulesetAllStatsRequireHitEvents : TestRuleset
{
- public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
+ public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
{
- return new[]
- {
- new StatisticRow
- {
- Columns = new[]
- {
- new StatisticItem("Statistic Requiring Hit Events 1",
- () => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true)
- }
- },
- new StatisticRow
- {
- Columns = new[]
- {
- new StatisticItem("Statistic Requiring Hit Events 2",
- () => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true)
- }
- }
- };
- }
+ new StatisticItem("Statistic Requiring Hit Events 1", () => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true),
+ new StatisticItem("Statistic Requiring Hit Events 2", () => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true)
+ };
}
private class TestRulesetNoStatsRequireHitEvents : TestRuleset
{
- public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
+ public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{
return new[]
{
- new StatisticRow
- {
- Columns = new[]
- {
- new StatisticItem("Statistic Not Requiring Hit Events 1",
- () => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events"))
- }
- },
- new StatisticRow
- {
- Columns = new[]
- {
- new StatisticItem("Statistic Not Requiring Hit Events 2",
- () => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events"))
- }
- }
+ new StatisticItem("Statistic Not Requiring Hit Events 1", () => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events")),
+ new StatisticItem("Statistic Not Requiring Hit Events 2", () => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events"))
};
}
}
private class TestRulesetMixed : TestRuleset
{
- public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
+ public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{
return new[]
{
- new StatisticRow
- {
- Columns = new[]
- {
- new StatisticItem("Statistic Requiring Hit Events",
- () => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true)
- }
- },
- new StatisticRow
- {
- Columns = new[]
- {
- new StatisticItem("Statistic Not Requiring Hit Events",
- () => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events"))
- }
- }
+ new StatisticItem("Statistic Requiring Hit Events", () => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true),
+ new StatisticItem("Statistic Not Requiring Hit Events", () => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events"))
};
}
}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs
index c2537cff79..379bd838cd 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapMetadataDisplay.cs
@@ -1,10 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -24,10 +23,10 @@ namespace osu.Game.Tests.Visual.SongSelect
{
public partial class TestSceneBeatmapMetadataDisplay : OsuTestScene
{
- private BeatmapMetadataDisplay display;
+ private BeatmapMetadataDisplay display = null!;
[Resolved]
- private BeatmapManager manager { get; set; }
+ private BeatmapManager manager { get; set; } = null!;
[Cached(typeof(BeatmapDifficultyCache))]
private readonly TestBeatmapDifficultyCache testDifficultyCache = new TestBeatmapDifficultyCache();
@@ -121,7 +120,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private partial class TestBeatmapDifficultyCache : BeatmapDifficultyCache
{
- private TaskCompletionSource calculationBlocker;
+ private TaskCompletionSource? calculationBlocker;
private bool blockCalculation;
@@ -142,10 +141,13 @@ namespace osu.Game.Tests.Visual.SongSelect
}
}
- public override async Task GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo rulesetInfo = null, IEnumerable mods = null, CancellationToken cancellationToken = default)
+ public override async Task GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo? rulesetInfo = null, IEnumerable? mods = null, CancellationToken cancellationToken = default)
{
if (blockCalculation)
+ {
+ Debug.Assert(calculationBlocker != null);
await calculationBlocker.Task.ConfigureAwait(false);
+ }
return await base.GetDifficultyAsync(beatmapInfo, rulesetInfo, mods, cancellationToken).ConfigureAwait(false);
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs
index 05fffc903d..3a1eb554ab 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs
@@ -80,6 +80,24 @@ namespace osu.Game.Tests.Visual.UserInterface
});
}
+ [Test]
+ public void TestCorrectScrollToWhenContentLoads()
+ {
+ AddRepeatStep("add many sections", () => append(1f), 3);
+
+ AddStep("add section with delayed load content", () =>
+ {
+ container.Add(new TestDelayedLoadSection("delayed"));
+ });
+
+ AddStep("add final section", () => append(0.5f));
+
+ AddStep("scroll to final section", () => container.ScrollTo(container.Children.Last()));
+
+ AddUntilStep("correct section selected", () => container.SelectedSection.Value == container.Children.Last());
+ AddUntilStep("wait for scroll to section", () => container.ScreenSpaceDrawQuad.AABBFloat.Contains(container.Children.Last().ScreenSpaceDrawQuad.AABBFloat));
+ }
+
[Test]
public void TestSelection()
{
@@ -196,6 +214,33 @@ namespace osu.Game.Tests.Visual.UserInterface
InputManager.ScrollVerticalBy(direction);
}
+ private partial class TestDelayedLoadSection : TestSection
+ {
+ public TestDelayedLoadSection(string label)
+ : base(label)
+ {
+ BackgroundColour = default_colour;
+ Width = 300;
+ AutoSizeAxes = Axes.Y;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Box box;
+
+ Add(box = new Box
+ {
+ Alpha = 0.01f,
+ RelativeSizeAxes = Axes.X,
+ });
+
+ // Emulate an operation that will be inhibited by IsMaskedAway.
+ box.ResizeHeightTo(2000, 50);
+ }
+ }
+
private partial class TestSection : TestBox
{
public bool Selected
diff --git a/osu.Game.Tests/WaveformTestBeatmap.cs b/osu.Game.Tests/WaveformTestBeatmap.cs
index 5e41392560..12660ed2e1 100644
--- a/osu.Game.Tests/WaveformTestBeatmap.cs
+++ b/osu.Game.Tests/WaveformTestBeatmap.cs
@@ -51,7 +51,7 @@ namespace osu.Game.Tests
protected override IBeatmap GetBeatmap() => beatmap;
- protected override Texture GetBackground() => null;
+ public override Texture GetBackground() => null;
protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile));
diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs
index 5695cb5574..94086f10f2 100644
--- a/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs
+++ b/osu.Game.Tournament.Tests/Screens/TestSceneMapPoolScreen.cs
@@ -24,6 +24,9 @@ namespace osu.Game.Tournament.Tests.Screens
Add(screen = new MapPoolScreen { Width = 0.7f });
}
+ [SetUp]
+ public void SetUp() => Schedule(() => Ladder.SplitMapPoolByMods.Value = true);
+
[Test]
public void TestFewMaps()
{
@@ -92,7 +95,7 @@ namespace osu.Game.Tournament.Tests.Screens
Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear();
for (int i = 0; i < 11; i++)
- addBeatmap(i > 4 ? $"M{i}" : "NM");
+ addBeatmap(i > 4 ? Ruleset.Value.CreateInstance().AllMods.ElementAt(i).Acronym : "NM");
});
AddStep("reset match", () =>
@@ -118,7 +121,7 @@ namespace osu.Game.Tournament.Tests.Screens
Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear();
for (int i = 0; i < 12; i++)
- addBeatmap(i > 4 ? $"M{i}" : "NM");
+ addBeatmap(i > 4 ? Ruleset.Value.CreateInstance().AllMods.ElementAt(i).Acronym : "NM");
});
AddStep("reset match", () =>
@@ -130,7 +133,27 @@ namespace osu.Game.Tournament.Tests.Screens
assertThreeWide();
}
- private void addBeatmap(string mods = "nm")
+ [Test]
+ public void TestSplitMapPoolByMods()
+ {
+ AddStep("load many maps", () =>
+ {
+ Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear();
+
+ for (int i = 0; i < 12; i++)
+ addBeatmap(i > 4 ? Ruleset.Value.CreateInstance().AllMods.ElementAt(i).Acronym : "NM");
+ });
+
+ AddStep("disable splitting map pool by mods", () => Ladder.SplitMapPoolByMods.Value = false);
+
+ AddStep("reset match", () =>
+ {
+ Ladder.CurrentMatch.Value = new TournamentMatch();
+ Ladder.CurrentMatch.Value = Ladder.Matches.First();
+ });
+ }
+
+ private void addBeatmap(string mods = "NM")
{
Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Add(new RoundBeatmap
{
diff --git a/osu.Game.Tournament/IPC/TourneyState.cs b/osu.Game.Tournament/IPC/TourneyState.cs
index 2c7253dc10..ef1c612a53 100644
--- a/osu.Game.Tournament/IPC/TourneyState.cs
+++ b/osu.Game.Tournament/IPC/TourneyState.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Tournament.IPC
{
public enum TourneyState
diff --git a/osu.Game.Tournament/Models/LadderInfo.cs b/osu.Game.Tournament/Models/LadderInfo.cs
index 6b64a1156e..cb4e8bc16a 100644
--- a/osu.Game.Tournament/Models/LadderInfo.cs
+++ b/osu.Game.Tournament/Models/LadderInfo.cs
@@ -42,5 +42,7 @@ namespace osu.Game.Tournament.Models
};
public Bindable AutoProgressScreens = new BindableBool(true);
+
+ public Bindable SplitMapPoolByMods = new BindableBool(true);
}
}
diff --git a/osu.Game.Tournament/Screens/Ladder/Components/ConditionalTournamentMatch.cs b/osu.Game.Tournament/Screens/Ladder/Components/ConditionalTournamentMatch.cs
index 04155fcb89..16224a7fb4 100644
--- a/osu.Game.Tournament/Screens/Ladder/Components/ConditionalTournamentMatch.cs
+++ b/osu.Game.Tournament/Screens/Ladder/Components/ConditionalTournamentMatch.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Tournament.Models;
namespace osu.Game.Tournament.Screens.Ladder.Components
diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs
index f0e34d78c3..cb6c5902ec 100644
--- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs
+++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Tournament.Screens.MapPool
{
public partial class MapPoolScreen : TournamentMatchScreen
{
- private readonly FillFlowContainer> mapFlows;
+ private FillFlowContainer> mapFlows;
[Resolved(canBeNull: true)]
private TournamentSceneManager sceneManager { get; set; }
@@ -32,12 +32,13 @@ namespace osu.Game.Tournament.Screens.MapPool
private TeamColour pickColour;
private ChoiceType pickType;
- private readonly OsuButton buttonRedBan;
- private readonly OsuButton buttonBlueBan;
- private readonly OsuButton buttonRedPick;
- private readonly OsuButton buttonBluePick;
+ private OsuButton buttonRedBan;
+ private OsuButton buttonBlueBan;
+ private OsuButton buttonRedPick;
+ private OsuButton buttonBluePick;
- public MapPoolScreen()
+ [BackgroundDependencyLoader]
+ private void load(MatchIPCInfo ipc)
{
InternalChildren = new Drawable[]
{
@@ -98,15 +99,26 @@ namespace osu.Game.Tournament.Screens.MapPool
Action = reset
},
new ControlPanel.Spacer(),
+ new OsuCheckbox
+ {
+ LabelText = "Split display by mods",
+ Current = LadderInfo.SplitMapPoolByMods,
+ },
},
}
};
+
+ ipc.Beatmap.BindValueChanged(beatmapChanged);
}
- [BackgroundDependencyLoader]
- private void load(MatchIPCInfo ipc)
+ private Bindable splitMapPoolByMods;
+
+ protected override void LoadComplete()
{
- ipc.Beatmap.BindValueChanged(beatmapChanged);
+ base.LoadComplete();
+
+ splitMapPoolByMods = LadderInfo.SplitMapPoolByMods.GetBoundCopy();
+ splitMapPoolByMods.BindValueChanged(_ => updateDisplay());
}
private void beatmapChanged(ValueChangedEvent beatmap)
@@ -213,24 +225,27 @@ namespace osu.Game.Tournament.Screens.MapPool
protected override void CurrentMatchChanged(ValueChangedEvent match)
{
base.CurrentMatchChanged(match);
+ updateDisplay();
+ }
+ private void updateDisplay()
+ {
mapFlows.Clear();
- if (match.NewValue == null)
+ if (CurrentMatch.Value == null)
return;
int totalRows = 0;
- if (match.NewValue.Round.Value != null)
+ if (CurrentMatch.Value.Round.Value != null)
{
FillFlowContainer currentFlow = null;
- string currentMod = null;
-
+ string currentMods = null;
int flowCount = 0;
- foreach (var b in match.NewValue.Round.Value.Beatmaps)
+ foreach (var b in CurrentMatch.Value.Round.Value.Beatmaps)
{
- if (currentFlow == null || currentMod != b.Mods)
+ if (currentFlow == null || (LadderInfo.SplitMapPoolByMods.Value && currentMods != b.Mods))
{
mapFlows.Add(currentFlow = new FillFlowContainer
{
@@ -240,7 +255,7 @@ namespace osu.Game.Tournament.Screens.MapPool
AutoSizeAxes = Axes.Y
});
- currentMod = b.Mods;
+ currentMods = b.Mods;
totalRows++;
flowCount = 0;
diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs
index 393feff087..5019d64276 100644
--- a/osu.Game/Beatmaps/BeatmapInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapInfo.cs
@@ -171,6 +171,11 @@ namespace osu.Game.Beatmaps
public double TimelineZoom { get; set; } = 1.0;
+ ///
+ /// The time in milliseconds when last exiting the editor with this beatmap loaded.
+ ///
+ public double? EditorTimestamp { get; set; }
+
[Ignored]
public CountdownType Countdown { get; set; } = CountdownType.Normal;
diff --git a/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs b/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs
new file mode 100644
index 0000000000..acd60b664d
--- /dev/null
+++ b/osu.Game/Beatmaps/BeatmapPanelBackgroundTextureLoaderStore.cs
@@ -0,0 +1,95 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.IO.Stores;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Processing;
+
+namespace osu.Game.Beatmaps
+{
+ // Implementation of this class is based off of `MaxDimensionLimitedTextureLoaderStore`.
+ // If issues are found it's worth checking to make sure similar issues exist there.
+ public class BeatmapPanelBackgroundTextureLoaderStore : IResourceStore
+ {
+ // The aspect ratio of SetPanelBackground at its maximum size (very tall window).
+ private const float minimum_display_ratio = 512 / 80f;
+
+ private readonly IResourceStore? textureStore;
+
+ public BeatmapPanelBackgroundTextureLoaderStore(IResourceStore? textureStore)
+ {
+ this.textureStore = textureStore;
+ }
+
+ public void Dispose()
+ {
+ textureStore?.Dispose();
+ }
+
+ public TextureUpload Get(string name)
+ {
+ var textureUpload = textureStore?.Get(name);
+
+ // NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp.
+ if (textureUpload == null)
+ return null!;
+
+ return limitTextureUploadSize(textureUpload);
+ }
+
+ public async Task GetAsync(string name, CancellationToken cancellationToken = new CancellationToken())
+ {
+ // NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp.
+ if (textureStore == null)
+ return null!;
+
+ var textureUpload = await textureStore.GetAsync(name, cancellationToken).ConfigureAwait(false);
+
+ if (textureUpload == null)
+ return null!;
+
+ return await Task.Run(() => limitTextureUploadSize(textureUpload), cancellationToken).ConfigureAwait(false);
+ }
+
+ private TextureUpload limitTextureUploadSize(TextureUpload textureUpload)
+ {
+ var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height);
+
+ // The original texture upload will no longer be returned or used.
+ textureUpload.Dispose();
+
+ Size size = image.Size();
+
+ // Assume that panel backgrounds are always displayed using `FillMode.Fill`.
+ // Also assume that all backgrounds are wider than they are tall, so the
+ // fill is always going to be based on width.
+ //
+ // We need to include enough height to make this work for all ratio panels are displayed at.
+ int usableHeight = (int)Math.Ceiling(size.Width * 1 / minimum_display_ratio);
+
+ usableHeight = Math.Min(size.Height, usableHeight);
+
+ // Crop the centre region of the background for now.
+ Rectangle cropRectangle = new Rectangle(
+ 0,
+ (size.Height - usableHeight) / 2,
+ size.Width,
+ usableHeight
+ );
+
+ image.Mutate(i => i.Crop(cropRectangle));
+
+ return new TextureUpload(image);
+ }
+
+ public Stream? GetStream(string name) => textureStore?.GetStream(name);
+
+ public IEnumerable GetAvailableResources() => textureStore?.GetAvailableResources() ?? Array.Empty();
+ }
+}
diff --git a/osu.Game/Beatmaps/DifficultyRating.cs b/osu.Game/Beatmaps/DifficultyRating.cs
index 478c0e36df..f0ee0ad705 100644
--- a/osu.Game/Beatmaps/DifficultyRating.cs
+++ b/osu.Game/Beatmaps/DifficultyRating.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Beatmaps
{
public enum DifficultyRating
diff --git a/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs
index 767504fcb1..5b9cf6846c 100644
--- a/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs
+++ b/osu.Game/Beatmaps/Drawables/BeatmapBackgroundSprite.cs
@@ -23,8 +23,9 @@ namespace osu.Game.Beatmaps.Drawables
[BackgroundDependencyLoader]
private void load()
{
- if (working.Background != null)
- Texture = working.Background;
+ var background = working.GetBackground();
+ if (background != null)
+ Texture = background;
}
}
}
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs
index 5c6f0c4ee1..175c15ea7b 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs
@@ -106,12 +106,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
new Drawable[]
{
- new OsuSpriteText
+ new TruncatingSpriteText
{
Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title),
Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
- Truncate = true
},
titleBadgeArea = new FillFlowContainer
{
@@ -140,21 +139,19 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
new[]
{
- new OsuSpriteText
+ new TruncatingSpriteText
{
Text = createArtistText(),
Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
- Truncate = true
},
Empty()
},
}
},
- new OsuSpriteText
+ new TruncatingSpriteText
{
RelativeSizeAxes = Axes.X,
- Truncate = true,
Text = BeatmapSet.Source,
Shadow = false,
Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold),
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs
index 720d892495..18e1584a98 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs
@@ -107,12 +107,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
new Drawable[]
{
- new OsuSpriteText
+ new TruncatingSpriteText
{
Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title),
Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
- Truncate = true
},
titleBadgeArea = new FillFlowContainer
{
@@ -141,12 +140,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
new[]
{
- new OsuSpriteText
+ new TruncatingSpriteText
{
Text = createArtistText(),
Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
- Truncate = true
},
Empty()
},
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardSize.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardSize.cs
index 1f6538a890..098265506d 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardSize.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardSize.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Beatmaps.Drawables.Cards
{
///
diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
index 0b390a2ab5..8089d789c1 100644
--- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
@@ -52,7 +52,7 @@ namespace osu.Game.Beatmaps
protected override IBeatmap GetBeatmap() => new Beatmap();
- protected override Texture GetBackground() => textures?.Get(@"Backgrounds/bg4");
+ public override Texture GetBackground() => textures?.Get(@"Backgrounds/bg4");
protected override Track GetBeatmapTrack() => GetVirtualTrack();
diff --git a/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs b/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs
index 02fcde5257..0b53278ab3 100644
--- a/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/FlatFileWorkingBeatmap.cs
@@ -43,7 +43,7 @@ namespace osu.Game.Beatmaps
}
protected override IBeatmap GetBeatmap() => beatmap;
- protected override Texture GetBackground() => throw new NotImplementedException();
+ public override Texture GetBackground() => throw new NotImplementedException();
protected override Track GetBeatmapTrack() => throw new NotImplementedException();
protected internal override ISkin GetSkin() => throw new NotImplementedException();
public override Stream GetStream(string storagePath) => throw new NotImplementedException();
diff --git a/osu.Game/Beatmaps/IBeatmapResourceProvider.cs b/osu.Game/Beatmaps/IBeatmapResourceProvider.cs
index 22ff7ce8c8..9e79e03785 100644
--- a/osu.Game/Beatmaps/IBeatmapResourceProvider.cs
+++ b/osu.Game/Beatmaps/IBeatmapResourceProvider.cs
@@ -9,13 +9,18 @@ using osu.Game.IO;
namespace osu.Game.Beatmaps
{
- public interface IBeatmapResourceProvider : IStorageResourceProvider
+ internal interface IBeatmapResourceProvider : IStorageResourceProvider
{
///
/// Retrieve a global large texture store, used for loading beatmap backgrounds.
///
TextureStore LargeTextureStore { get; }
+ ///
+ /// Retrieve a global large texture store, used specifically for retrieving cropped beatmap panel backgrounds.
+ ///
+ TextureStore BeatmapPanelTextureStore { get; }
+
///
/// Access a global track store for retrieving beatmap tracks from.
///
diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs
index 0f0e72b0ac..bdfa6bdf6d 100644
--- a/osu.Game/Beatmaps/IWorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs
@@ -32,12 +32,12 @@ namespace osu.Game.Beatmaps
///
/// Whether the Beatmap has finished loading.
///
- public bool BeatmapLoaded { get; }
+ bool BeatmapLoaded { get; }
///
/// Whether the Track has finished loading.
///
- public bool TrackLoaded { get; }
+ bool TrackLoaded { get; }
///
/// Retrieves the which this represents.
@@ -47,7 +47,12 @@ namespace osu.Game.Beatmaps
///
/// Retrieves the background for this .
///
- Texture Background { get; }
+ Texture GetBackground();
+
+ ///
+ /// Retrieves a cropped background for this used for display on panels.
+ ///
+ Texture GetPanelBackground();
///
/// Retrieves the for the of this .
@@ -124,12 +129,12 @@ namespace osu.Game.Beatmaps
///
/// Beings loading the contents of this asynchronously.
///
- public void BeginAsyncLoad();
+ void BeginAsyncLoad();
///
/// Cancels the asynchronous loading of the contents of this .
///
- public void CancelAsyncLoad();
+ void CancelAsyncLoad();
///
/// Reads the correct track restart point from beatmap metadata and sets looping to enabled.
diff --git a/osu.Game/Beatmaps/Legacy/LegacyOrigins.cs b/osu.Game/Beatmaps/Legacy/LegacyOrigins.cs
index 62b0edc384..31f67d6dfd 100644
--- a/osu.Game/Beatmaps/Legacy/LegacyOrigins.cs
+++ b/osu.Game/Beatmaps/Legacy/LegacyOrigins.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Beatmaps.Legacy
{
internal enum LegacyOrigins
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index 59a71fd80c..25159996f3 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -34,8 +34,6 @@ namespace osu.Game.Beatmaps
public Storyboard Storyboard => storyboard.Value;
- public Texture Background => GetBackground(); // Texture uses ref counting, so we want to return a new instance every usage.
-
public ISkin Skin => skin.Value;
private AudioManager audioManager { get; }
@@ -67,7 +65,8 @@ namespace osu.Game.Beatmaps
protected virtual Storyboard GetStoryboard() => new Storyboard { BeatmapInfo = BeatmapInfo };
protected abstract IBeatmap GetBeatmap();
- protected abstract Texture GetBackground();
+ public abstract Texture GetBackground();
+ public virtual Texture GetPanelBackground() => GetBackground();
protected abstract Track GetBeatmapTrack();
///
diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
index ef843909d8..78eed626f2 100644
--- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs
@@ -42,6 +42,7 @@ namespace osu.Game.Beatmaps
private readonly AudioManager audioManager;
private readonly IResourceStore resources;
private readonly LargeTextureStore largeTextureStore;
+ private readonly LargeTextureStore beatmapPanelTextureStore;
private readonly ITrackStore trackStore;
private readonly IResourceStore files;
@@ -58,6 +59,7 @@ namespace osu.Game.Beatmaps
this.host = host;
this.files = files;
largeTextureStore = new LargeTextureStore(host?.Renderer ?? new DummyRenderer(), host?.CreateTextureLoaderStore(files));
+ beatmapPanelTextureStore = new LargeTextureStore(host?.Renderer ?? new DummyRenderer(), new BeatmapPanelBackgroundTextureLoaderStore(host?.CreateTextureLoaderStore(files)));
this.trackStore = trackStore;
}
@@ -110,6 +112,7 @@ namespace osu.Game.Beatmaps
#region IResourceStorageProvider
TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore;
+ TextureStore IBeatmapResourceProvider.BeatmapPanelTextureStore => beatmapPanelTextureStore;
ITrackStore IBeatmapResourceProvider.Tracks => trackStore;
IRenderer IStorageResourceProvider.Renderer => host?.Renderer ?? new DummyRenderer();
AudioManager IStorageResourceProvider.AudioManager => audioManager;
@@ -160,7 +163,11 @@ namespace osu.Game.Beatmaps
}
}
- protected override Texture GetBackground()
+ public override Texture GetPanelBackground() => getBackgroundFromStore(resources.BeatmapPanelTextureStore);
+
+ public override Texture GetBackground() => getBackgroundFromStore(resources.LargeTextureStore);
+
+ private Texture getBackgroundFromStore(TextureStore store)
{
if (string.IsNullOrEmpty(Metadata?.BackgroundFile))
return null;
@@ -168,7 +175,7 @@ namespace osu.Game.Beatmaps
try
{
string fileStorePath = BeatmapSetInfo.GetPathForFile(Metadata.BackgroundFile);
- var texture = resources.LargeTextureStore.Get(fileStorePath);
+ var texture = store.Get(fileStorePath);
if (texture == null)
{
@@ -257,7 +264,7 @@ namespace osu.Game.Beatmaps
if (beatmapFileStream == null)
{
Logger.Log($"Beatmap failed to load (file {BeatmapInfo.Path} not found on disk at expected location {fileStorePath})", level: LogLevel.Error);
- return null;
+ return new Storyboard();
}
using (var reader = new LineBufferedReader(beatmapFileStream))
diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs
index 23156b1ad5..0ab0ff520d 100644
--- a/osu.Game/Collections/DrawableCollectionListItem.cs
+++ b/osu.Game/Collections/DrawableCollectionListItem.cs
@@ -86,6 +86,7 @@ namespace osu.Game.Collections
RelativeSizeAxes = Axes.Both,
Size = Vector2.One,
CornerRadius = item_height / 2,
+ CommitOnFocusLost = true,
PlaceholderText = collection.IsManaged ? string.Empty : "Create a new collection"
},
}
diff --git a/osu.Game/Configuration/IntroSequence.cs b/osu.Game/Configuration/IntroSequence.cs
index 8327ea2f57..5672c44bbe 100644
--- a/osu.Game/Configuration/IntroSequence.cs
+++ b/osu.Game/Configuration/IntroSequence.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Configuration
{
public enum IntroSequence
diff --git a/osu.Game/Configuration/ReleaseStream.cs b/osu.Game/Configuration/ReleaseStream.cs
index 9cdd91bfd0..ed0bee1dd8 100644
--- a/osu.Game/Configuration/ReleaseStream.cs
+++ b/osu.Game/Configuration/ReleaseStream.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Configuration
{
public enum ReleaseStream
diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs
index 276563e163..5e2f0c2128 100644
--- a/osu.Game/Configuration/SessionStatics.cs
+++ b/osu.Game/Configuration/SessionStatics.cs
@@ -6,6 +6,7 @@
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
+using osu.Game.Overlays.Mods;
namespace osu.Game.Configuration
{
@@ -21,6 +22,7 @@ namespace osu.Game.Configuration
SetDefault(Static.LowBatteryNotificationShownOnce, false);
SetDefault(Static.FeaturedArtistDisclaimerShownOnce, false);
SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null);
+ SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null);
SetDefault(Static.SeasonalBackgrounds, null);
}
@@ -56,5 +58,11 @@ namespace osu.Game.Configuration
/// Used to debounce hover sounds game-wide to avoid volume saturation, especially in scrolling views with many UI controls like .
///
LastHoverSoundPlaybackTime,
+
+ ///
+ /// The last playback time in milliseconds of an on/off sample (from ).
+ /// Used to debounce on/off sounds game-wide to avoid volume saturation, especially in activating mod presets with many mods.
+ ///
+ LastModSelectPanelSamplePlaybackTime
}
}
diff --git a/osu.Game/Configuration/ToolbarClockDisplayMode.cs b/osu.Game/Configuration/ToolbarClockDisplayMode.cs
index 682e221ef8..2f42f7a9b5 100644
--- a/osu.Game/Configuration/ToolbarClockDisplayMode.cs
+++ b/osu.Game/Configuration/ToolbarClockDisplayMode.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Configuration
{
public enum ToolbarClockDisplayMode
diff --git a/osu.Game/Database/BeatmapLookupCache.cs b/osu.Game/Database/BeatmapLookupCache.cs
index d9bf0138dc..973c25ec4f 100644
--- a/osu.Game/Database/BeatmapLookupCache.cs
+++ b/osu.Game/Database/BeatmapLookupCache.cs
@@ -1,13 +1,10 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using JetBrains.Annotations;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@@ -21,8 +18,7 @@ namespace osu.Game.Database
/// The beatmap to lookup.
/// An optional cancellation token.
/// The populated beatmap, or null if the beatmap does not exist or the request could not be satisfied.
- [ItemCanBeNull]
- public Task GetBeatmapAsync(int beatmapId, CancellationToken token = default) => LookupAsync(beatmapId, token);
+ public Task GetBeatmapAsync(int beatmapId, CancellationToken token = default) => LookupAsync(beatmapId, token);
///
/// Perform an API lookup on the specified beatmaps, populating a model.
@@ -30,10 +26,10 @@ namespace osu.Game.Database
/// The beatmaps to lookup.
/// An optional cancellation token.
/// The populated beatmaps. May include null results for failed retrievals.
- public Task GetBeatmapsAsync(int[] beatmapIds, CancellationToken token = default) => LookupAsync(beatmapIds, token);
+ public Task GetBeatmapsAsync(int[] beatmapIds, CancellationToken token = default) => LookupAsync(beatmapIds, token);
protected override GetBeatmapsRequest CreateRequest(IEnumerable ids) => new GetBeatmapsRequest(ids.ToArray());
- protected override IEnumerable RetrieveResults(GetBeatmapsRequest request) => request.Response?.Beatmaps;
+ protected override IEnumerable? RetrieveResults(GetBeatmapsRequest request) => request.Response?.Beatmaps;
}
}
diff --git a/osu.Game/Database/ISoftDelete.cs b/osu.Game/Database/ISoftDelete.cs
index b07c8db2de..afa42c2002 100644
--- a/osu.Game/Database/ISoftDelete.cs
+++ b/osu.Game/Database/ISoftDelete.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Database
{
///
diff --git a/osu.Game/Database/MemoryCachingComponent.cs b/osu.Game/Database/MemoryCachingComponent.cs
index 5d1a381f09..e98475efae 100644
--- a/osu.Game/Database/MemoryCachingComponent.cs
+++ b/osu.Game/Database/MemoryCachingComponent.cs
@@ -1,13 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Concurrent;
+using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
-using JetBrains.Annotations;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Statistics;
@@ -19,8 +17,9 @@ namespace osu.Game.Database
/// Currently not persisted between game sessions.
///
public abstract partial class MemoryCachingComponent : Component
+ where TLookup : notnull
{
- private readonly ConcurrentDictionary cache = new ConcurrentDictionary();
+ private readonly ConcurrentDictionary cache = new ConcurrentDictionary();
private readonly GlobalStatistic statistics;
@@ -37,12 +36,12 @@ namespace osu.Game.Database
///
/// The lookup to retrieve.
/// An optional to cancel the operation.
- protected async Task GetAsync([NotNull] TLookup lookup, CancellationToken token = default)
+ protected async Task GetAsync(TLookup lookup, CancellationToken token = default)
{
- if (CheckExists(lookup, out TValue performance))
+ if (CheckExists(lookup, out TValue? existing))
{
statistics.Value.HitCount++;
- return performance;
+ return existing;
}
var computed = await ComputeValueAsync(lookup, token).ConfigureAwait(false);
@@ -73,7 +72,7 @@ namespace osu.Game.Database
statistics.Value.Usage = cache.Count;
}
- protected bool CheckExists([NotNull] TLookup lookup, out TValue value) =>
+ protected bool CheckExists(TLookup lookup, [MaybeNullWhen(false)] out TValue value) =>
cache.TryGetValue(lookup, out value);
///
@@ -82,7 +81,7 @@ namespace osu.Game.Database
/// The lookup to retrieve.
/// An optional to cancel the operation.
/// The computed value.
- protected abstract Task ComputeValueAsync(TLookup lookup, CancellationToken token = default);
+ protected abstract Task ComputeValueAsync(TLookup lookup, CancellationToken token = default);
private class MemoryCachingStatistics
{
diff --git a/osu.Game/Database/OnlineLookupCache.cs b/osu.Game/Database/OnlineLookupCache.cs
index d9b37e2f29..3b54804fec 100644
--- a/osu.Game/Database/OnlineLookupCache.cs
+++ b/osu.Game/Database/OnlineLookupCache.cs
@@ -1,14 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Game.Online.API;
@@ -21,7 +18,7 @@ namespace osu.Game.Database
where TRequest : APIRequest
{
[Resolved]
- private IAPIProvider api { get; set; }
+ private IAPIProvider api { get; set; } = null!;
///
/// Creates an to retrieve the values for a given collection of s.
@@ -32,8 +29,7 @@ namespace osu.Game.Database
///
/// Retrieves a list of s from a successful created by .
///
- [CanBeNull]
- protected abstract IEnumerable RetrieveResults(TRequest request);
+ protected abstract IEnumerable? RetrieveResults(TRequest request);
///
/// Perform a lookup using the specified , populating a .
@@ -41,8 +37,7 @@ namespace osu.Game.Database
/// The ID to lookup.
/// An optional cancellation token.
/// The populated , or null if the value does not exist or the request could not be satisfied.
- [ItemCanBeNull]
- protected Task LookupAsync(TLookup id, CancellationToken token = default) => GetAsync(id, token);
+ protected Task LookupAsync(TLookup id, CancellationToken token = default) => GetAsync(id, token);
///
/// Perform an API lookup on the specified , populating a .
@@ -50,9 +45,9 @@ namespace osu.Game.Database
/// The IDs to lookup.
/// An optional cancellation token.
/// The populated values. May include null results for failed retrievals.
- protected Task LookupAsync(TLookup[] ids, CancellationToken token = default)
+ protected Task LookupAsync(TLookup[] ids, CancellationToken token = default)
{
- var lookupTasks = new List>();
+ var lookupTasks = new List>();
foreach (var id in ids)
{
@@ -69,18 +64,18 @@ namespace osu.Game.Database
}
// cannot be sealed due to test usages (see TestUserLookupCache).
- protected override async Task ComputeValueAsync(TLookup lookup, CancellationToken token = default)
+ protected override async Task ComputeValueAsync(TLookup lookup, CancellationToken token = default)
=> await queryValue(lookup).ConfigureAwait(false);
- private readonly Queue<(TLookup id, TaskCompletionSource)> pendingTasks = new Queue<(TLookup, TaskCompletionSource)>();
- private Task pendingRequestTask;
+ private readonly Queue<(TLookup id, TaskCompletionSource)> pendingTasks = new Queue<(TLookup, TaskCompletionSource)>();
+ private Task? pendingRequestTask;
private readonly object taskAssignmentLock = new object();
- private Task queryValue(TLookup id)
+ private Task queryValue(TLookup id)
{
lock (taskAssignmentLock)
{
- var tcs = new TaskCompletionSource();
+ var tcs = new TaskCompletionSource();
// Add to the queue.
pendingTasks.Enqueue((id, tcs));
@@ -96,14 +91,14 @@ namespace osu.Game.Database
private async Task performLookup()
{
// contains at most 50 unique IDs from tasks, which is used to perform the lookup.
- var nextTaskBatch = new Dictionary>>();
+ var nextTaskBatch = new Dictionary>>();
// Grab at most 50 unique IDs from the queue.
lock (taskAssignmentLock)
{
while (pendingTasks.Count > 0 && nextTaskBatch.Count < 50)
{
- (TLookup id, TaskCompletionSource task) next = pendingTasks.Dequeue();
+ (TLookup id, TaskCompletionSource task) next = pendingTasks.Dequeue();
// Perform a secondary check for existence, in case the value was queried in a previous batch.
if (CheckExists(next.id, out var existing))
@@ -113,7 +108,7 @@ namespace osu.Game.Database
if (nextTaskBatch.TryGetValue(next.id, out var tasks))
tasks.Add(next.task);
else
- nextTaskBatch[next.id] = new List> { next.task };
+ nextTaskBatch[next.id] = new List> { next.task };
}
}
}
diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs
index 94108531e8..68a4679656 100644
--- a/osu.Game/Database/RealmAccess.cs
+++ b/osu.Game/Database/RealmAccess.cs
@@ -22,12 +22,15 @@ using osu.Framework.Statistics;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
+using osu.Game.Extensions;
using osu.Game.Input.Bindings;
+using osu.Game.IO.Legacy;
using osu.Game.Models;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
+using osu.Game.Scoring.Legacy;
using osu.Game.Skinning;
using Realms;
using Realms.Exceptions;
@@ -71,8 +74,11 @@ namespace osu.Game.Database
/// 24 2022-08-22 Added MaximumStatistics to ScoreInfo.
/// 25 2022-09-18 Remove skins to add with new naming.
/// 26 2023-02-05 Added BeatmapHash to ScoreInfo.
+ /// 27 2023-06-06 Added EditorTimestamp to BeatmapInfo.
+ /// 28 2023-06-08 Added IsLegacyScore to ScoreInfo, parsed from replay files.
+ /// 29 2023-06-12 Run migration of old lazer scores to be best-effort in the new scoring number space. No actual realm changes.
///
- private const int schema_version = 26;
+ private const int schema_version = 29;
///
/// Lock object which is held during sections, blocking realm retrieval during blocking periods.
@@ -719,6 +725,11 @@ namespace osu.Game.Database
private void applyMigrationsForVersion(Migration migration, ulong targetVersion)
{
+ Logger.Log($"Running realm migration to version {targetVersion}...");
+ Stopwatch stopwatch = new Stopwatch();
+
+ stopwatch.Start();
+
switch (targetVersion)
{
case 7:
@@ -879,6 +890,7 @@ namespace osu.Game.Database
break;
case 26:
+ {
// Add ScoreInfo.BeatmapHash property to ensure scores correspond to the correct version of beatmap.
var scores = migration.NewRealm.All();
@@ -886,7 +898,76 @@ namespace osu.Game.Database
score.BeatmapHash = score.BeatmapInfo.Hash;
break;
+ }
+
+ case 28:
+ {
+ var files = new RealmFileStore(this, storage);
+ var scores = migration.NewRealm.All();
+
+ foreach (var score in scores)
+ {
+ string? replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(@".osr", StringComparison.InvariantCultureIgnoreCase))?.File.GetStoragePath();
+ if (replayFilename == null)
+ continue;
+
+ try
+ {
+ using (var stream = files.Store.GetStream(replayFilename))
+ {
+ if (stream == null)
+ continue;
+
+ // Trimmed down logic from LegacyScoreDecoder to extract the version from replays.
+ using (SerializationReader sr = new SerializationReader(stream))
+ {
+ sr.ReadByte(); // Ruleset.
+ int version = sr.ReadInt32();
+ if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION)
+ score.IsLegacyScore = true;
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, $"Failed to read replay {replayFilename} during score migration", LoggingTarget.Database);
+ }
+ }
+
+ break;
+ }
+
+ case 29:
+ {
+ var scores = migration.NewRealm
+ .All()
+ .Where(s => !s.IsLegacyScore);
+
+ foreach (var score in scores)
+ {
+ // Recalculate the old-style standardised score to see if this was an old lazer score.
+ bool oldScoreMatchesExpectations = StandardisedScoreMigrationTools.GetOldStandardised(score) == score.TotalScore;
+ // Some older scores don't have correct statistics populated, so let's give them benefit of doubt.
+ bool scoreIsVeryOld = score.Date < new DateTime(2023, 1, 1, 0, 0, 0);
+
+ if (oldScoreMatchesExpectations || scoreIsVeryOld)
+ {
+ try
+ {
+ long calculatedNew = StandardisedScoreMigrationTools.GetNewStandardised(score);
+ score.TotalScore = calculatedNew;
+ }
+ catch
+ {
+ }
+ }
+ }
+
+ break;
+ }
}
+
+ Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
}
private string? getRulesetShortNameFromLegacyID(long rulesetId)
diff --git a/osu.Game/Database/StandardisedScoreMigrationTools.cs b/osu.Game/Database/StandardisedScoreMigrationTools.cs
new file mode 100644
index 0000000000..af91bee9e4
--- /dev/null
+++ b/osu.Game/Database/StandardisedScoreMigrationTools.cs
@@ -0,0 +1,196 @@
+// 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.Game.Beatmaps;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+
+namespace osu.Game.Database
+{
+ public static class StandardisedScoreMigrationTools
+ {
+ public static long GetNewStandardised(ScoreInfo score)
+ {
+ int maxJudgementIndex = 0;
+
+ // Avoid retrieving from realm inside loops.
+ int maxCombo = score.MaxCombo;
+
+ var ruleset = score.Ruleset.CreateInstance();
+ var processor = ruleset.CreateScoreProcessor();
+
+ processor.TrackHitEvents = false;
+
+ var beatmap = new Beatmap();
+
+ HitResult maxRulesetJudgement = ruleset.GetHitResults().First().result;
+
+ // This is a list of all results, ordered from best to worst.
+ // We are constructing a "best possible" score from the statistics provided because it's the best we can do.
+ List sortedHits = score.Statistics
+ .Where(kvp => kvp.Key.AffectsCombo())
+ .OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key))
+ .SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value))
+ .ToList();
+
+ // Attempt to use maximum statistics from the database.
+ var maximumJudgements = score.MaximumStatistics
+ .Where(kvp => kvp.Key.AffectsCombo())
+ .OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key))
+ .SelectMany(kvp => Enumerable.Repeat(new FakeJudgement(kvp.Key), kvp.Value))
+ .ToList();
+
+ // Some older scores may not have maximum statistics populated correctly.
+ // In this case we need to fill them with best-known-defaults.
+ if (maximumJudgements.Count != sortedHits.Count)
+ {
+ maximumJudgements = sortedHits
+ .Select(r => new FakeJudgement(getMaxJudgementFor(r, maxRulesetJudgement)))
+ .ToList();
+ }
+
+ // This is required to get the correct maximum combo portion.
+ foreach (var judgement in maximumJudgements)
+ beatmap.HitObjects.Add(new FakeHit(judgement));
+ processor.ApplyBeatmap(beatmap);
+ processor.Mods.Value = score.Mods;
+
+ // Insert all misses into a queue.
+ // These will be nibbled at whenever we need to reset the combo.
+ Queue misses = new Queue(score.Statistics
+ .Where(kvp => kvp.Key == HitResult.Miss || kvp.Key == HitResult.LargeTickMiss)
+ .SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value)));
+
+ foreach (var result in sortedHits)
+ {
+ // For the main part of this loop, ignore all misses, as they will be inserted from the queue.
+ if (result == HitResult.Miss || result == HitResult.LargeTickMiss)
+ continue;
+
+ // Reset combo if required.
+ if (processor.Combo.Value == maxCombo)
+ insertMiss();
+
+ processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++])
+ {
+ Type = result
+ });
+ }
+
+ // Ensure we haven't forgotten any misses.
+ while (misses.Count > 0)
+ insertMiss();
+
+ var bonusHits = score.Statistics
+ .Where(kvp => kvp.Key.IsBonus())
+ .SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value));
+
+ foreach (var result in bonusHits)
+ processor.ApplyResult(new JudgementResult(null!, new FakeJudgement(result)) { Type = result });
+
+ // Not true for all scores for whatever reason. Oh well.
+ // Debug.Assert(processor.HighestCombo.Value == score.MaxCombo);
+
+ return processor.TotalScore.Value;
+
+ void insertMiss()
+ {
+ if (misses.Count > 0)
+ {
+ processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++])
+ {
+ Type = misses.Dequeue(),
+ });
+ }
+ else
+ {
+ // We ran out of misses. But we can't let max combo increase beyond the known value,
+ // so let's forge a miss.
+ processor.ApplyResult(new JudgementResult(null!, new FakeJudgement(getMaxJudgementFor(HitResult.Miss, maxRulesetJudgement)))
+ {
+ Type = HitResult.Miss,
+ });
+ }
+ }
+ }
+
+ private static HitResult getMaxJudgementFor(HitResult hitResult, HitResult max)
+ {
+ switch (hitResult)
+ {
+ case HitResult.Miss:
+ case HitResult.Meh:
+ case HitResult.Ok:
+ case HitResult.Good:
+ case HitResult.Great:
+ case HitResult.Perfect:
+ return max;
+
+ case HitResult.SmallTickMiss:
+ case HitResult.SmallTickHit:
+ return HitResult.SmallTickHit;
+
+ case HitResult.LargeTickMiss:
+ case HitResult.LargeTickHit:
+ return HitResult.LargeTickHit;
+ }
+
+ return HitResult.IgnoreHit;
+ }
+
+ public static long GetOldStandardised(ScoreInfo score)
+ {
+ double accuracyScore =
+ (double)score.Statistics.Where(kvp => kvp.Key.AffectsAccuracy()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value)
+ / score.MaximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value);
+ double comboScore = (double)score.MaxCombo / score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value);
+ double bonusScore = score.Statistics.Where(kvp => kvp.Key.IsBonus()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value);
+
+ double accuracyPortion = 0.3;
+
+ switch (score.RulesetID)
+ {
+ case 1:
+ accuracyPortion = 0.75;
+ break;
+
+ case 3:
+ accuracyPortion = 0.99;
+ break;
+ }
+
+ double modMultiplier = 1;
+
+ foreach (var mod in score.Mods)
+ modMultiplier *= mod.ScoreMultiplier;
+
+ return (long)((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier);
+ }
+
+ private class FakeHit : HitObject
+ {
+ private readonly Judgement judgement;
+
+ public override Judgement CreateJudgement() => judgement;
+
+ public FakeHit(Judgement judgement)
+ {
+ this.judgement = judgement;
+ }
+ }
+
+ private class FakeJudgement : Judgement
+ {
+ public override HitResult MaxResult { get; }
+
+ public FakeJudgement(HitResult maxResult)
+ {
+ MaxResult = maxResult;
+ }
+ }
+ }
+}
diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs
index b1609fbf7b..e581d5ce82 100644
--- a/osu.Game/Database/UserLookupCache.cs
+++ b/osu.Game/Database/UserLookupCache.cs
@@ -1,13 +1,10 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
-using JetBrains.Annotations;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@@ -21,8 +18,7 @@ namespace osu.Game.Database
/// The user to lookup.
/// An optional cancellation token.
/// The populated user, or null if the user does not exist or the request could not be satisfied.
- [ItemCanBeNull]
- public Task GetUserAsync(int userId, CancellationToken token = default) => LookupAsync(userId, token);
+ public Task GetUserAsync(int userId, CancellationToken token = default) => LookupAsync(userId, token);
///
/// Perform an API lookup on the specified users, populating a model.
@@ -30,10 +26,10 @@ namespace osu.Game.Database
/// The users to lookup.
/// An optional cancellation token.
/// The populated users. May include null results for failed retrievals.
- public Task GetUsersAsync(int[] userIds, CancellationToken token = default) => LookupAsync(userIds, token);
+ public Task GetUsersAsync(int[] userIds, CancellationToken token = default) => LookupAsync(userIds, token);
protected override GetUsersRequest CreateRequest(IEnumerable ids) => new GetUsersRequest(ids.ToArray());
- protected override IEnumerable RetrieveResults(GetUsersRequest request) => request.Response?.Users;
+ protected override IEnumerable? RetrieveResults(GetUsersRequest request) => request.Response?.Users;
}
}
diff --git a/osu.Game/Extensions/DrawableExtensions.cs b/osu.Game/Extensions/DrawableExtensions.cs
index 915a2292a2..6553ad3886 100644
--- a/osu.Game/Extensions/DrawableExtensions.cs
+++ b/osu.Game/Extensions/DrawableExtensions.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
+using osu.Framework.Platform;
using osuTK;
namespace osu.Game.Extensions
@@ -43,5 +44,20 @@ namespace osu.Game.Extensions
/// The delta vector in Parent's coordinates.
public static Vector2 ScreenSpaceDeltaToParentSpace(this Drawable drawable, Vector2 delta) =>
drawable.Parent.ToLocalSpace(drawable.Parent.ToScreenSpace(Vector2.Zero) + delta);
+
+ ///
+ /// Some elements don't handle rewind correctly and fixing them is non-trivial.
+ /// In the future we need a better solution to this, but as a temporary work-around, give these components the game-wide
+ /// clock so they don't need to worry about rewind.
+ ///
+ /// This only works if input handling components handle OnPressed/OnReleased which results in a correct state while rewinding.
+ ///
+ /// This is kinda dodgy (and will cause weirdness when pausing gameplay) but is better than completely broken rewind.
+ ///
+ public static void ApplyGameWideClock(this Drawable drawable, GameHost host)
+ {
+ drawable.Clock = host.UpdateThread.Clock;
+ drawable.ProcessCustomClock = false;
+ }
}
}
diff --git a/osu.Game/Extensions/LanguageExtensions.cs b/osu.Game/Extensions/LanguageExtensions.cs
index 04231c384c..44932cf3c8 100644
--- a/osu.Game/Extensions/LanguageExtensions.cs
+++ b/osu.Game/Extensions/LanguageExtensions.cs
@@ -21,7 +21,12 @@ namespace osu.Game.Extensions
/// This is required as enum member names are not allowed to contain hyphens.
///
public static string ToCultureCode(this Language language)
- => language.ToString().Replace("_", "-");
+ {
+ if (language == Language.zh_hant)
+ return @"zh-tw";
+
+ return language.ToString().Replace("_", "-");
+ }
///
/// Attempts to parse the supplied to a value.
@@ -30,7 +35,15 @@ namespace osu.Game.Extensions
/// The parsed . Valid only if the return value of the method is .
/// Whether the parsing succeeded.
public static bool TryParseCultureCode(string cultureCode, out Language language)
- => Enum.TryParse(cultureCode.Replace("-", "_"), out language);
+ {
+ if (cultureCode == @"zh-tw")
+ {
+ language = Language.zh_hant;
+ return true;
+ }
+
+ return Enum.TryParse(cultureCode.Replace("-", "_"), out language);
+ }
///
/// Parses the that is specified in ,
diff --git a/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs b/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs
index b79eb4927f..3ace67f410 100644
--- a/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs
+++ b/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Graphics.Backgrounds
[BackgroundDependencyLoader]
private void load(LargeTextureStore textures)
{
- Sprite.Texture = Beatmap?.Background ?? textures.Get(fallbackTextureName);
+ Sprite.Texture = Beatmap?.GetBackground() ?? textures.Get(fallbackTextureName);
}
public override bool Equals(Background other)
diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs
index 8dd6eac7bb..27ff6b851d 100644
--- a/osu.Game/Graphics/Containers/SectionsContainer.cs
+++ b/osu.Game/Graphics/Containers/SectionsContainer.cs
@@ -1,17 +1,16 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Diagnostics;
using System.Linq;
-using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Layout;
+using osu.Framework.Logging;
+using osu.Framework.Threading;
using osu.Framework.Utils;
namespace osu.Game.Graphics.Containers
@@ -23,11 +22,35 @@ namespace osu.Game.Graphics.Containers
public partial class SectionsContainer : Container
where T : Drawable
{
- public Bindable SelectedSection { get; } = new Bindable();
+ public Bindable SelectedSection { get; } = new Bindable();
- private T lastClickedSection;
+ private T? lastClickedSection;
- public Drawable ExpandableHeader
+ protected override Container Content => scrollContentContainer;
+
+ private readonly UserTrackingScrollContainer scrollContainer;
+ private readonly Container headerBackgroundContainer;
+ private readonly MarginPadding originalSectionsMargin;
+
+ private Drawable? fixedHeader;
+
+ private Drawable? footer;
+ private Drawable? headerBackground;
+
+ private FlowContainer scrollContentContainer = null!;
+
+ private float? headerHeight, footerHeight;
+
+ private float? lastKnownScroll;
+
+ ///
+ /// The percentage of the container to consider the centre-point for deciding the active section (and scrolling to a requested section).
+ ///
+ private const float scroll_y_centre = 0.1f;
+
+ private Drawable? expandableHeader;
+
+ public Drawable? ExpandableHeader
{
get => expandableHeader;
set
@@ -42,11 +65,12 @@ namespace osu.Game.Graphics.Containers
if (value == null) return;
AddInternal(expandableHeader);
+
lastKnownScroll = null;
}
}
- public Drawable FixedHeader
+ public Drawable? FixedHeader
{
get => fixedHeader;
set
@@ -63,7 +87,7 @@ namespace osu.Game.Graphics.Containers
}
}
- public Drawable Footer
+ public Drawable? Footer
{
get => footer;
set
@@ -75,16 +99,17 @@ namespace osu.Game.Graphics.Containers
footer = value;
- if (value == null) return;
+ if (footer == null) return;
footer.Anchor |= Anchor.y2;
footer.Origin |= Anchor.y2;
+
scrollContainer.Add(footer);
lastKnownScroll = null;
}
}
- public Drawable HeaderBackground
+ public Drawable? HeaderBackground
{
get => headerBackground;
set
@@ -102,23 +127,6 @@ namespace osu.Game.Graphics.Containers
}
}
- protected override Container Content => scrollContentContainer;
-
- private readonly UserTrackingScrollContainer scrollContainer;
- private readonly Container headerBackgroundContainer;
- private readonly MarginPadding originalSectionsMargin;
- private Drawable expandableHeader, fixedHeader, footer, headerBackground;
- private FlowContainer scrollContentContainer;
-
- private float? headerHeight, footerHeight;
-
- private float? lastKnownScroll;
-
- ///
- /// The percentage of the container to consider the centre-point for deciding the active section (and scrolling to a requested section).
- ///
- private const float scroll_y_centre = 0.1f;
-
public SectionsContainer()
{
AddRangeInternal(new Drawable[]
@@ -150,31 +158,63 @@ namespace osu.Game.Graphics.Containers
footerHeight = null;
}
+ private ScheduledDelegate? scrollToTargetDelegate;
+
public void ScrollTo(Drawable target)
{
+ Logger.Log($"Scrolling to {target}..");
+
lastKnownScroll = null;
- // implementation similar to ScrollIntoView but a bit more nuanced.
- float top = scrollContainer.GetChildPosInContent(target);
+ float scrollTarget = getScrollTargetForDrawable(target);
- float bottomScrollExtent = scrollContainer.ScrollableExtent;
- float scrollTarget = top - scrollContainer.DisplayableContent * scroll_y_centre;
-
- if (scrollTarget > bottomScrollExtent)
+ if (scrollTarget > scrollContainer.ScrollableExtent)
scrollContainer.ScrollToEnd();
else
scrollContainer.ScrollTo(scrollTarget);
if (target is T section)
lastClickedSection = section;
+
+ // Content may load in as a scroll occurs, changing the scroll target we need to aim for.
+ // This scheduled operation ensures that we keep trying until actually arriving at the target.
+ scrollToTargetDelegate?.Cancel();
+ scrollToTargetDelegate = Scheduler.AddDelayed(() =>
+ {
+ if (scrollContainer.UserScrolling)
+ {
+ Logger.Log("Scroll operation interrupted by user scroll");
+ scrollToTargetDelegate?.Cancel();
+ scrollToTargetDelegate = null;
+ return;
+ }
+
+ if (Precision.AlmostEquals(scrollContainer.Current, scrollTarget, 1))
+ {
+ Logger.Log($"Finished scrolling to {target}!");
+ scrollToTargetDelegate?.Cancel();
+ scrollToTargetDelegate = null;
+ return;
+ }
+
+ if (!Precision.AlmostEquals(getScrollTargetForDrawable(target), scrollTarget, 1))
+ {
+ Logger.Log($"Reattempting scroll to {target} due to change in position");
+ ScrollTo(target);
+ }
+ }, 50, true);
+ }
+
+ private float getScrollTargetForDrawable(Drawable target)
+ {
+ // implementation similar to ScrollIntoView but a bit more nuanced.
+ return scrollContainer.GetChildPosInContent(target) - scrollContainer.DisplayableContent * scroll_y_centre;
}
public void ScrollToTop() => scrollContainer.ScrollTo(0);
- [NotNull]
protected virtual UserTrackingScrollContainer CreateScrollContainer() => new UserTrackingScrollContainer();
- [NotNull]
protected virtual FlowContainer CreateScrollContentContainer() =>
new FillFlowContainer
{
diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs
index d799e82bc9..82f89d6889 100644
--- a/osu.Game/Graphics/ScreenshotManager.cs
+++ b/osu.Game/Graphics/ScreenshotManager.cs
@@ -19,6 +19,7 @@ using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.Configuration;
using osu.Game.Input.Bindings;
+using osu.Game.Online.Multiplayer;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using SixLabors.ImageSharp;
@@ -69,7 +70,7 @@ namespace osu.Game.Graphics
{
case GlobalAction.TakeScreenshot:
shutter.Play();
- TakeScreenshotAsync();
+ TakeScreenshotAsync().FireAndForget();
return true;
}
@@ -86,70 +87,75 @@ namespace osu.Game.Graphics
{
Interlocked.Increment(ref screenShotTasks);
- if (!captureMenuCursor.Value)
+ try
{
- cursorVisibility.Value = false;
-
- // We need to wait for at most 3 draw nodes to be drawn, following which we can be assured at least one DrawNode has been generated/drawn with the set value
- const int frames_to_wait = 3;
-
- int framesWaited = 0;
-
- using (var framesWaitedEvent = new ManualResetEventSlim(false))
+ if (!captureMenuCursor.Value)
{
- ScheduledDelegate waitDelegate = host.DrawThread.Scheduler.AddDelayed(() =>
+ cursorVisibility.Value = false;
+
+ // We need to wait for at most 3 draw nodes to be drawn, following which we can be assured at least one DrawNode has been generated/drawn with the set value
+ const int frames_to_wait = 3;
+
+ int framesWaited = 0;
+
+ using (var framesWaitedEvent = new ManualResetEventSlim(false))
{
- if (framesWaited++ >= frames_to_wait)
- // ReSharper disable once AccessToDisposedClosure
- framesWaitedEvent.Set();
- }, 10, true);
+ ScheduledDelegate waitDelegate = host.DrawThread.Scheduler.AddDelayed(() =>
+ {
+ if (framesWaited++ >= frames_to_wait)
+ // ReSharper disable once AccessToDisposedClosure
+ framesWaitedEvent.Set();
+ }, 10, true);
- if (!framesWaitedEvent.Wait(1000))
- throw new TimeoutException("Screenshot data did not arrive in a timely fashion");
+ if (!framesWaitedEvent.Wait(1000))
+ throw new TimeoutException("Screenshot data did not arrive in a timely fashion");
- waitDelegate.Cancel();
+ waitDelegate.Cancel();
+ }
+ }
+
+ using (var image = await host.TakeScreenshotAsync().ConfigureAwait(false))
+ {
+ host.GetClipboard()?.SetImage(image);
+
+ (string filename, var stream) = getWritableStream();
+
+ if (filename == null) return;
+
+ using (stream)
+ {
+ switch (screenshotFormat.Value)
+ {
+ case ScreenshotFormat.Png:
+ await image.SaveAsPngAsync(stream).ConfigureAwait(false);
+ break;
+
+ case ScreenshotFormat.Jpg:
+ const int jpeg_quality = 92;
+
+ await image.SaveAsJpegAsync(stream, new JpegEncoder { Quality = jpeg_quality }).ConfigureAwait(false);
+ break;
+
+ default:
+ throw new InvalidOperationException($"Unknown enum member {nameof(ScreenshotFormat)} {screenshotFormat.Value}.");
+ }
+ }
+
+ notificationOverlay.Post(new SimpleNotification
+ {
+ Text = $"Screenshot {filename} saved!",
+ Activated = () =>
+ {
+ storage.PresentFileExternally(filename);
+ return true;
+ }
+ });
}
}
-
- using (var image = await host.TakeScreenshotAsync().ConfigureAwait(false))
+ finally
{
- if (Interlocked.Decrement(ref screenShotTasks) == 0 && cursorVisibility.Value == false)
+ if (Interlocked.Decrement(ref screenShotTasks) == 0)
cursorVisibility.Value = true;
-
- host.GetClipboard()?.SetImage(image);
-
- (string filename, var stream) = getWritableStream();
-
- if (filename == null) return;
-
- using (stream)
- {
- switch (screenshotFormat.Value)
- {
- case ScreenshotFormat.Png:
- await image.SaveAsPngAsync(stream).ConfigureAwait(false);
- break;
-
- case ScreenshotFormat.Jpg:
- const int jpeg_quality = 92;
-
- await image.SaveAsJpegAsync(stream, new JpegEncoder { Quality = jpeg_quality }).ConfigureAwait(false);
- break;
-
- default:
- throw new InvalidOperationException($"Unknown enum member {nameof(ScreenshotFormat)} {screenshotFormat.Value}.");
- }
- }
-
- notificationOverlay.Post(new SimpleNotification
- {
- Text = $"Screenshot {filename} saved!",
- Activated = () =>
- {
- storage.PresentFileExternally(filename);
- return true;
- }
- });
}
});
diff --git a/osu.Game/Graphics/Sprites/OsuSpriteText.cs b/osu.Game/Graphics/Sprites/OsuSpriteText.cs
index e149e0abfb..afbec0eab4 100644
--- a/osu.Game/Graphics/Sprites/OsuSpriteText.cs
+++ b/osu.Game/Graphics/Sprites/OsuSpriteText.cs
@@ -3,12 +3,19 @@
#nullable disable
+using System;
using osu.Framework.Graphics.Sprites;
namespace osu.Game.Graphics.Sprites
{
public partial class OsuSpriteText : SpriteText
{
+ [Obsolete("Use TruncatingSpriteText instead.")]
+ public new bool Truncate
+ {
+ set => throw new InvalidOperationException($"Use {nameof(TruncatingSpriteText)} instead.");
+ }
+
public OsuSpriteText()
{
Shadow = true;
diff --git a/osu.Game/Graphics/Sprites/TruncatingSpriteText.cs b/osu.Game/Graphics/Sprites/TruncatingSpriteText.cs
new file mode 100644
index 0000000000..46abdbf09e
--- /dev/null
+++ b/osu.Game/Graphics/Sprites/TruncatingSpriteText.cs
@@ -0,0 +1,29 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Localisation;
+
+namespace osu.Game.Graphics.Sprites
+{
+ ///
+ /// A derived version of which automatically shows non-truncated text in tooltip when required.
+ ///
+ public sealed partial class TruncatingSpriteText : OsuSpriteText, IHasTooltip
+ {
+ ///
+ /// Whether a tooltip should be shown with non-truncated text on hover.
+ ///
+ public bool ShowTooltip { get; init; } = true;
+
+ public LocalisableString TooltipText => Text;
+
+ public override bool HandlePositionalInput => IsTruncated && ShowTooltip;
+
+ public TruncatingSpriteText()
+ {
+ ((SpriteText)this).Truncate = true;
+ }
+ }
+}
diff --git a/osu.Game/Graphics/UserInterface/MenuItemType.cs b/osu.Game/Graphics/UserInterface/MenuItemType.cs
index 1eb45d6b1c..0269f2cb57 100644
--- a/osu.Game/Graphics/UserInterface/MenuItemType.cs
+++ b/osu.Game/Graphics/UserInterface/MenuItemType.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Graphics.UserInterface
{
public enum MenuItemType
diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs
index 3230bb0569..b530172f3e 100644
--- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs
+++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs
@@ -335,12 +335,11 @@ namespace osu.Game.Graphics.UserInterface
{
new Drawable[]
{
- Text = new OsuSpriteText
+ Text = new TruncatingSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
- Truncate = true,
},
Icon = new SpriteIcon
{
diff --git a/osu.Game/Graphics/UserInterface/SelectionState.cs b/osu.Game/Graphics/UserInterface/SelectionState.cs
index edabf0547b..c85b2ad3ab 100644
--- a/osu.Game/Graphics/UserInterface/SelectionState.cs
+++ b/osu.Game/Graphics/UserInterface/SelectionState.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Graphics.UserInterface
{
public enum SelectionState
diff --git a/osu.Game/Graphics/UserInterface/TernaryState.cs b/osu.Game/Graphics/UserInterface/TernaryState.cs
index effbe624c3..d4de28044f 100644
--- a/osu.Game/Graphics/UserInterface/TernaryState.cs
+++ b/osu.Game/Graphics/UserInterface/TernaryState.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Graphics.UserInterface
{
///
diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs
index d580eea248..fdd96d3890 100644
--- a/osu.Game/Input/Bindings/GlobalActionContainer.cs
+++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs
@@ -101,6 +101,10 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.J }, GlobalAction.EditorFlipVertically),
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelDown }, GlobalAction.EditorDecreaseDistanceSpacing),
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.EditorIncreaseDistanceSpacing),
+ // Framework automatically converts wheel up/down to left/right when shift is held.
+ // See https://github.com/ppy/osu-framework/blob/master/osu.Framework/Input/StateChanges/MouseScrollRelativeInput.cs#L37-L38.
+ new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor),
+ new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor),
};
public IEnumerable InGameKeyBindings => new[]
@@ -355,6 +359,12 @@ namespace osu.Game.Input.Bindings
ToggleProfile,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCloneSelection))]
- EditorCloneSelection
+ EditorCloneSelection,
+
+ [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCyclePreviousBeatSnapDivisor))]
+ EditorCyclePreviousBeatSnapDivisor,
+
+ [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCycleNextBeatSnapDivisor))]
+ EditorCycleNextBeatSnapDivisor,
}
}
diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
index 303dbb6f46..aa608a603b 100644
--- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
+++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
@@ -279,6 +279,16 @@ namespace osu.Game.Localisation
///
public static LocalisableString EditorDecreaseDistanceSpacing => new TranslatableString(getKey(@"editor_decrease_distance_spacing"), @"Decrease distance spacing");
+ ///
+ /// "Cycle previous beat snap divisor"
+ ///
+ public static LocalisableString EditorCyclePreviousBeatSnapDivisor => new TranslatableString(getKey(@"editor_cycle_previous_beat_snap_divisor"), @"Cycle previous beat snap divisor");
+
+ ///
+ /// "Cycle next beat snap divisor"
+ ///
+ public static LocalisableString EditorCycleNextBeatSnapDivisor => new TranslatableString(getKey(@"editor_cycle_next_snap_divisor"), @"Cycle next beat snap divisor");
+
///
/// "Toggle skin editor"
///
diff --git a/osu.Game/Localisation/Language.cs b/osu.Game/Localisation/Language.cs
index 6a4e5110e6..711e95486f 100644
--- a/osu.Game/Localisation/Language.cs
+++ b/osu.Game/Localisation/Language.cs
@@ -22,6 +22,9 @@ namespace osu.Game.Localisation
[Description(@"Български")]
bg,
+ [Description(@"Català")]
+ ca,
+
[Description(@"Česky")]
cs,
@@ -37,12 +40,27 @@ namespace osu.Game.Localisation
[Description(@"español")]
es,
+ // TODO: Requires Arabic glyphs to be added to resources (and possibly also RTL support).
+ // [Description(@"فارسی")]
+ // fa_ir,
+
[Description(@"Suomi")]
fi,
+ // TODO: Doesn't work as appropriate satellite assemblies aren't copied from resources (see: https://github.com/ppy/osu/discussions/18851#discussioncomment-3042170)
+ // [Description(@"Filipino")]
+ // fil,
+
[Description(@"français")]
fr,
+ // TODO: Requires Hebrew glyphs to be added to resources (and possibly also RTL support).
+ // [Description(@"עברית")]
+ // he,
+
+ [Description(@"Hrvatski")]
+ hr_hr,
+
[Description(@"Magyar")]
hu,
@@ -58,6 +76,15 @@ namespace osu.Game.Localisation
[Description(@"한국어")]
ko,
+ [Description(@"Lietuvių")]
+ lt,
+
+ [Description(@"Latviešu")]
+ lv_lv,
+
+ [Description(@"Melayu")]
+ ms_my,
+
[Description(@"Nederlands")]
nl,
@@ -79,12 +106,28 @@ namespace osu.Game.Localisation
[Description(@"Русский")]
ru,
+ // TODO: Requires Sinhala glyphs to be added to resources.
+ // Additionally, no translations available yet.
+ // [Description(@"සිංහල")]
+ // si_lk,
+
[Description(@"Slovenčina")]
sk,
+ [Description(@"Slovenščina")]
+ sl,
+
+ [Description(@"Српски")]
+ sr,
+
[Description(@"Svenska")]
sv,
+ // Tajik has no associated localisations yet, and is not supported on Windows versions <10.
+ // TODO: update language mapping in osu-resources to redirect tg-TJ to tg-Cyrl-TJ (which is supported on earlier Windows versions)
+ // [Description(@"Тоҷикӣ")]
+ // tg_tj,
+
[Description(@"ไทย")]
th,
diff --git a/osu.Game/Localisation/RulesetSettingsStrings.cs b/osu.Game/Localisation/RulesetSettingsStrings.cs
index 52e6a5eaac..3fa7656cbb 100644
--- a/osu.Game/Localisation/RulesetSettingsStrings.cs
+++ b/osu.Game/Localisation/RulesetSettingsStrings.cs
@@ -82,7 +82,7 @@ namespace osu.Game.Localisation
///
/// "{0}ms (speed {1})"
///
- public static LocalisableString ScrollSpeedTooltip(double arg0, int arg1) => new TranslatableString(getKey(@"ruleset"), @"{0}ms (speed {1})", arg0, arg1);
+ public static LocalisableString ScrollSpeedTooltip(int scrollTime, int scrollSpeed) => new TranslatableString(getKey(@"ruleset"), @"{0}ms (speed {1})", scrollTime, scrollSpeed);
private static string getKey(string key) => $@"{prefix}:{key}";
}
diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs
index 94bb77d6ec..4f586c8fff 100644
--- a/osu.Game/Online/API/APIAccess.cs
+++ b/osu.Game/Online/API/APIAccess.cs
@@ -18,6 +18,7 @@ using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Configuration;
+using osu.Game.Localisation;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Notifications;
@@ -28,6 +29,7 @@ namespace osu.Game.Online.API
{
public partial class APIAccess : Component, IAPIProvider
{
+ private readonly OsuGameBase game;
private readonly OsuConfigManager config;
private readonly string versionHash;
@@ -52,6 +54,8 @@ namespace osu.Game.Online.API
public IBindableList Friends => friends;
public IBindable Activity => activity;
+ public Language Language => game.CurrentLanguage.Value;
+
private Bindable localUser { get; } = new Bindable(createGuestUser());
private BindableList friends { get; } = new BindableList();
@@ -64,8 +68,9 @@ namespace osu.Game.Online.API
private readonly Logger log;
- public APIAccess(OsuConfigManager config, EndpointConfiguration endpointConfiguration, string versionHash)
+ public APIAccess(OsuGameBase game, OsuConfigManager config, EndpointConfiguration endpointConfiguration, string versionHash)
{
+ this.game = game;
this.config = config;
this.versionHash = versionHash;
diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs
index dc6a3fe3d5..cd6e8df754 100644
--- a/osu.Game/Online/API/APIRequest.cs
+++ b/osu.Game/Online/API/APIRequest.cs
@@ -9,6 +9,7 @@ using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Framework.IO.Network;
using osu.Framework.Logging;
+using osu.Game.Extensions;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API
@@ -116,10 +117,11 @@ namespace osu.Game.Online.API
WebRequest.Failed += Fail;
WebRequest.AllowRetryOnTimeout = false;
- WebRequest.AddHeader("x-api-version", API.APIVersion.ToString(CultureInfo.InvariantCulture));
+ WebRequest.AddHeader(@"Accept-Language", API.Language.ToCultureCode());
+ WebRequest.AddHeader(@"x-api-version", API.APIVersion.ToString(CultureInfo.InvariantCulture));
if (!string.IsNullOrEmpty(API.AccessToken))
- WebRequest.AddHeader("Authorization", $"Bearer {API.AccessToken}");
+ WebRequest.AddHeader(@"Authorization", $@"Bearer {API.AccessToken}");
if (isFailing) return;
diff --git a/osu.Game/Online/API/APIRequestCompletionState.cs b/osu.Game/Online/API/APIRequestCompletionState.cs
index 52eb669a7d..84c9974dd8 100644
--- a/osu.Game/Online/API/APIRequestCompletionState.cs
+++ b/osu.Game/Online/API/APIRequestCompletionState.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Online.API
{
public enum APIRequestCompletionState
diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs
index abe2755654..16afef8e30 100644
--- a/osu.Game/Online/API/DummyAPIAccess.cs
+++ b/osu.Game/Online/API/DummyAPIAccess.cs
@@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Game.Localisation;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Notifications;
using osu.Game.Tests;
@@ -29,6 +30,8 @@ namespace osu.Game.Online.API
public Bindable Activity { get; } = new Bindable();
+ public Language Language => Language.en;
+
public string AccessToken => "token";
public bool IsLoggedIn => State.Value == APIState.Online;
diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs
index 6054effaa1..a1d7006c8c 100644
--- a/osu.Game/Online/API/IAPIProvider.cs
+++ b/osu.Game/Online/API/IAPIProvider.cs
@@ -4,6 +4,7 @@
using System;
using System.Threading.Tasks;
using osu.Framework.Bindables;
+using osu.Game.Localisation;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Notifications;
using osu.Game.Users;
@@ -27,6 +28,11 @@ namespace osu.Game.Online.API
///
IBindable Activity { get; }
+ ///
+ /// The language supplied by this provider to API requests.
+ ///
+ Language Language { get; }
+
///
/// Retrieve the OAuth access token.
///
diff --git a/osu.Game/Online/API/Requests/GetUsersRequest.cs b/osu.Game/Online/API/Requests/GetUsersRequest.cs
index b57bb215aa..6f7e9c07d2 100644
--- a/osu.Game/Online/API/Requests/GetUsersRequest.cs
+++ b/osu.Game/Online/API/Requests/GetUsersRequest.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
namespace osu.Game.Online.API.Requests
diff --git a/osu.Game/Online/Chat/ChannelType.cs b/osu.Game/Online/Chat/ChannelType.cs
index a864e20830..bd628e90c4 100644
--- a/osu.Game/Online/Chat/ChannelType.cs
+++ b/osu.Game/Online/Chat/ChannelType.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Online.Chat
{
public enum ChannelType
diff --git a/osu.Game/Online/Leaderboards/LeaderboardState.cs b/osu.Game/Online/Leaderboards/LeaderboardState.cs
index abc0ef4f19..6b07500a98 100644
--- a/osu.Game/Online/Leaderboards/LeaderboardState.cs
+++ b/osu.Game/Online/Leaderboards/LeaderboardState.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Online.Leaderboards
{
public enum LeaderboardState
diff --git a/osu.Game/Online/Multiplayer/ForceGameplayStartCountdown.cs b/osu.Game/Online/Multiplayer/ForceGameplayStartCountdown.cs
index bbfc5a02c6..c497601e37 100644
--- a/osu.Game/Online/Multiplayer/ForceGameplayStartCountdown.cs
+++ b/osu.Game/Online/Multiplayer/ForceGameplayStartCountdown.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using MessagePack;
namespace osu.Game.Online.Multiplayer
diff --git a/osu.Game/Online/Multiplayer/IMultiplayerServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerServer.cs
index cc7a474ce7..d3a070af6d 100644
--- a/osu.Game/Online/Multiplayer/IMultiplayerServer.cs
+++ b/osu.Game/Online/Multiplayer/IMultiplayerServer.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Online.Multiplayer
{
///
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 2be7327234..5716b7ad3b 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -783,7 +783,7 @@ namespace osu.Game.Online.Multiplayer
RoomUpdated?.Invoke();
}
- private PlaylistItem createPlaylistItem(MultiplayerPlaylistItem item) => new PlaylistItem(new APIBeatmap { OnlineID = item.BeatmapID })
+ private PlaylistItem createPlaylistItem(MultiplayerPlaylistItem item) => new PlaylistItem(new APIBeatmap { OnlineID = item.BeatmapID, StarRating = item.StarRating })
{
ID = item.ID,
OwnerID = item.OwnerID,
diff --git a/osu.Game/Online/Multiplayer/MultiplayerUserState.cs b/osu.Game/Online/Multiplayer/MultiplayerUserState.cs
index 0f7dc6b8cd..d1369a7970 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerUserState.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerUserState.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Online.Multiplayer
{
public enum MultiplayerUserState
diff --git a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs
index c45f703b05..8be703e620 100644
--- a/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs
+++ b/osu.Game/Online/Rooms/MultiplayerPlaylistItem.cs
@@ -53,22 +53,12 @@ namespace osu.Game.Online.Rooms
[Key(9)]
public DateTimeOffset? PlayedAt { get; set; }
+ [Key(10)]
+ public double StarRating { get; set; }
+
+ [SerializationConstructor]
public MultiplayerPlaylistItem()
{
}
-
- public MultiplayerPlaylistItem(PlaylistItem item)
- {
- ID = item.ID;
- OwnerID = item.OwnerID;
- BeatmapID = item.Beatmap.OnlineID;
- BeatmapChecksum = item.Beatmap.MD5Hash;
- RulesetID = item.RulesetID;
- RequiredMods = item.RequiredMods.ToArray();
- AllowedMods = item.AllowedMods.ToArray();
- Expired = item.Expired;
- PlaylistOrder = item.PlaylistOrder ?? 0;
- PlayedAt = item.PlayedAt;
- }
}
}
diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs
index 1d496cc636..cce633d46a 100644
--- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs
+++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailabilityTracker.cs
@@ -67,7 +67,7 @@ namespace osu.Game.Online.Rooms
{
var beatmap = task.GetResultSafely();
- if (SelectedItem.Value?.Beatmap.OnlineID == beatmap.OnlineID)
+ if (beatmap != null && SelectedItem.Value?.Beatmap.OnlineID == beatmap.OnlineID)
{
selectedBeatmap = beatmap;
beginTracking();
diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs
index 2213311c67..a900d8f3d7 100644
--- a/osu.Game/Online/Rooms/PlaylistItem.cs
+++ b/osu.Game/Online/Rooms/PlaylistItem.cs
@@ -91,7 +91,7 @@ namespace osu.Game.Online.Rooms
}
public PlaylistItem(MultiplayerPlaylistItem item)
- : this(new APIBeatmap { OnlineID = item.BeatmapID })
+ : this(new APIBeatmap { OnlineID = item.BeatmapID, StarRating = item.StarRating })
{
ID = item.ID;
OwnerID = item.OwnerID;
diff --git a/osu.Game/Online/Spectator/SpectatedUserState.cs b/osu.Game/Online/Spectator/SpectatedUserState.cs
index edf0859a33..0f0a3068b8 100644
--- a/osu.Game/Online/Spectator/SpectatedUserState.cs
+++ b/osu.Game/Online/Spectator/SpectatedUserState.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Online.Spectator
{
public enum SpectatedUserState
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index fe6e479d19..3768dad370 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -1136,12 +1136,22 @@ namespace osu.Game
if (entry.Level == LogLevel.Error)
{
- Schedule(() => Notifications.Post(new SimpleNotification
+ Schedule(() =>
{
- Text = $"Encountered tablet error: \"{message}\"",
- Icon = FontAwesome.Solid.PenSquare,
- IconColour = Colours.RedDark,
- }));
+ Notifications.Post(new SimpleNotification
+ {
+ Text = $"Disabling tablet support due to error: \"{message}\"",
+ Icon = FontAwesome.Solid.PenSquare,
+ IconColour = Colours.RedDark,
+ });
+
+ // We only have one tablet handler currently.
+ // The loop here is weakly guarding against a future where more than one is added.
+ // If this is ever the case, this logic needs adjustment as it should probably only
+ // disable the relevant tablet handler rather than all.
+ foreach (var tabletHandler in Host.AvailableInputHandlers.OfType())
+ tabletHandler.Enabled.Value = false;
+ });
}
else if (notifyOnWarning)
{
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index c55b6c249f..63efe0e2c8 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -14,6 +14,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
+using osu.Framework.Configuration;
using osu.Framework.Development;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
@@ -27,6 +28,7 @@ using osu.Framework.Input.Handlers.Mouse;
using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Input.Handlers.Touch;
using osu.Framework.IO.Stores;
+using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Timing;
@@ -36,11 +38,13 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
using osu.Game.Configuration;
using osu.Game.Database;
+using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Cursor;
using osu.Game.Input;
using osu.Game.Input.Bindings;
using osu.Game.IO;
+using osu.Game.Localisation;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.Chat;
@@ -157,6 +161,11 @@ namespace osu.Game
protected Storage Storage { get; set; }
+ ///
+ /// The language in which the game is currently displayed in.
+ ///
+ public Bindable CurrentLanguage { get; } = new Bindable();
+
protected Bindable Beatmap { get; private set; } // cached via load() method
///
@@ -216,6 +225,10 @@ namespace osu.Game
private readonly BindableNumber globalTrackVolumeAdjust = new BindableNumber(global_track_volume_adjust);
+ private Bindable frameworkLocale = null!;
+
+ private IBindable localisationParameters = null!;
+
///
/// Number of unhandled exceptions to allow before aborting execution.
///
@@ -238,7 +251,7 @@ namespace osu.Game
}
[BackgroundDependencyLoader]
- private void load(ReadableKeyCombinationProvider keyCombinationProvider)
+ private void load(ReadableKeyCombinationProvider keyCombinationProvider, FrameworkConfigManager frameworkConfig)
{
try
{
@@ -283,7 +296,15 @@ namespace osu.Game
MessageFormatter.WebsiteRootUrl = endpoints.WebsiteRootUrl;
- dependencies.CacheAs(API ??= new APIAccess(LocalConfig, endpoints, VersionHash));
+ frameworkLocale = frameworkConfig.GetBindable(FrameworkSetting.Locale);
+ frameworkLocale.BindValueChanged(_ => updateLanguage());
+
+ localisationParameters = Localisation.CurrentParameters.GetBoundCopy();
+ localisationParameters.BindValueChanged(_ => updateLanguage(), true);
+
+ CurrentLanguage.BindValueChanged(val => frameworkLocale.Value = val.NewValue.ToCultureCode());
+
+ dependencies.CacheAs(API ??= new APIAccess(this, LocalConfig, endpoints, VersionHash));
var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures);
@@ -394,6 +415,8 @@ namespace osu.Game
Beatmap.BindValueChanged(onBeatmapChanged);
}
+ private void updateLanguage() => CurrentLanguage.Value = LanguageExtensions.GetLanguageFor(frameworkLocale.Value, localisationParameters.Value);
+
private void addFilesWarning()
{
var realmStore = new RealmFileStore(realm, Storage);
diff --git a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs
index 57b6f6268c..21b6147113 100644
--- a/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs
+++ b/osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs
@@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
new Drawable?[]
{
createIcon(),
- text = new OsuSpriteText
+ text = new TruncatingSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
@@ -94,7 +94,6 @@ namespace osu.Game.Overlays.Chat.ChannelList
Colour = colourProvider.Light3,
Margin = new MarginPadding { Bottom = 2 },
RelativeSizeAxes = Axes.X,
- Truncate = true,
},
createMentionPill(),
close = createCloseButton(),
diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs
index 2f4c175ac4..c85206d5f7 100644
--- a/osu.Game/Overlays/Chat/ChatLine.cs
+++ b/osu.Game/Overlays/Chat/ChatLine.cs
@@ -56,11 +56,11 @@ namespace osu.Game.Overlays.Chat
[Resolved]
private OverlayColourProvider? colourProvider { get; set; }
- private readonly OsuSpriteText drawableTimestamp;
+ private OsuSpriteText drawableTimestamp = null!;
- private readonly DrawableChatUsername drawableUsername;
+ private DrawableChatUsername drawableUsername = null!;
- private readonly LinkFlowContainer drawableContentFlow;
+ private LinkFlowContainer drawableContentFlow = null!;
private readonly Bindable prefer24HourTime = new Bindable();
@@ -69,8 +69,16 @@ namespace osu.Game.Overlays.Chat
public ChatLine(Message message)
{
Message = message;
+
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager configManager)
+ {
+ configManager.BindWith(OsuSetting.Prefer24HourTime, prefer24HourTime);
+ prefer24HourTime.BindValueChanged(_ => updateTimestamp());
InternalChild = new GridContainer
{
@@ -103,7 +111,6 @@ namespace osu.Game.Overlays.Chat
Origin = Anchor.TopRight,
Anchor = Anchor.TopRight,
Margin = new MarginPadding { Horizontal = Spacing },
- ReportRequested = this.ShowPopover,
},
drawableContentFlow = new LinkFlowContainer(styleMessageContent)
{
@@ -115,13 +122,6 @@ namespace osu.Game.Overlays.Chat
};
}
- [BackgroundDependencyLoader]
- private void load(OsuConfigManager configManager)
- {
- configManager.BindWith(OsuSetting.Prefer24HourTime, prefer24HourTime);
- prefer24HourTime.BindValueChanged(_ => updateTimestamp());
- }
-
protected override void LoadComplete()
{
base.LoadComplete();
@@ -130,6 +130,17 @@ namespace osu.Game.Overlays.Chat
updateMessageContent();
FinishTransforms(true);
+
+ if (this.FindClosestParent() != null)
+ {
+ // This guards against cases like in-game chat where there's no available popover container.
+ // There may be a future where a global one becomes available, at which point this code may be unnecessary.
+ //
+ // See:
+ // https://github.com/ppy/osu/pull/23698
+ // https://github.com/ppy/osu/pull/14554
+ drawableUsername.ReportRequested = this.ShowPopover;
+ }
}
public Popover GetPopover() => new ReportChatPopover(message);
diff --git a/osu.Game/Overlays/Chat/ChatTextBar.cs b/osu.Game/Overlays/Chat/ChatTextBar.cs
index fd5e0e9836..87e787fcb8 100644
--- a/osu.Game/Overlays/Chat/ChatTextBar.cs
+++ b/osu.Game/Overlays/Chat/ChatTextBar.cs
@@ -73,14 +73,13 @@ namespace osu.Game.Overlays.Chat
Width = chatting_text_width,
Masking = true,
Padding = new MarginPadding { Horizontal = padding },
- Child = chattingText = new OsuSpriteText
+ Child = chattingText = new TruncatingSpriteText
{
MaxWidth = chatting_text_width - padding * 2,
Font = OsuFont.Torus.With(size: 20),
Colour = colourProvider.Background1,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
- Truncate = true,
},
},
searchIconContainer = new Container
diff --git a/osu.Game/Overlays/Chat/DrawableChatUsername.cs b/osu.Game/Overlays/Chat/DrawableChatUsername.cs
index 4b4afc204c..18632aa4af 100644
--- a/osu.Game/Overlays/Chat/DrawableChatUsername.cs
+++ b/osu.Game/Overlays/Chat/DrawableChatUsername.cs
@@ -83,11 +83,9 @@ namespace osu.Game.Overlays.Chat
Action = openUserProfile;
- drawableText = new OsuSpriteText
+ drawableText = new TruncatingSpriteText
{
Shadow = false,
- Truncate = true,
- EllipsisString = "…",
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
};
diff --git a/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs
index 792d6cc785..f36e6b49bb 100644
--- a/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs
+++ b/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs
@@ -100,17 +100,15 @@ namespace osu.Game.Overlays.Dashboard.Home
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
- new OsuSpriteText
+ new TruncatingSpriteText
{
RelativeSizeAxes = Axes.X,
- Truncate = true,
Font = OsuFont.GetFont(weight: FontWeight.Regular),
Text = BeatmapSet.Title
},
- new OsuSpriteText
+ new TruncatingSpriteText
{
RelativeSizeAxes = Axes.X,
- Truncate = true,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular),
Text = BeatmapSet.Artist
},
diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs
index b8d802ad4b..68c6c78986 100644
--- a/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs
+++ b/osu.Game/Overlays/FirstRunSetup/ScreenWelcome.cs
@@ -13,7 +13,6 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Threading;
-using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@@ -68,13 +67,12 @@ namespace osu.Game.Overlays.FirstRunSetup
private partial class LanguageSelectionFlow : FillFlowContainer
{
- private Bindable frameworkLocale = null!;
- private IBindable localisationParameters = null!;
+ private Bindable language = null!;
private ScheduledDelegate? updateSelectedDelegate;
[BackgroundDependencyLoader]
- private void load(FrameworkConfigManager frameworkConfig, LocalisationManager localisation)
+ private void load(OsuGameBase game)
{
Direction = FillDirection.Full;
Spacing = new Vector2(5);
@@ -82,25 +80,18 @@ namespace osu.Game.Overlays.FirstRunSetup
ChildrenEnumerable = Enum.GetValues()
.Select(l => new LanguageButton(l)
{
- Action = () => frameworkLocale.Value = l.ToCultureCode()
+ Action = () => language.Value = l,
});
- frameworkLocale = frameworkConfig.GetBindable(FrameworkSetting.Locale);
- frameworkLocale.BindValueChanged(_ => onLanguageChange());
-
- localisationParameters = localisation.CurrentParameters.GetBoundCopy();
- localisationParameters.BindValueChanged(_ => onLanguageChange(), true);
- }
-
- private void onLanguageChange()
- {
- var language = LanguageExtensions.GetLanguageFor(frameworkLocale.Value, localisationParameters.Value);
-
- // Changing language may cause a short period of blocking the UI thread while the new glyphs are loaded.
- // Scheduling ensures the button animation plays smoothly after any blocking operation completes.
- // Note that a delay is required (the alternative would be a double-schedule; delay feels better).
- updateSelectedDelegate?.Cancel();
- updateSelectedDelegate = Scheduler.AddDelayed(() => updateSelectedStates(language), 50);
+ language = game.CurrentLanguage.GetBoundCopy();
+ language.BindValueChanged(v =>
+ {
+ // Changing language may cause a short period of blocking the UI thread while the new glyphs are loaded.
+ // Scheduling ensures the button animation plays smoothly after any blocking operation completes.
+ // Note that a delay is required (the alternative would be a double-schedule; delay feels better).
+ updateSelectedDelegate?.Cancel();
+ updateSelectedDelegate = Scheduler.AddDelayed(() => updateSelectedStates(v.NewValue), 50);
+ }, true);
}
private void updateSelectedStates(Language language)
diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs
index 5d9f616e5f..fe42cc0abf 100644
--- a/osu.Game/Overlays/Mods/ModColumn.cs
+++ b/osu.Game/Overlays/Mods/ModColumn.cs
@@ -176,7 +176,7 @@ namespace osu.Game.Overlays.Mods
dequeuedAction();
// each time we play an animation, we decrease the time until the next animation (to ramp the visual and audible elements).
- selectionDelay = Math.Max(30, selectionDelay * 0.8f);
+ selectionDelay = Math.Max(ModSelectPanel.SAMPLE_PLAYBACK_DELAY, selectionDelay * 0.8f);
lastSelection = Time.Current;
}
else
diff --git a/osu.Game/Overlays/Mods/ModSelectPanel.cs b/osu.Game/Overlays/Mods/ModSelectPanel.cs
index 81285833bd..1f42292590 100644
--- a/osu.Game/Overlays/Mods/ModSelectPanel.cs
+++ b/osu.Game/Overlays/Mods/ModSelectPanel.cs
@@ -14,6 +14,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Audio;
+using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@@ -45,6 +46,8 @@ namespace osu.Game.Overlays.Mods
public const float CORNER_RADIUS = 7;
public const float HEIGHT = 42;
+ public const double SAMPLE_PLAYBACK_DELAY = 30;
+
protected virtual float IdleSwitchWidth => 14;
protected virtual float ExpandedSwitchWidth => 30;
protected virtual Colour4 BackgroundColour => Active.Value ? AccentColour.Darken(0.3f) : ColourProvider.Background3;
@@ -69,6 +72,8 @@ namespace osu.Game.Overlays.Mods
private Sample? sampleOff;
private Sample? sampleOn;
+ private Bindable lastPlaybackTime = null!;
+
protected ModSelectPanel()
{
RelativeSizeAxes = Axes.X;
@@ -118,23 +123,23 @@ namespace osu.Game.Overlays.Mods
Direction = FillDirection.Vertical,
Children = new[]
{
- titleText = new OsuSpriteText
+ titleText = new TruncatingSpriteText
{
Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
- Truncate = true,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
Margin = new MarginPadding
{
Left = -18 * ShearedOverlayContainer.SHEAR
- }
+ },
+ ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`.
},
- descriptionText = new OsuSpriteText
+ descriptionText = new TruncatingSpriteText
{
Font = OsuFont.Default.With(size: 12),
RelativeSizeAxes = Axes.X,
- Truncate = true,
- Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0)
+ Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
+ ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`.
}
}
}
@@ -163,13 +168,15 @@ namespace osu.Game.Overlays.Mods
protected abstract void Deselect();
[BackgroundDependencyLoader]
- private void load(AudioManager audio, ISamplePlaybackDisabler? samplePlaybackDisabler)
+ private void load(AudioManager audio, SessionStatics statics, ISamplePlaybackDisabler? samplePlaybackDisabler)
{
sampleOn = audio.Samples.Get(@"UI/check-on");
sampleOff = audio.Samples.Get(@"UI/check-off");
if (samplePlaybackDisabler != null)
((IBindable)samplePlaybackDisabled).BindTo(samplePlaybackDisabler.SamplePlaybackDisabled);
+
+ lastPlaybackTime = statics.GetBindable(Static.LastHoverSoundPlaybackTime);
}
protected sealed override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet);
@@ -192,10 +199,17 @@ namespace osu.Game.Overlays.Mods
if (samplePlaybackDisabled.Value)
return;
- if (Active.Value)
- sampleOn?.Play();
- else
- sampleOff?.Play();
+ bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= SAMPLE_PLAYBACK_DELAY;
+
+ if (enoughTimePassedSinceLastPlayback)
+ {
+ if (Active.Value)
+ sampleOn?.Play();
+ else
+ sampleOff?.Play();
+
+ lastPlaybackTime.Value = Time.Current;
+ }
}
protected override bool OnHover(HoverEvent e)
diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs
index 66fb3571ba..e3e3b4bd80 100644
--- a/osu.Game/Overlays/NowPlayingOverlay.cs
+++ b/osu.Game/Overlays/NowPlayingOverlay.cs
@@ -415,7 +415,7 @@ namespace osu.Game.Overlays
[BackgroundDependencyLoader]
private void load(LargeTextureStore textures)
{
- sprite.Texture = beatmap?.Background ?? textures.Get(@"Backgrounds/bg4");
+ sprite.Texture = beatmap?.GetBackground() ?? textures.Get(@"Backgrounds/bg4");
}
}
diff --git a/osu.Game/Overlays/OverlayActivation.cs b/osu.Game/Overlays/OverlayActivation.cs
index 354153734e..68d7ee8ea9 100644
--- a/osu.Game/Overlays/OverlayActivation.cs
+++ b/osu.Game/Overlays/OverlayActivation.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Overlays
{
public enum OverlayActivation
diff --git a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs
index 1755c12f94..fc354027c1 100644
--- a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
{
protected override LocalisableString Header => AudioSettingsStrings.OffsetHeader;
- public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "universal", "uo", "timing" });
+ public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "universal", "uo", "timing", "delay", "latency" });
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
diff --git a/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs
index 982cbec376..cf7f63211e 100644
--- a/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs
@@ -2,35 +2,27 @@
// 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.Localisation;
using osu.Game.Configuration;
-using osu.Game.Extensions;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Settings.Sections.General
{
public partial class LanguageSettings : SettingsSubsection
{
- private SettingsDropdown languageSelection = null!;
- private Bindable frameworkLocale = null!;
- private IBindable localisationParameters = null!;
-
protected override LocalisableString Header => GeneralSettingsStrings.LanguageHeader;
[BackgroundDependencyLoader]
- private void load(FrameworkConfigManager frameworkConfig, OsuConfigManager config, LocalisationManager localisation)
+ private void load(OsuGameBase game, OsuConfigManager config, FrameworkConfigManager frameworkConfig)
{
- frameworkLocale = frameworkConfig.GetBindable(FrameworkSetting.Locale);
- localisationParameters = localisation.CurrentParameters.GetBoundCopy();
-
Children = new Drawable[]
{
- languageSelection = new SettingsEnumDropdown
+ new SettingsEnumDropdown
{
LabelText = GeneralSettingsStrings.LanguageDropdown,
+ Current = game.CurrentLanguage,
},
new SettingsCheckbox
{
@@ -43,14 +35,6 @@ namespace osu.Game.Overlays.Settings.Sections.General
Current = config.GetBindable(OsuSetting.Prefer24HourTime)
},
};
-
- frameworkLocale.BindValueChanged(_ => updateSelection());
- localisationParameters.BindValueChanged(_ => updateSelection(), true);
-
- languageSelection.Current.BindValueChanged(val => frameworkLocale.Value = val.NewValue.ToCultureCode());
}
-
- private void updateSelection() =>
- languageSelection.Current.Value = LanguageExtensions.GetLanguageFor(frameworkLocale.Value, localisationParameters.Value);
}
}
diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs
index d571557993..1681187f82 100644
--- a/osu.Game/Overlays/SettingsPanel.cs
+++ b/osu.Game/Overlays/SettingsPanel.cs
@@ -328,7 +328,7 @@ namespace osu.Game.Overlays
base.UpdateAfterChildren();
// no null check because the usage of this class is strict
- HeaderBackground.Alpha = -ExpandableHeader.Y / ExpandableHeader.LayoutSize.Y;
+ HeaderBackground!.Alpha = -ExpandableHeader!.Y / ExpandableHeader.LayoutSize.Y;
}
}
}
diff --git a/osu.Game/Overlays/SortDirection.cs b/osu.Game/Overlays/SortDirection.cs
index 98ac31103f..3af9614972 100644
--- a/osu.Game/Overlays/SortDirection.cs
+++ b/osu.Game/Overlays/SortDirection.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Overlays
{
public enum SortDirection
diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs
index ab4f07b982..0ab842c907 100644
--- a/osu.Game/Overlays/UserProfileOverlay.cs
+++ b/osu.Game/Overlays/UserProfileOverlay.cs
@@ -120,7 +120,7 @@ namespace osu.Game.Overlays
if (lastSection != section.NewValue)
{
lastSection = section.NewValue;
- tabs.Current.Value = lastSection;
+ tabs.Current.Value = lastSection!;
}
};
diff --git a/osu.Game/Rulesets/Configuration/IRulesetConfigManager.cs b/osu.Game/Rulesets/Configuration/IRulesetConfigManager.cs
index af315bfb28..5a3ad5e786 100644
--- a/osu.Game/Rulesets/Configuration/IRulesetConfigManager.cs
+++ b/osu.Game/Rulesets/Configuration/IRulesetConfigManager.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using osu.Framework.Configuration.Tracking;
diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
index 8dd1b51cae..00c90bd317 100644
--- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
+++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
@@ -137,7 +137,7 @@ namespace osu.Game.Rulesets.Difficulty
foreach (var combination in CreateDifficultyAdjustmentModCombinations())
{
- Mod classicMod = rulesetInstance.CreateAllMods().SingleOrDefault(m => m is ModClassic);
+ Mod classicMod = rulesetInstance.CreateMod();
var finalCombination = ModUtils.FlattenMod(combination);
if (classicMod != null)
diff --git a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs
index 23fa28e7bc..8c3a5c026d 100644
--- a/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs
+++ b/osu.Game/Rulesets/Edit/Checks/CheckBackgroundQuality.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Edit.Checks
if (backgroundFile == null)
yield break;
- var texture = context.WorkingBeatmap.Background;
+ var texture = context.WorkingBeatmap.GetBackground();
if (texture == null)
yield break;
diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
index a8972775de..817e8bd5fe 100644
--- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
@@ -125,6 +125,7 @@ namespace osu.Game.Rulesets.Edit
if (currentSnap > DistanceSpacingMultiplier.MinValue)
{
currentDistanceSpacingButton.Enabled.Value = currentDistanceSpacingButton.Expanded.Value
+ && !DistanceSpacingMultiplier.Disabled
&& !Precision.AlmostEquals(currentSnap, DistanceSpacingMultiplier.Value, DistanceSpacingMultiplier.Precision / 2);
currentDistanceSpacingButton.ContractedLabelText = $"current {currentSnap:N2}x";
currentDistanceSpacingButton.ExpandedLabelText = $"Use current ({currentSnap:N2}x)";
@@ -141,28 +142,31 @@ namespace osu.Game.Rulesets.Edit
{
base.LoadComplete();
- if (!DistanceSpacingMultiplier.Disabled)
+ if (DistanceSpacingMultiplier.Disabled)
{
- DistanceSpacingMultiplier.Value = EditorBeatmap.BeatmapInfo.DistanceSpacing;
- DistanceSpacingMultiplier.BindValueChanged(multiplier =>
- {
- distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})";
- distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({multiplier.NewValue:0.##x})";
-
- if (multiplier.NewValue != multiplier.OldValue)
- onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier));
-
- EditorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue;
- }, true);
-
- // Manual binding to handle enabling distance spacing when the slider is interacted with.
- distanceSpacingSlider.Current.BindValueChanged(spacing =>
- {
- DistanceSpacingMultiplier.Value = spacing.NewValue;
- DistanceSnapToggle.Value = TernaryState.True;
- });
- DistanceSpacingMultiplier.BindValueChanged(spacing => distanceSpacingSlider.Current.Value = spacing.NewValue);
+ distanceSpacingSlider.Hide();
+ return;
}
+
+ DistanceSpacingMultiplier.Value = EditorBeatmap.BeatmapInfo.DistanceSpacing;
+ DistanceSpacingMultiplier.BindValueChanged(multiplier =>
+ {
+ distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})";
+ distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({multiplier.NewValue:0.##x})";
+
+ if (multiplier.NewValue != multiplier.OldValue)
+ onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier));
+
+ EditorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue;
+ }, true);
+
+ // Manual binding to handle enabling distance spacing when the slider is interacted with.
+ distanceSpacingSlider.Current.BindValueChanged(spacing =>
+ {
+ DistanceSpacingMultiplier.Value = spacing.NewValue;
+ DistanceSnapToggle.Value = TernaryState.True;
+ });
+ DistanceSpacingMultiplier.BindValueChanged(spacing => distanceSpacingSlider.Current.Value = spacing.NewValue);
}
protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
diff --git a/osu.Game/Rulesets/Mods/IHasNoTimedInputs.cs b/osu.Game/Rulesets/Mods/IHasNoTimedInputs.cs
new file mode 100644
index 0000000000..c0d709ad4a
--- /dev/null
+++ b/osu.Game/Rulesets/Mods/IHasNoTimedInputs.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Rulesets.Mods
+{
+ ///
+ /// Denotes a mod which removes timed inputs from a ruleset which would usually have them.
+ ///
+ ///
+ /// This will be used, for instance, to omit showing offset calibration UI post-gameplay.
+ ///
+ public interface IHasNoTimedInputs
+ {
+ }
+}
diff --git a/osu.Game/Rulesets/Objects/Drawables/ArmedState.cs b/osu.Game/Rulesets/Objects/Drawables/ArmedState.cs
index 4faf0920d1..b2d9f50602 100644
--- a/osu.Game/Rulesets/Objects/Drawables/ArmedState.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/ArmedState.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Rulesets.Objects.Drawables
{
public enum ArmedState
diff --git a/osu.Game/Rulesets/Objects/IBarLine.cs b/osu.Game/Rulesets/Objects/IBarLine.cs
index 8cdead6776..14df80e3b9 100644
--- a/osu.Game/Rulesets/Objects/IBarLine.cs
+++ b/osu.Game/Rulesets/Objects/IBarLine.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Rulesets.Objects
{
///
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
index d9738ecd0a..8eda2a8f61 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
@@ -586,7 +586,5 @@ namespace osu.Game.Rulesets.Objects.Legacy
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), Filename);
}
-
-#nullable disable
}
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs
index 639cacb128..0b69817c13 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHit.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Objects.Legacy.Mania
diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs
index 330ebf72c7..84cde5fa95 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSlider.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Objects.Legacy.Mania
diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs
index 980d37ccd5..cb5178ce48 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHit.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Rulesets.Objects.Legacy.Taiko
{
///
diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs
index a391c8cb43..821554f7ee 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSlider.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Rulesets.Objects.Legacy.Taiko
{
///
diff --git a/osu.Game/Rulesets/Objects/Types/IHasColumn.cs b/osu.Game/Rulesets/Objects/Types/IHasColumn.cs
index 3978a7e765..dc07cfbb6a 100644
--- a/osu.Game/Rulesets/Objects/Types/IHasColumn.cs
+++ b/osu.Game/Rulesets/Objects/Types/IHasColumn.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Rulesets.Objects.Types
{
///
diff --git a/osu.Game/Rulesets/Objects/Types/IHasCombo.cs b/osu.Game/Rulesets/Objects/Types/IHasCombo.cs
index d02b97a3e4..d1a4683a1d 100644
--- a/osu.Game/Rulesets/Objects/Types/IHasCombo.cs
+++ b/osu.Game/Rulesets/Objects/Types/IHasCombo.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Rulesets.Objects.Types
{
///
diff --git a/osu.Game/Rulesets/Objects/Types/IHasDistance.cs b/osu.Game/Rulesets/Objects/Types/IHasDistance.cs
index 549abc046a..b497ca5da3 100644
--- a/osu.Game/Rulesets/Objects/Types/IHasDistance.cs
+++ b/osu.Game/Rulesets/Objects/Types/IHasDistance.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd