1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 07:07:45 +08:00

Merge branch 'master' into diffcalc-total-scorev1

This commit is contained in:
Dan Balasescu 2023-06-15 19:33:29 +09:00
commit 0844a21a51
207 changed files with 2041 additions and 1002 deletions

View File

@ -2,7 +2,7 @@ blank_issues_enabled: false
contact_links: contact_links:
- name: Help - name: Help
url: https://github.com/ppy/osu/discussions/categories/q-a 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 - name: Suggestions or feature request
url: https://github.com/ppy/osu/discussions/categories/ideas 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! about: Got something you think should change or be added? Search for or start a new discussion!

View File

@ -11,7 +11,7 @@
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger> <AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.521.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2023.608.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" /> <AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />

View File

@ -0,0 +1,180 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{
/// <summary>
/// A grid which displays coloured beat divisor lines in proximity to the selection or placement cursor.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public partial class CatchBeatSnapGrid : Component
{
private const double visible_range = 750;
/// <summary>
/// The range of time values of the current selection.
/// </summary>
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<DrawableGridLine> availableLines = new Stack<DrawableGridLine>();
private void createLines()
{
foreach (var line in lineContainer.Objects.OfType<DrawableGridLine>())
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<DrawableGridLine>())
{
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;
}
}
}
}

View File

@ -33,6 +33,8 @@ namespace osu.Game.Rulesets.Catch.Edit
private InputManager inputManager = null!; private InputManager inputManager = null!;
private CatchBeatSnapGrid beatSnapGrid = null!;
private readonly BindableDouble timeRangeMultiplier = new BindableDouble(1) private readonly BindableDouble timeRangeMultiplier = new BindableDouble(1)
{ {
MinValue = 1, MinValue = 1,
@ -65,6 +67,8 @@ namespace osu.Game.Rulesets.Catch.Edit
Catcher.BASE_DASH_SPEED, -Catcher.BASE_DASH_SPEED, Catcher.BASE_DASH_SPEED, -Catcher.BASE_DASH_SPEED,
Catcher.BASE_WALK_SPEED, -Catcher.BASE_WALK_SPEED, Catcher.BASE_WALK_SPEED, -Catcher.BASE_WALK_SPEED,
})); }));
AddInternal(beatSnapGrid = new CatchBeatSnapGrid());
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -74,6 +78,29 @@ namespace osu.Game.Rulesets.Catch.Edit
inputManager = GetContainingInputManager(); 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) protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
{ {
// osu!catch's distance snap implementation is limited, in that a custom spacing cannot be specified. // osu!catch's distance snap implementation is limited, in that a custom spacing cannot be specified.

View File

@ -3,6 +3,7 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
@ -41,6 +42,8 @@ namespace osu.Game.Rulesets.Catch.UI
internal CatcherArea CatcherArea { get; private set; } = null!; internal CatcherArea CatcherArea { get; private set; } = null!;
public Container UnderlayElements { get; private set; } = null!;
private readonly IBeatmapDifficultyInfo difficulty; private readonly IBeatmapDifficultyInfo difficulty;
public CatchPlayfield(IBeatmapDifficultyInfo difficulty) public CatchPlayfield(IBeatmapDifficultyInfo difficulty)
@ -62,6 +65,10 @@ namespace osu.Game.Rulesets.Catch.UI
AddRangeInternal(new[] AddRangeInternal(new[]
{ {
UnderlayElements = new Container
{
RelativeSizeAxes = Axes.Both,
},
droppedObjectContainer, droppedObjectContainer,
Catcher.CreateProxiedContent(), Catcher.CreateProxiedContent(),
HitObjectContainer.CreateProxy(), HitObjectContainer.CreateProxy(),

View File

@ -21,18 +21,29 @@ namespace osu.Game.Rulesets.Mania.Configuration
{ {
base.InitialiseDefaults(); 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.ScrollDirection, ManiaScrollingDirection.Down);
SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false); 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<double?>(ManiaRulesetSetting.ScrollTime, null);
if (Get<double?>(ManiaRulesetSetting.ScrollTime) is double scrollTime)
{
SetValue(ManiaRulesetSetting.ScrollSpeed, (int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime));
SetValue<double?>(ManiaRulesetSetting.ScrollTime, null);
}
#pragma warning restore CS0618
} }
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
{ {
new TrackedSetting<double>(ManiaRulesetSetting.ScrollTime, new TrackedSetting<int>(ManiaRulesetSetting.ScrollSpeed,
scrollTime => new SettingDescription( speed => new SettingDescription(
rawValue: scrollTime, rawValue: speed,
name: RulesetSettingsStrings.ScrollSpeed, 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 public enum ManiaRulesetSetting
{ {
[Obsolete("Use ScrollSpeed instead.")] // Can be removed 2023-11-30
ScrollTime, ScrollTime,
ScrollSpeed,
ScrollDirection, ScrollDirection,
TimingBasedNoteColouring TimingBasedNoteColouring
} }

View File

@ -129,7 +129,7 @@ namespace osu.Game.Rulesets.Mania.Edit
} }
Color4 colour = BindableBeatDivisor.GetColourFor( Color4 colour = BindableBeatDivisor.GetColourFor(
BindableBeatDivisor.GetDivisorForBeatIndex(Math.Max(1, beat), beatDivisor.Value), colours); BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value), colours);
foreach (var grid in grids) foreach (var grid in grids)
{ {

View File

@ -389,41 +389,23 @@ namespace osu.Game.Rulesets.Mania
return base.GetDisplayNameForHitResult(result); 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[] RelativeSizeAxes = Axes.X,
{ AutoSizeAxes = Axes.Y
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) }),
{ new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(score.HitEvents)
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}),
}
},
new StatisticRow
{ {
Columns = new[] RelativeSizeAxes = Axes.X,
{ Height = 250
new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(score.HitEvents) }, true),
{ new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
RelativeSizeAxes = Axes.X,
Height = 250
}, true),
}
},
new StatisticRow
{ {
Columns = new[] new AverageHitError(score.HitEvents),
{ new UnstableRate(score.HitEvents)
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] }), true)
{
new AverageHitError(score.HitEvents),
new UnstableRate(score.HitEvents)
}), true)
}
}
}; };
public override IRulesetFilterCriteria CreateRulesetFilterCriteria() public override IRulesetFilterCriteria CreateRulesetFilterCriteria()

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation; using osu.Framework.Localisation;
@ -34,10 +33,10 @@ namespace osu.Game.Rulesets.Mania
LabelText = RulesetSettingsStrings.ScrollingDirection, LabelText = RulesetSettingsStrings.ScrollingDirection,
Current = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection) Current = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection)
}, },
new SettingsSlider<double, ManiaScrollSlider> new SettingsSlider<int, ManiaScrollSlider>
{ {
LabelText = RulesetSettingsStrings.ScrollSpeed, LabelText = RulesetSettingsStrings.ScrollSpeed,
Current = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime), Current = config.GetBindable<int>(ManiaRulesetSetting.ScrollSpeed),
KeyboardStep = 5 KeyboardStep = 5
}, },
new SettingsCheckbox new SettingsCheckbox
@ -48,9 +47,9 @@ namespace osu.Game.Rulesets.Mania
}; };
} }
private partial class ManiaScrollSlider : RoundedSliderBar<double> private partial class ManiaScrollSlider : RoundedSliderBar<int>
{ {
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);
} }
} }
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;

View File

@ -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: // 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. // 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. // 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. // How far past the hit target this hold note is.
float yOffset = Direction.Value == ScrollingDirection.Up ? -Y : Y; float yOffset = Direction.Value == ScrollingDirection.Up ? -Y : Y;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Rulesets.Mania.Objects namespace osu.Game.Rulesets.Mania.Objects
{ {
public class HeadNote : Note public class HeadNote : Note

View File

@ -18,8 +18,8 @@ namespace osu.Game.Rulesets.Mania.Scoring
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion) protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
{ {
return 200000 * comboProgress return 10000 * comboProgress
+ 800000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress + 990000 * Math.Pow(Accuracy.Value, 2 + 2 * Accuracy.Value) * accuracyProgress
+ bonusPortion; + bonusPortion;
} }

View File

@ -139,11 +139,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case 3: case 3:
switch (columnIndex) 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(); default: throw new ArgumentOutOfRangeException();
} }
@ -185,11 +185,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case 1: return colour_orange; case 1: return colour_orange;
case 2: return colour_yellow; case 2: return colour_green;
case 3: return colour_cyan; case 3: return colour_cyan;
case 4: return colour_purple; case 4: return colour_orange;
case 5: return colour_pink; case 5: return colour_pink;
@ -201,17 +201,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
{ {
case 0: return colour_pink; case 0: return colour_pink;
case 1: return colour_cyan; case 1: return colour_orange;
case 2: return colour_pink; case 2: return colour_pink;
case 3: return colour_special_column; 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(); default: throw new ArgumentOutOfRangeException();
} }
@ -225,9 +225,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case 2: return colour_orange; 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; case 5: return colour_orange;
@ -273,9 +273,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case 3: return colour_yellow; 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; case 6: return colour_yellow;

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Pooling;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Extensions;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
@ -39,7 +40,11 @@ namespace osu.Game.Rulesets.Mania.UI
public readonly Bindable<ManiaAction> Action = new Bindable<ManiaAction>(); public readonly Bindable<ManiaAction> Action = new Bindable<ManiaAction>();
public readonly ColumnHitObjectArea HitObjectArea; public readonly ColumnHitObjectArea HitObjectArea;
internal readonly Container BackgroundContainer = new Container { RelativeSizeAxes = Axes.Both };
internal readonly Container TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }; internal readonly Container TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both };
private DrawablePool<PoolableHitExplosion> hitExplosionPool; private DrawablePool<PoolableHitExplosion> hitExplosionPool;
private readonly OrderedHitPolicy hitPolicy; private readonly OrderedHitPolicy hitPolicy;
public Container UnderlayElements => HitObjectArea.UnderlayElements; public Container UnderlayElements => HitObjectArea.UnderlayElements;
@ -76,30 +81,31 @@ namespace osu.Game.Rulesets.Mania.UI
skin.SourceChanged += onSourceChanged; skin.SourceChanged += onSourceChanged;
onSourceChanged(); onSourceChanged();
Drawable background = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground()) InternalChildren = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
};
InternalChildren = new[]
{ {
hitExplosionPool = new DrawablePool<PoolableHitExplosion>(5), hitExplosionPool = new DrawablePool<PoolableHitExplosion>(5),
sampleTriggerSource = new GameplaySampleTriggerSource(HitObjectContainer), 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, HitObjectArea,
keyArea = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea()) keyArea = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
{ {
RelativeSizeAxes = Axes.Both, 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, TopLevelContainer,
new ColumnTouchInputArea(this) new ColumnTouchInputArea(this)
}; };
applyGameWideClock(background); var background = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
applyGameWideClock(keyArea); {
RelativeSizeAxes = Axes.Both,
};
background.ApplyGameWideClock(host);
keyArea.ApplyGameWideClock(host);
BackgroundContainer.Add(background);
TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy()); TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
RegisterPool<Note, DrawableNote>(10, 50); RegisterPool<Note, DrawableNote>(10, 50);
@ -107,18 +113,6 @@ namespace osu.Game.Rulesets.Mania.UI
RegisterPool<HeadNote, DrawableHoldNoteHead>(10, 50); RegisterPool<HeadNote, DrawableHoldNoteHead>(10, 50);
RegisterPool<TailNote, DrawableHoldNoteTail>(10, 50); RegisterPool<TailNote, DrawableHoldNoteTail>(10, 50);
RegisterPool<HoldNoteTick, DrawableHoldNoteTick>(50, 250); RegisterPool<HoldNoteTick, DrawableHoldNoteTick>(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() private void onSourceChanged()

View File

@ -33,12 +33,12 @@ namespace osu.Game.Rulesets.Mania.UI
public partial class DrawableManiaRuleset : DrawableScrollingRuleset<ManiaHitObject> public partial class DrawableManiaRuleset : DrawableScrollingRuleset<ManiaHitObject>
{ {
/// <summary> /// <summary>
/// The minimum time range. This occurs at a <see cref="relativeTimeRange"/> of 40. /// The minimum time range. This occurs at a <see cref="ManiaRulesetSetting.ScrollSpeed"/> of 40.
/// </summary> /// </summary>
public const double MIN_TIME_RANGE = 290; public const double MIN_TIME_RANGE = 290;
/// <summary> /// <summary>
/// The maximum time range. This occurs at a <see cref="relativeTimeRange"/> of 1. /// The maximum time range. This occurs with a <see cref="ManiaRulesetSetting.ScrollSpeed"/> of 1.
/// </summary> /// </summary>
public const double MAX_TIME_RANGE = 11485; public const double MAX_TIME_RANGE = 11485;
@ -69,7 +69,8 @@ namespace osu.Game.Rulesets.Mania.UI
protected override ScrollVisualisationMethod VisualisationMethod => scrollMethod; protected override ScrollVisualisationMethod VisualisationMethod => scrollMethod;
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>(); private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
private readonly BindableDouble configTimeRange = new BindableDouble(); private readonly BindableInt configScrollSpeed = new BindableInt();
private double smoothTimeRange;
// Stores the current speed adjustment active in gameplay. // Stores the current speed adjustment active in gameplay.
private readonly Track speedAdjustmentTrack = new TrackVirtual(0); private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
@ -78,6 +79,9 @@ namespace osu.Game.Rulesets.Mania.UI
: base(ruleset, beatmap, mods) : base(ruleset, beatmap, mods)
{ {
BarLines = new BarLineGenerator<BarLine>(Beatmap).BarLines; BarLines = new BarLineGenerator<BarLine>(Beatmap).BarLines;
TimeRange.MinValue = 1;
TimeRange.MaxValue = MAX_TIME_RANGE;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -104,30 +108,28 @@ namespace osu.Game.Rulesets.Mania.UI
Config.BindWith(ManiaRulesetSetting.ScrollDirection, configDirection); Config.BindWith(ManiaRulesetSetting.ScrollDirection, configDirection);
configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true); configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true);
Config.BindWith(ManiaRulesetSetting.ScrollTime, configTimeRange); Config.BindWith(ManiaRulesetSetting.ScrollSpeed, configScrollSpeed);
TimeRange.MinValue = configTimeRange.MinValue; configScrollSpeed.BindValueChanged(speed => this.TransformTo(nameof(smoothTimeRange), ComputeScrollTime(speed.NewValue), 200, Easing.OutQuint));
TimeRange.MaxValue = configTimeRange.MaxValue;
TimeRange.Value = smoothTimeRange = ComputeScrollTime(configScrollSpeed.Value);
} }
protected override void AdjustScrollSpeed(int amount) protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += 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 Update() protected override void Update()
{ {
base.Update(); base.Update();
updateTimeRange(); 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;
/// <summary>
/// Computes a scroll time (in milliseconds) from a scroll speed in the range of 1-40.
/// </summary>
/// <param name="scrollSpeed">The scroll speed.</param>
/// <returns>The scroll time.</returns>
public static double ComputeScrollTime(int scrollSpeed) => MAX_TIME_RANGE / scrollSpeed;
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer(); public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer();

View File

@ -60,6 +60,7 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;
AutoSizeAxes = Axes.X; AutoSizeAxes = Axes.X;
Container columnBackgrounds;
Container topLevelContainer; Container topLevelContainer;
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
@ -77,9 +78,10 @@ namespace osu.Game.Rulesets.Mania.UI
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}, },
columnFlow = new ColumnFlow<Column>(definition) columnBackgrounds = new Container
{ {
RelativeSizeAxes = Axes.Y, Name = "Column backgrounds",
RelativeSizeAxes = Axes.Both,
}, },
new Container new Container
{ {
@ -98,6 +100,10 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
} }
}, },
columnFlow = new ColumnFlow<Column>(definition)
{
RelativeSizeAxes = Axes.Y,
},
new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.StageForeground), _ => null) new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.StageForeground), _ => null)
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
@ -126,6 +132,7 @@ namespace osu.Game.Rulesets.Mania.UI
}; };
topLevelContainer.Add(column.TopLevelContainer.CreateProxy()); topLevelContainer.Add(column.TopLevelContainer.CreateProxy());
columnBackgrounds.Add(column.BackgroundContainer.CreateProxy());
columnFlow.SetContentForColumn(i, column); columnFlow.SetContentForColumn(i, column);
AddNested(column); AddNested(column);
} }

View File

@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("place first object", () => InputManager.Click(MouseButton.Left)); 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)); 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("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)); 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("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", () => AddAssert("object 3 snapped to 1", () =>
{ {
@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
return Precision.AlmostEquals(first.EndPosition, third.Position); 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", () => AddAssert("object 2 snapped to 1", () =>
{ {

View File

@ -1,22 +1,41 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods namespace osu.Game.Rulesets.Osu.Tests.Mods
{ {
public partial class TestSceneOsuModAutoplay : OsuModTestScene 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] [Test]
public void TestSpmUnaffectedByRateAdjust() public void TestSpmUnaffectedByRateAdjust()
=> runSpmTest(new OsuModDaycore => runSpmTest(new OsuModDaycore
@ -32,6 +51,36 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
FinalRate = { Value = 1.3 } 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<HitObject>
{
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) private void runSpmTest(Mod mod)
{ {
SpinnerSpmCalculator? spmCalculator = null; SpinnerSpmCalculator? spmCalculator = null;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{ {
public enum SliderPosition public enum SliderPosition

View File

@ -187,7 +187,7 @@ namespace osu.Game.Rulesets.Osu.Edit
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
float snapRadius = 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; playfield.GamefieldToScreenSpace(Vector2.Zero).X;
foreach (var b in blueprints) foreach (var b in blueprints)

View File

@ -42,14 +42,14 @@ namespace osu.Game.Rulesets.Osu.Mods
private PlayfieldAdjustmentContainer bubbleContainer = null!; private PlayfieldAdjustmentContainer bubbleContainer = null!;
private DrawablePool<BubbleDrawable> bubblePool = null!;
private readonly Bindable<int> currentCombo = new BindableInt(); private readonly Bindable<int> currentCombo = new BindableInt();
private float maxSize; private float maxSize;
private float bubbleSize; private float bubbleSize;
private double bubbleFade; private double bubbleFade;
private readonly DrawablePool<BubbleDrawable> bubblePool = new DrawablePool<BubbleDrawable>(100);
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank; public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
@ -72,6 +72,7 @@ namespace osu.Game.Rulesets.Osu.Mods
bubbleContainer = drawableRuleset.CreatePlayfieldAdjustmentContainer(); bubbleContainer = drawableRuleset.CreatePlayfieldAdjustmentContainer();
drawableRuleset.Overlays.Add(bubbleContainer); drawableRuleset.Overlays.Add(bubbleContainer);
drawableRuleset.Overlays.Add(bubblePool = new DrawablePool<BubbleDrawable>(100));
} }
public void ApplyToDrawableHitObject(DrawableHitObject drawableObject) public void ApplyToDrawableHitObject(DrawableHitObject drawableObject)

View File

@ -18,7 +18,7 @@ using static osu.Game.Input.Handlers.ReplayInputHandler;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer, IHasNoTimedInputs
{ {
public override LocalisableString Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things."; public override LocalisableString Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";

View File

@ -75,18 +75,24 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both };
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
{ {
shakeContainer = new ShakeContainer shakeContainer = new ShakeContainer
{ {
ShakeDuration = 30, ShakeDuration = 30,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new[]
{ {
Body = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling), Body = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling),
tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both }, // proxied here so that the tail is drawn under repeats/ticks - legacy skins rely on this
tailContainer.CreateProxy(),
tickContainer = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both }, tickContainer = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both },
repeatContainer = new Container<DrawableSliderRepeat> { RelativeSizeAxes = Axes.Both }, repeatContainer = new Container<DrawableSliderRepeat> { 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. // slider head is not included in shake as it handles hit detection, and handles its own shaking.

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public interface IRequireTracking public interface IRequireTracking

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Rulesets.Osu.Objects namespace osu.Game.Rulesets.Osu.Objects
{ {
public interface ISliderProgress public interface ISliderProgress

View File

@ -291,56 +291,32 @@ namespace osu.Game.Rulesets.Osu
return base.GetDisplayNameForHitResult(result); 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(); var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList();
return new[] return new[]
{ {
new StatisticRow new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
{ {
Columns = new[] RelativeSizeAxes = Axes.X,
{ AutoSizeAxes = Axes.Y
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) }),
{ new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents)
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}),
}
},
new StatisticRow
{ {
Columns = new[] RelativeSizeAxes = Axes.X,
{ Height = 250
new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents) }, true),
{ new StatisticItem("Accuracy Heatmap", () => new AccuracyHeatmap(score, playableBeatmap)
RelativeSizeAxes = Axes.X,
Height = 250
}, true),
}
},
new StatisticRow
{ {
Columns = new[] RelativeSizeAxes = Axes.X,
{ Height = 250
new StatisticItem("Accuracy Heatmap", () => new AccuracyHeatmap(score, playableBeatmap) }, true),
{ new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
RelativeSizeAxes = Axes.X,
Height = 250
}, true),
}
},
new StatisticRow
{ {
Columns = new[] new AverageHitError(timedHitEvents),
{ new UnstableRate(timedHitEvents)
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] }), true)
{
new AverageHitError(timedHitEvents),
new UnstableRate(timedHitEvents)
}), true)
}
}
}; };
} }

View File

@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.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) protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
{ {
return 700000 * comboProgress return 700000 * comboProgress

View File

@ -48,21 +48,26 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
private Bindable<bool> configHitLighting = null!; private Bindable<bool> configHitLighting = null!;
private static readonly Vector2 circle_size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
[Resolved] [Resolved]
private DrawableHitObject drawableObject { get; set; } = null!; private DrawableHitObject drawableObject { get; set; } = null!;
public ArgonMainCirclePiece(bool withOuterFill) public ArgonMainCirclePiece(bool withOuterFill)
{ {
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); Size = circle_size;
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
Origin = Anchor.Centre; Origin = Anchor.Centre;
InternalChildren = new Drawable[] 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, Alpha = withOuterFill ? 1 : 0,
}, },
outerGradient = new Circle // renders the outer bright gradient outerGradient = new Circle // renders the outer bright gradient
@ -88,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
Masking = true, Masking = true,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = Size, Size = circle_size,
Child = new KiaiFlash Child = new KiaiFlash
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,

View File

@ -15,187 +15,175 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
[Test] [Test]
public void TestHitAllDrumRoll() public void TestHitAllDrumRoll()
{ {
const double hit_time = 1000;
PerformTest(new List<ReplayFrame> PerformTest(new List<ReplayFrame>
{ {
new TaikoReplayFrame(0), new TaikoReplayFrame(0),
new TaikoReplayFrame(1000, TaikoAction.LeftCentre), new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
new TaikoReplayFrame(1001), 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(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2001), new TaikoReplayFrame(2001),
}, CreateBeatmap(new DrumRoll }, CreateBeatmap(createDrumRoll(false)));
{
StartTime = hit_time,
Duration = 1000
}));
AssertJudgementCount(3); AssertJudgementCount(6);
AssertResult<DrumRollTick>(0, HitResult.SmallBonus); AssertResult<DrumRollTick>(0, HitResult.SmallBonus);
AssertResult<DrumRollTick>(1, HitResult.SmallBonus); AssertResult<DrumRollTick>(1, HitResult.SmallBonus);
AssertResult<DrumRollTick>(2, HitResult.SmallBonus);
AssertResult<DrumRollTick>(3, HitResult.SmallBonus);
AssertResult<DrumRollTick>(4, HitResult.SmallBonus);
AssertResult<DrumRoll>(0, HitResult.IgnoreHit); AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
} }
[Test] [Test]
public void TestHitSomeDrumRoll() public void TestHitSomeDrumRoll()
{ {
const double hit_time = 1000;
PerformTest(new List<ReplayFrame> PerformTest(new List<ReplayFrame>
{ {
new TaikoReplayFrame(0), new TaikoReplayFrame(0),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre), new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2001), new TaikoReplayFrame(2001),
}, CreateBeatmap(new DrumRoll }, CreateBeatmap(createDrumRoll(false)));
{
StartTime = hit_time,
Duration = 1000
}));
AssertJudgementCount(3); AssertJudgementCount(6);
AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss); AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(1, HitResult.SmallBonus); AssertResult<DrumRollTick>(1, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(2, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(3, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(4, HitResult.SmallBonus);
AssertResult<DrumRoll>(0, HitResult.IgnoreHit); AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
} }
[Test] [Test]
public void TestHitNoneDrumRoll() public void TestHitNoneDrumRoll()
{ {
const double hit_time = 1000;
PerformTest(new List<ReplayFrame> PerformTest(new List<ReplayFrame>
{ {
new TaikoReplayFrame(0), new TaikoReplayFrame(0),
}, CreateBeatmap(new DrumRoll }, CreateBeatmap(createDrumRoll(false)));
{
StartTime = hit_time,
Duration = 1000
}));
AssertJudgementCount(3); AssertJudgementCount(6);
AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss); AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(1, HitResult.IgnoreMiss); AssertResult<DrumRollTick>(1, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(2, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(3, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(4, HitResult.IgnoreMiss);
AssertResult<DrumRoll>(0, HitResult.IgnoreHit); AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
} }
[Test] [Test]
public void TestHitAllStrongDrumRollWithOneKey() public void TestHitAllStrongDrumRollWithOneKey()
{ {
const double hit_time = 1000;
PerformTest(new List<ReplayFrame> PerformTest(new List<ReplayFrame>
{ {
new TaikoReplayFrame(0), new TaikoReplayFrame(0),
new TaikoReplayFrame(1000, TaikoAction.LeftCentre), new TaikoReplayFrame(1000, TaikoAction.LeftCentre),
new TaikoReplayFrame(1001), 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(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2001), new TaikoReplayFrame(2001),
}, CreateBeatmap(new DrumRoll }, CreateBeatmap(createDrumRoll(true)));
AssertJudgementCount(12);
for (int i = 0; i < 5; i++)
{ {
StartTime = hit_time, AssertResult<DrumRollTick>(i, HitResult.SmallBonus);
Duration = 1000, AssertResult<StrongNestedHitObject>(i, HitResult.LargeBonus);
IsStrong = true }
}));
AssertJudgementCount(6);
AssertResult<DrumRollTick>(0, HitResult.SmallBonus);
AssertResult<StrongNestedHitObject>(0, HitResult.LargeBonus);
AssertResult<DrumRollTick>(1, HitResult.SmallBonus);
AssertResult<StrongNestedHitObject>(1, HitResult.LargeBonus);
AssertResult<DrumRoll>(0, HitResult.IgnoreHit); AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
AssertResult<StrongNestedHitObject>(2, HitResult.IgnoreHit); AssertResult<StrongNestedHitObject>(5, HitResult.IgnoreHit);
} }
[Test] [Test]
public void TestHitSomeStrongDrumRollWithOneKey() public void TestHitSomeStrongDrumRollWithOneKey()
{ {
const double hit_time = 1000;
PerformTest(new List<ReplayFrame> PerformTest(new List<ReplayFrame>
{ {
new TaikoReplayFrame(0), new TaikoReplayFrame(0),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre), new TaikoReplayFrame(2000, TaikoAction.LeftCentre),
new TaikoReplayFrame(2001), new TaikoReplayFrame(2001),
}, CreateBeatmap(new DrumRoll }, CreateBeatmap(createDrumRoll(true)));
{
StartTime = hit_time,
Duration = 1000,
IsStrong = true
}));
AssertJudgementCount(6); AssertJudgementCount(12);
AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss); AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss);
AssertResult<StrongNestedHitObject>(0, HitResult.IgnoreMiss); AssertResult<StrongNestedHitObject>(0, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(1, HitResult.SmallBonus); AssertResult<DrumRollTick>(4, HitResult.SmallBonus);
AssertResult<StrongNestedHitObject>(1, HitResult.LargeBonus); AssertResult<StrongNestedHitObject>(4, HitResult.LargeBonus);
AssertResult<DrumRoll>(0, HitResult.IgnoreHit); AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
AssertResult<StrongNestedHitObject>(2, HitResult.IgnoreHit); AssertResult<StrongNestedHitObject>(5, HitResult.IgnoreHit);
} }
[Test] [Test]
public void TestHitAllStrongDrumRollWithBothKeys() public void TestHitAllStrongDrumRollWithBothKeys()
{ {
const double hit_time = 1000;
PerformTest(new List<ReplayFrame> PerformTest(new List<ReplayFrame>
{ {
new TaikoReplayFrame(0), new TaikoReplayFrame(0),
new TaikoReplayFrame(1000, TaikoAction.LeftCentre, TaikoAction.RightCentre), new TaikoReplayFrame(1000, TaikoAction.LeftCentre, TaikoAction.RightCentre),
new TaikoReplayFrame(1001), 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(2000, TaikoAction.LeftCentre, TaikoAction.RightCentre),
new TaikoReplayFrame(2001), new TaikoReplayFrame(2001),
}, CreateBeatmap(new DrumRoll }, CreateBeatmap(createDrumRoll(true)));
AssertJudgementCount(12);
for (int i = 0; i < 5; i++)
{ {
StartTime = hit_time, AssertResult<DrumRollTick>(i, HitResult.SmallBonus);
Duration = 1000, AssertResult<StrongNestedHitObject>(i, HitResult.LargeBonus);
IsStrong = true }
}));
AssertJudgementCount(6);
AssertResult<DrumRollTick>(0, HitResult.SmallBonus);
AssertResult<StrongNestedHitObject>(0, HitResult.LargeBonus);
AssertResult<DrumRollTick>(1, HitResult.SmallBonus);
AssertResult<StrongNestedHitObject>(1, HitResult.LargeBonus);
AssertResult<DrumRoll>(0, HitResult.IgnoreHit); AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
AssertResult<StrongNestedHitObject>(2, HitResult.IgnoreHit); AssertResult<StrongNestedHitObject>(5, HitResult.IgnoreHit);
} }
[Test] [Test]
public void TestHitSomeStrongDrumRollWithBothKeys() public void TestHitSomeStrongDrumRollWithBothKeys()
{ {
const double hit_time = 1000;
PerformTest(new List<ReplayFrame> PerformTest(new List<ReplayFrame>
{ {
new TaikoReplayFrame(0), new TaikoReplayFrame(0),
new TaikoReplayFrame(2000, TaikoAction.LeftCentre, TaikoAction.RightCentre), new TaikoReplayFrame(2000, TaikoAction.LeftCentre, TaikoAction.RightCentre),
new TaikoReplayFrame(2001), new TaikoReplayFrame(2001),
}, CreateBeatmap(new DrumRoll }, CreateBeatmap(createDrumRoll(true)));
{
StartTime = hit_time,
Duration = 1000,
IsStrong = true
}));
AssertJudgementCount(6); AssertJudgementCount(12);
AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss); AssertResult<DrumRollTick>(0, HitResult.IgnoreMiss);
AssertResult<StrongNestedHitObject>(0, HitResult.IgnoreMiss); AssertResult<StrongNestedHitObject>(0, HitResult.IgnoreMiss);
AssertResult<DrumRollTick>(1, HitResult.SmallBonus); AssertResult<DrumRollTick>(4, HitResult.SmallBonus);
AssertResult<StrongNestedHitObject>(1, HitResult.LargeBonus); AssertResult<StrongNestedHitObject>(4, HitResult.LargeBonus);
AssertResult<DrumRoll>(0, HitResult.IgnoreHit); AssertResult<DrumRoll>(0, HitResult.IgnoreHit);
AssertResult<StrongNestedHitObject>(2, HitResult.IgnoreHit); AssertResult<StrongNestedHitObject>(5, HitResult.IgnoreHit);
} }
private DrumRoll createDrumRoll(bool strong) => new DrumRoll
{
StartTime = 1000,
Duration = 1000,
IsStrong = strong
};
} }
} }

View File

@ -92,6 +92,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
}).ToList(); }).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<HitObject> 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; return converted;
} }
@ -133,7 +141,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
StartTime = obj.StartTime, StartTime = obj.StartTime,
Samples = obj.Samples, Samples = obj.Samples,
Duration = taikoDuration, Duration = taikoDuration,
TickRate = beatmap.Difficulty.SliderTickRate == 3 ? 3 : 4,
SliderVelocity = obj is IHasSliderVelocity velocityData ? velocityData.SliderVelocity : 1 SliderVelocity = obj is IHasSliderVelocity velocityData ? velocityData.SliderVelocity : 1
}; };
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using System.Threading; using System.Threading;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -69,6 +67,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
double scoringDistance = base_distance * difficulty.SliderMultiplier * SliderVelocity; double scoringDistance = base_distance * difficulty.SliderMultiplier * SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength; Velocity = scoringDistance / timingPoint.BeatLength;
TickRate = difficulty.SliderTickRate == 3 ? 3 : 4;
tickSpacing = timingPoint.BeatLength / TickRate; tickSpacing = timingPoint.BeatLength / TickRate;
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Rulesets.Taiko.Objects namespace osu.Game.Rulesets.Taiko.Objects
{ {
/// <summary> /// <summary>

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
private const double pre_beat_transition_time = 80; 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; private ColourInfo accentColour;
@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
if (drawableHitObject.State.Value == ArmedState.Idle) if (drawableHitObject.State.Value == ArmedState.Idle)
{ {
flash flash
.FadeTo(flash_opacity) .FadeTo(kiai_flash_opacity)
.Then() .Then()
.FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine); .FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine);
} }

View File

@ -1,9 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // 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;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -18,7 +16,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
public partial class ArgonHitExplosion : CompositeDrawable, IAnimatableHitExplosion public partial class ArgonHitExplosion : CompositeDrawable, IAnimatableHitExplosion
{ {
private readonly TaikoSkinComponents component; private readonly TaikoSkinComponents component;
private readonly Circle outer; private readonly Circle outer;
private readonly Circle inner;
public ArgonHitExplosion(TaikoSkinComponents component) public ArgonHitExplosion(TaikoSkinComponents component)
{ {
@ -34,13 +34,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(
new Color4(255, 227, 236, 255),
new Color4(255, 198, 211, 255)
),
Masking = true, Masking = true,
}, },
new Circle inner = new Circle
{ {
Name = "Inner circle", Name = "Inner circle",
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
@ -48,12 +44,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4.White, Colour = Color4.White,
Size = new Vector2(0.85f), 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, Masking = true,
}, },
}; };
@ -63,6 +53,16 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
{ {
this.FadeOut(); 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) switch (component)
{ {
case TaikoSkinComponents.TaikoExplosionGreat: case TaikoSkinComponents.TaikoExplosionGreat:

View File

@ -19,6 +19,20 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
{ {
public partial class ArgonInputDrum : AspectContainer 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; private const float rim_size = 0.3f;
public ArgonInputDrum() public ArgonInputDrum()
@ -141,14 +155,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
Anchor = anchor, Anchor = anchor,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal( Colour = RIM_HIT_GRADIENT,
new Color4(227, 248, 255, 255),
new Color4(198, 245, 255, 255)
),
EdgeEffect = new EdgeEffectParameters EdgeEffect = new EdgeEffectParameters
{ {
Type = EdgeEffectType.Glow, Type = EdgeEffectType.Glow,
Colour = new Color4(126, 215, 253, 170), Colour = RIM_HIT_GLOW.Opacity(0.66f),
Radius = 50, Radius = 50,
}, },
Alpha = 0, Alpha = 0,
@ -166,14 +177,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
Anchor = anchor, Anchor = anchor,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal( Colour = CENTRE_HIT_GRADIENT,
new Color4(255, 227, 236, 255),
new Color4(255, 198, 211, 255)
),
EdgeEffect = new EdgeEffectParameters EdgeEffect = new EdgeEffectParameters
{ {
Type = EdgeEffectType.Glow, Type = EdgeEffectType.Glow,
Colour = new Color4(255, 147, 199, 255), Colour = CENTRE_HIT_GLOW,
Radius = 50, Radius = 50,
}, },
Size = new Vector2(1 - rim_size), Size = new Vector2(1 - rim_size),

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
private const double pre_beat_transition_time = 80; private const double pre_beat_transition_time = 80;
private const float flash_opacity = 0.3f; private const float kiai_flash_opacity = 0.15f;
[Resolved] [Resolved]
private DrawableHitObject drawableHitObject { get; set; } = null!; private DrawableHitObject drawableHitObject { get; set; } = null!;
@ -187,7 +187,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
if (drawableHitObject.State.Value == ArmedState.Idle) if (drawableHitObject.State.Value == ArmedState.Idle)
{ {
flashBox flashBox
.FadeTo(flash_opacity) .FadeTo(kiai_flash_opacity)
.Then() .Then()
.FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine); .FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine);
} }

View File

@ -229,45 +229,27 @@ namespace osu.Game.Rulesets.Taiko
return base.GetDisplayNameForHitResult(result); 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(); var timedHitEvents = score.HitEvents.Where(e => e.HitObject is Hit).ToList();
return new[] return new[]
{ {
new StatisticRow new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
{ {
Columns = new[] RelativeSizeAxes = Axes.X,
{ AutoSizeAxes = Axes.Y
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap) }),
{ new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents)
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}),
}
},
new StatisticRow
{ {
Columns = new[] RelativeSizeAxes = Axes.X,
{ Height = 250
new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents) }, true),
{ new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
RelativeSizeAxes = Axes.X,
Height = 250
}, true),
}
},
new StatisticRow
{ {
Columns = new[] new AverageHitError(timedHitEvents),
{ new UnstableRate(timedHitEvents)
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[] }), true)
{
new AverageHitError(timedHitEvents),
new UnstableRate(timedHitEvents)
}), true)
}
}
}; };
} }
} }

View File

@ -231,7 +231,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
protected override IBeatmap GetBeatmap() => beatmap; 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 override Track GetBeatmapTrack() => throw new NotImplementedException();

View File

@ -131,7 +131,7 @@ namespace osu.Game.Tests.Editing.Checks
var mock = new Mock<IWorkingBeatmap>(); var mock = new Mock<IWorkingBeatmap>();
mock.SetupGet(w => w.Beatmap).Returns(beatmap); 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<string>())).Returns(stream); mock.Setup(w => w.GetStream(It.IsAny<string>())).Returns(stream);
return mock; return mock;

View File

@ -73,7 +73,5 @@ namespace osu.Game.Tests.Rulesets
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null; public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => null;
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null; public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null;
} }
#nullable enable
} }
} }

View File

@ -286,7 +286,7 @@ namespace osu.Game.Tests.Visual.Background
this.renderer = renderer; this.renderer = renderer;
} }
protected override Texture GetBackground() => renderer.CreateTexture(1, 1); public override Texture GetBackground() => renderer.CreateTexture(1, 1);
} }
private partial class TestWorkingBeatmapWithStoryboard : TestWorkingBeatmap private partial class TestWorkingBeatmapWithStoryboard : TestWorkingBeatmap

View File

@ -264,8 +264,9 @@ namespace osu.Game.Tests.Visual.Collections
assertCollectionName(1, "First"); assertCollectionName(1, "First");
} }
[Test] [TestCase(false)]
public void TestCollectionRenamedOnTextChange() [TestCase(true)]
public void TestCollectionRenamedOnTextChange(bool commitWithEnter)
{ {
BeatmapCollection first = null!; BeatmapCollection first = null!;
DrawableCollectionListItem firstItem = null!; DrawableCollectionListItem firstItem = null!;
@ -293,9 +294,19 @@ namespace osu.Game.Tests.Visual.Collections
AddStep("change first collection name", () => AddStep("change first collection name", () =>
{ {
firstItem.ChildrenOfType<TextBox>().First().Text = "First"; firstItem.ChildrenOfType<TextBox>().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"); AddUntilStep("collection has new name", () => first.Name == "First");
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@ -23,8 +21,8 @@ namespace osu.Game.Tests.Visual.Editing
{ {
public partial class TestSceneBeatDivisorControl : OsuManualInputManagerTestScene public partial class TestSceneBeatDivisorControl : OsuManualInputManagerTestScene
{ {
private BeatDivisorControl beatDivisorControl; private BeatDivisorControl beatDivisorControl = null!;
private BindableBeatDivisor bindableBeatDivisor; private BindableBeatDivisor bindableBeatDivisor = null!;
private SliderBar<int> tickSliderBar => beatDivisorControl.ChildrenOfType<SliderBar<int>>().Single(); private SliderBar<int> tickSliderBar => beatDivisorControl.ChildrenOfType<SliderBar<int>>().Single();
private Triangle tickMarkerHead => tickSliderBar.ChildrenOfType<Triangle>().Single(); private Triangle tickMarkerHead => tickSliderBar.ChildrenOfType<Triangle>().Single();
@ -51,9 +49,9 @@ namespace osu.Game.Tests.Visual.Editing
[Test] [Test]
public void TestBindableBeatDivisor() public void TestBindableBeatDivisor()
{ {
AddRepeatStep("move previous", () => bindableBeatDivisor.Previous(), 2); AddRepeatStep("move previous", () => bindableBeatDivisor.SelectPrevious(), 2);
AddAssert("divisor is 4", () => bindableBeatDivisor.Value == 4); 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); AddAssert("divisor is 12", () => bindableBeatDivisor.Value == 8);
} }
@ -101,16 +99,22 @@ namespace osu.Game.Tests.Visual.Editing
public void TestBeatChevronNavigation() public void TestBeatChevronNavigation()
{ {
switchBeatSnap(1); switchBeatSnap(1);
assertBeatSnap(16);
switchBeatSnap(-4);
assertBeatSnap(1); assertBeatSnap(1);
switchBeatSnap(3); switchBeatSnap(3);
assertBeatSnap(8); assertBeatSnap(8);
switchBeatSnap(-1); switchBeatSnap(3);
assertBeatSnap(16);
switchBeatSnap(-2);
assertBeatSnap(4); assertBeatSnap(4);
switchBeatSnap(-3); switchBeatSnap(-3);
assertBeatSnap(16); assertBeatSnap(1);
} }
[Test] [Test]
@ -163,9 +167,11 @@ namespace osu.Game.Tests.Visual.Editing
switchPresets(1); switchPresets(1);
assertPreset(BeatDivisorType.Triplets); assertPreset(BeatDivisorType.Triplets);
assertBeatSnap(6);
switchPresets(1); switchPresets(1);
assertPreset(BeatDivisorType.Common); assertPreset(BeatDivisorType.Common);
assertBeatSnap(4);
switchPresets(-1); switchPresets(-1);
assertPreset(BeatDivisorType.Triplets); assertPreset(BeatDivisorType.Triplets);
@ -181,6 +187,7 @@ namespace osu.Game.Tests.Visual.Editing
setDivisorViaInput(15); setDivisorViaInput(15);
assertPreset(BeatDivisorType.Custom, 15); assertPreset(BeatDivisorType.Custom, 15);
assertBeatSnap(15);
switchBeatSnap(-1); switchBeatSnap(-1);
assertBeatSnap(5); assertBeatSnap(5);
@ -190,12 +197,14 @@ namespace osu.Game.Tests.Visual.Editing
setDivisorViaInput(5); setDivisorViaInput(5);
assertPreset(BeatDivisorType.Custom, 15); assertPreset(BeatDivisorType.Custom, 15);
assertBeatSnap(5);
switchPresets(1); switchPresets(1);
assertPreset(BeatDivisorType.Common); assertPreset(BeatDivisorType.Common);
switchPresets(-1); switchPresets(-1);
assertPreset(BeatDivisorType.Triplets); assertPreset(BeatDivisorType.Custom, 15);
assertBeatSnap(15);
} }
private void switchBeatSnap(int direction) => AddRepeatStep($"move snap {(direction > 0 ? "forward" : "backward")}", () => 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)); }, Math.Abs(direction));
private void assertBeatSnap(int expected) => AddAssert($"beat snap is {expected}", 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")}", () => 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) 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) if (type == BeatDivisorType.Custom)
{ {
@ -237,7 +246,7 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
BeatDivisorControl.CustomDivisorPopover popover = null; BeatDivisorControl.CustomDivisorPopover? popover = null;
AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType<BeatDivisorControl.CustomDivisorPopover>().SingleOrDefault()) != null && popover.IsLoaded); AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType<BeatDivisorControl.CustomDivisorPopover>().SingleOrDefault()) != null && popover.IsLoaded);
AddStep($"set divisor to {divisor}", () => AddStep($"set divisor to {divisor}", () =>
{ {

View File

@ -209,10 +209,14 @@ namespace osu.Game.Tests.Visual.Editing
public override void TearDownSteps() public override void TearDownSteps()
{ {
base.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<BeatmapMetadata>();
r.RemoveAll<BeatmapInfo>();
r.RemoveAll<BeatmapSetInfo>();
}));
} }
} }
} }

View File

@ -8,6 +8,8 @@ using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Tests.Visual.Ranking; using osu.Game.Tests.Visual.Ranking;
@ -49,6 +51,21 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any()); AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().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<SettingsButton>().Any());
}
[Test] [Test]
public void TestCalibrationFromZero() public void TestCalibrationFromZero()
{ {

View File

@ -129,7 +129,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
Playlist = Playlist =
{ {
new MultiplayerPlaylistItem(playlistItem), TestMultiplayerClient.CreateMultiplayerPlaylistItem(playlistItem),
}, },
Users = { localUser }, Users = { localUser },
Host = localUser, Host = localUser,

View File

@ -906,7 +906,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
enterGameplay(); enterGameplay();
AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 })); 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) new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{ {
RulesetID = new OsuRuleset().RulesetInfo.OnlineID, RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
@ -938,7 +938,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
enterGameplay(); enterGameplay();
AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 })); 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) new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{ {
RulesetID = new OsuRuleset().RulesetInfo.OnlineID, RulesetID = new OsuRuleset().RulesetInfo.OnlineID,

View File

@ -215,7 +215,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
/// </summary> /// </summary>
private void addItemStep(bool expired = false, int? userId = null) => AddStep("add item", () => 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, Expired = expired,
PlayedAt = DateTimeOffset.Now PlayedAt = DateTimeOffset.Now

View File

@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("add playlist item", () => AddStep("add playlist item", () =>
{ {
MultiplayerPlaylistItem item = new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)); MultiplayerPlaylistItem item = TestMultiplayerClient.CreateMultiplayerPlaylistItem(new PlaylistItem(importedBeatmap));
MultiplayerClient.AddUserPlaylistItem(userId(), item).WaitSafely(); MultiplayerClient.AddUserPlaylistItem(userId(), item).WaitSafely();

View File

@ -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))); 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); 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", () => 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); AddUntilStep("wait for editor exit", () => Game.ScreenStack.CurrentScreen is not Editor);
} }
private EditorBeatmap getEditorBeatmap() => ((Editor)Game.ScreenStack.CurrentScreen).ChildrenOfType<EditorBeatmap>().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<EditorClock>().First().Seek(1234));
AddUntilStep("time is correct", () => getEditor().ChildrenOfType<EditorClock>().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<EditorClock>().First().CurrentTime, () => Is.EqualTo(1234));
}
private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType<EditorBeatmap>().Single();
private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen;
} }
} }

View File

@ -17,6 +17,7 @@ using osu.Game.Beatmaps;
using osu.Game.Collections; using osu.Game.Collections;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Leaderboards; using osu.Game.Online.Leaderboards;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.BeatmapListing;
@ -539,6 +540,11 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("open room", () => multiplayerComponents.ChildrenOfType<LoungeSubScreen>().Single().Open()); AddStep("open room", () => multiplayerComponents.ChildrenOfType<LoungeSubScreen>().Single().Open());
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action()); AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
AddWaitStep("wait two frames", 2); 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] [Test]

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Linq; using System.Linq;
using System.Threading; 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 readonly APIUser streamingUser = new APIUser { Id = 2, Username = "Test user" };
private TestSpectatorClient spectatorClient; private TestSpectatorClient spectatorClient = null!;
private CurrentlyPlayingDisplay currentlyPlaying; private CurrentlyPlayingDisplay currentlyPlaying = null!;
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
@ -88,13 +86,13 @@ namespace osu.Game.Tests.Visual.Online
"pishifat" "pishifat"
}; };
protected override Task<APIUser> ComputeValueAsync(int lookup, CancellationToken token = default) protected override Task<APIUser?> ComputeValueAsync(int lookup, CancellationToken token = default)
{ {
// tests against failed lookups // tests against failed lookups
if (lookup == 13) if (lookup == 13)
return Task.FromResult<APIUser>(null); return Task.FromResult<APIUser?>(null);
return Task.FromResult(new APIUser return Task.FromResult<APIUser?>(new APIUser
{ {
Id = lookup, Id = lookup,
Username = usernames[lookup % usernames.Length], Username = usernames[lookup % usernames.Length],

View File

@ -174,78 +174,33 @@ namespace osu.Game.Tests.Visual.Ranking
private class TestRulesetAllStatsRequireHitEvents : TestRuleset private class TestRulesetAllStatsRequireHitEvents : TestRuleset
{ {
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
{ {
return new[] 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)
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)
}
}
};
}
} }
private class TestRulesetNoStatsRequireHitEvents : TestRuleset private class TestRulesetNoStatsRequireHitEvents : TestRuleset
{ {
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{ {
return new[] return new[]
{ {
new StatisticRow 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"))
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"))
}
}
}; };
} }
} }
private class TestRulesetMixed : TestRuleset private class TestRulesetMixed : TestRuleset
{ {
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{ {
return new[] return new[]
{ {
new StatisticRow 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"))
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"))
}
}
}; };
} }
} }

View File

@ -1,10 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -24,10 +23,10 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
public partial class TestSceneBeatmapMetadataDisplay : OsuTestScene public partial class TestSceneBeatmapMetadataDisplay : OsuTestScene
{ {
private BeatmapMetadataDisplay display; private BeatmapMetadataDisplay display = null!;
[Resolved] [Resolved]
private BeatmapManager manager { get; set; } private BeatmapManager manager { get; set; } = null!;
[Cached(typeof(BeatmapDifficultyCache))] [Cached(typeof(BeatmapDifficultyCache))]
private readonly TestBeatmapDifficultyCache testDifficultyCache = new TestBeatmapDifficultyCache(); private readonly TestBeatmapDifficultyCache testDifficultyCache = new TestBeatmapDifficultyCache();
@ -121,7 +120,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private partial class TestBeatmapDifficultyCache : BeatmapDifficultyCache private partial class TestBeatmapDifficultyCache : BeatmapDifficultyCache
{ {
private TaskCompletionSource<bool> calculationBlocker; private TaskCompletionSource<bool>? calculationBlocker;
private bool blockCalculation; private bool blockCalculation;
@ -142,10 +141,13 @@ namespace osu.Game.Tests.Visual.SongSelect
} }
} }
public override async Task<StarDifficulty?> GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo rulesetInfo = null, IEnumerable<Mod> mods = null, CancellationToken cancellationToken = default) public override async Task<StarDifficulty?> GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo? rulesetInfo = null, IEnumerable<Mod>? mods = null, CancellationToken cancellationToken = default)
{ {
if (blockCalculation) if (blockCalculation)
{
Debug.Assert(calculationBlocker != null);
await calculationBlocker.Task.ConfigureAwait(false); await calculationBlocker.Task.ConfigureAwait(false);
}
return await base.GetDifficultyAsync(beatmapInfo, rulesetInfo, mods, cancellationToken).ConfigureAwait(false); return await base.GetDifficultyAsync(beatmapInfo, rulesetInfo, mods, cancellationToken).ConfigureAwait(false);
} }

View File

@ -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] [Test]
public void TestSelection() public void TestSelection()
{ {
@ -196,6 +214,33 @@ namespace osu.Game.Tests.Visual.UserInterface
InputManager.ScrollVerticalBy(direction); 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 private partial class TestSection : TestBox
{ {
public bool Selected public bool Selected

View File

@ -51,7 +51,7 @@ namespace osu.Game.Tests
protected override IBeatmap GetBeatmap() => beatmap; protected override IBeatmap GetBeatmap() => beatmap;
protected override Texture GetBackground() => null; public override Texture GetBackground() => null;
protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile)); protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile));

View File

@ -24,6 +24,9 @@ namespace osu.Game.Tournament.Tests.Screens
Add(screen = new MapPoolScreen { Width = 0.7f }); Add(screen = new MapPoolScreen { Width = 0.7f });
} }
[SetUp]
public void SetUp() => Schedule(() => Ladder.SplitMapPoolByMods.Value = true);
[Test] [Test]
public void TestFewMaps() public void TestFewMaps()
{ {
@ -92,7 +95,7 @@ namespace osu.Game.Tournament.Tests.Screens
Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear(); Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear();
for (int i = 0; i < 11; i++) 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", () => AddStep("reset match", () =>
@ -118,7 +121,7 @@ namespace osu.Game.Tournament.Tests.Screens
Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear(); Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear();
for (int i = 0; i < 12; i++) 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", () => AddStep("reset match", () =>
@ -130,7 +133,27 @@ namespace osu.Game.Tournament.Tests.Screens
assertThreeWide(); 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 Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Add(new RoundBeatmap
{ {

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Tournament.IPC namespace osu.Game.Tournament.IPC
{ {
public enum TourneyState public enum TourneyState

View File

@ -42,5 +42,7 @@ namespace osu.Game.Tournament.Models
}; };
public Bindable<bool> AutoProgressScreens = new BindableBool(true); public Bindable<bool> AutoProgressScreens = new BindableBool(true);
public Bindable<bool> SplitMapPoolByMods = new BindableBool(true);
} }
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
namespace osu.Game.Tournament.Screens.Ladder.Components namespace osu.Game.Tournament.Screens.Ladder.Components

View File

@ -24,7 +24,7 @@ namespace osu.Game.Tournament.Screens.MapPool
{ {
public partial class MapPoolScreen : TournamentMatchScreen public partial class MapPoolScreen : TournamentMatchScreen
{ {
private readonly FillFlowContainer<FillFlowContainer<TournamentBeatmapPanel>> mapFlows; private FillFlowContainer<FillFlowContainer<TournamentBeatmapPanel>> mapFlows;
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private TournamentSceneManager sceneManager { get; set; } private TournamentSceneManager sceneManager { get; set; }
@ -32,12 +32,13 @@ namespace osu.Game.Tournament.Screens.MapPool
private TeamColour pickColour; private TeamColour pickColour;
private ChoiceType pickType; private ChoiceType pickType;
private readonly OsuButton buttonRedBan; private OsuButton buttonRedBan;
private readonly OsuButton buttonBlueBan; private OsuButton buttonBlueBan;
private readonly OsuButton buttonRedPick; private OsuButton buttonRedPick;
private readonly OsuButton buttonBluePick; private OsuButton buttonBluePick;
public MapPoolScreen() [BackgroundDependencyLoader]
private void load(MatchIPCInfo ipc)
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
@ -98,15 +99,26 @@ namespace osu.Game.Tournament.Screens.MapPool
Action = reset Action = reset
}, },
new ControlPanel.Spacer(), new ControlPanel.Spacer(),
new OsuCheckbox
{
LabelText = "Split display by mods",
Current = LadderInfo.SplitMapPoolByMods,
},
}, },
} }
}; };
ipc.Beatmap.BindValueChanged(beatmapChanged);
} }
[BackgroundDependencyLoader] private Bindable<bool> splitMapPoolByMods;
private void load(MatchIPCInfo ipc)
protected override void LoadComplete()
{ {
ipc.Beatmap.BindValueChanged(beatmapChanged); base.LoadComplete();
splitMapPoolByMods = LadderInfo.SplitMapPoolByMods.GetBoundCopy();
splitMapPoolByMods.BindValueChanged(_ => updateDisplay());
} }
private void beatmapChanged(ValueChangedEvent<TournamentBeatmap> beatmap) private void beatmapChanged(ValueChangedEvent<TournamentBeatmap> beatmap)
@ -213,24 +225,27 @@ namespace osu.Game.Tournament.Screens.MapPool
protected override void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match) protected override void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match)
{ {
base.CurrentMatchChanged(match); base.CurrentMatchChanged(match);
updateDisplay();
}
private void updateDisplay()
{
mapFlows.Clear(); mapFlows.Clear();
if (match.NewValue == null) if (CurrentMatch.Value == null)
return; return;
int totalRows = 0; int totalRows = 0;
if (match.NewValue.Round.Value != null) if (CurrentMatch.Value.Round.Value != null)
{ {
FillFlowContainer<TournamentBeatmapPanel> currentFlow = null; FillFlowContainer<TournamentBeatmapPanel> currentFlow = null;
string currentMod = null; string currentMods = null;
int flowCount = 0; 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<TournamentBeatmapPanel> mapFlows.Add(currentFlow = new FillFlowContainer<TournamentBeatmapPanel>
{ {
@ -240,7 +255,7 @@ namespace osu.Game.Tournament.Screens.MapPool
AutoSizeAxes = Axes.Y AutoSizeAxes = Axes.Y
}); });
currentMod = b.Mods; currentMods = b.Mods;
totalRows++; totalRows++;
flowCount = 0; flowCount = 0;

View File

@ -171,6 +171,11 @@ namespace osu.Game.Beatmaps
public double TimelineZoom { get; set; } = 1.0; public double TimelineZoom { get; set; } = 1.0;
/// <summary>
/// The time in milliseconds when last exiting the editor with this beatmap loaded.
/// </summary>
public double? EditorTimestamp { get; set; }
[Ignored] [Ignored]
public CountdownType Countdown { get; set; } = CountdownType.Normal; public CountdownType Countdown { get; set; } = CountdownType.Normal;

View File

@ -0,0 +1,95 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<TextureUpload>
{
// The aspect ratio of SetPanelBackground at its maximum size (very tall window).
private const float minimum_display_ratio = 512 / 80f;
private readonly IResourceStore<TextureUpload>? textureStore;
public BeatmapPanelBackgroundTextureLoaderStore(IResourceStore<TextureUpload>? 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<TextureUpload> 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<string> GetAvailableResources() => textureStore?.GetAvailableResources() ?? Array.Empty<string>();
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
public enum DifficultyRating public enum DifficultyRating

View File

@ -23,8 +23,9 @@ namespace osu.Game.Beatmaps.Drawables
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
if (working.Background != null) var background = working.GetBackground();
Texture = working.Background; if (background != null)
Texture = background;
} }
} }
} }

View File

@ -106,12 +106,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
new Drawable[] new Drawable[]
{ {
new OsuSpriteText new TruncatingSpriteText
{ {
Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title), Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title),
Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold), Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Truncate = true
}, },
titleBadgeArea = new FillFlowContainer titleBadgeArea = new FillFlowContainer
{ {
@ -140,21 +139,19 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
new[] new[]
{ {
new OsuSpriteText new TruncatingSpriteText
{ {
Text = createArtistText(), Text = createArtistText(),
Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold), Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Truncate = true
}, },
Empty() Empty()
}, },
} }
}, },
new OsuSpriteText new TruncatingSpriteText
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Truncate = true,
Text = BeatmapSet.Source, Text = BeatmapSet.Source,
Shadow = false, Shadow = false,
Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold),

View File

@ -107,12 +107,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
new Drawable[] new Drawable[]
{ {
new OsuSpriteText new TruncatingSpriteText
{ {
Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title), Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title),
Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold), Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Truncate = true
}, },
titleBadgeArea = new FillFlowContainer titleBadgeArea = new FillFlowContainer
{ {
@ -141,12 +140,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
new[] new[]
{ {
new OsuSpriteText new TruncatingSpriteText
{ {
Text = createArtistText(), Text = createArtistText(),
Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold), Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Truncate = true
}, },
Empty() Empty()
}, },

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Beatmaps.Drawables.Cards namespace osu.Game.Beatmaps.Drawables.Cards
{ {
/// <summary> /// <summary>

View File

@ -52,7 +52,7 @@ namespace osu.Game.Beatmaps
protected override IBeatmap GetBeatmap() => new Beatmap(); 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(); protected override Track GetBeatmapTrack() => GetVirtualTrack();

View File

@ -43,7 +43,7 @@ namespace osu.Game.Beatmaps
} }
protected override IBeatmap GetBeatmap() => beatmap; 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 override Track GetBeatmapTrack() => throw new NotImplementedException();
protected internal override ISkin GetSkin() => throw new NotImplementedException(); protected internal override ISkin GetSkin() => throw new NotImplementedException();
public override Stream GetStream(string storagePath) => throw new NotImplementedException(); public override Stream GetStream(string storagePath) => throw new NotImplementedException();

View File

@ -9,13 +9,18 @@ using osu.Game.IO;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
public interface IBeatmapResourceProvider : IStorageResourceProvider internal interface IBeatmapResourceProvider : IStorageResourceProvider
{ {
/// <summary> /// <summary>
/// Retrieve a global large texture store, used for loading beatmap backgrounds. /// Retrieve a global large texture store, used for loading beatmap backgrounds.
/// </summary> /// </summary>
TextureStore LargeTextureStore { get; } TextureStore LargeTextureStore { get; }
/// <summary>
/// Retrieve a global large texture store, used specifically for retrieving cropped beatmap panel backgrounds.
/// </summary>
TextureStore BeatmapPanelTextureStore { get; }
/// <summary> /// <summary>
/// Access a global track store for retrieving beatmap tracks from. /// Access a global track store for retrieving beatmap tracks from.
/// </summary> /// </summary>

View File

@ -32,12 +32,12 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// Whether the Beatmap has finished loading. /// Whether the Beatmap has finished loading.
///</summary> ///</summary>
public bool BeatmapLoaded { get; } bool BeatmapLoaded { get; }
/// <summary> /// <summary>
/// Whether the Track has finished loading. /// Whether the Track has finished loading.
///</summary> ///</summary>
public bool TrackLoaded { get; } bool TrackLoaded { get; }
/// <summary> /// <summary>
/// Retrieves the <see cref="IBeatmap"/> which this <see cref="IWorkingBeatmap"/> represents. /// Retrieves the <see cref="IBeatmap"/> which this <see cref="IWorkingBeatmap"/> represents.
@ -47,7 +47,12 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// Retrieves the background for this <see cref="IWorkingBeatmap"/>. /// Retrieves the background for this <see cref="IWorkingBeatmap"/>.
/// </summary> /// </summary>
Texture Background { get; } Texture GetBackground();
/// <summary>
/// Retrieves a cropped background for this <see cref="IWorkingBeatmap"/> used for display on panels.
/// </summary>
Texture GetPanelBackground();
/// <summary> /// <summary>
/// Retrieves the <see cref="Waveform"/> for the <see cref="Track"/> of this <see cref="IWorkingBeatmap"/>. /// Retrieves the <see cref="Waveform"/> for the <see cref="Track"/> of this <see cref="IWorkingBeatmap"/>.
@ -124,12 +129,12 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// Beings loading the contents of this <see cref="IWorkingBeatmap"/> asynchronously. /// Beings loading the contents of this <see cref="IWorkingBeatmap"/> asynchronously.
/// </summary> /// </summary>
public void BeginAsyncLoad(); void BeginAsyncLoad();
/// <summary> /// <summary>
/// Cancels the asynchronous loading of the contents of this <see cref="IWorkingBeatmap"/>. /// Cancels the asynchronous loading of the contents of this <see cref="IWorkingBeatmap"/>.
/// </summary> /// </summary>
public void CancelAsyncLoad(); void CancelAsyncLoad();
/// <summary> /// <summary>
/// Reads the correct track restart point from beatmap metadata and sets looping to enabled. /// Reads the correct track restart point from beatmap metadata and sets looping to enabled.

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Beatmaps.Legacy namespace osu.Game.Beatmaps.Legacy
{ {
internal enum LegacyOrigins internal enum LegacyOrigins

View File

@ -34,8 +34,6 @@ namespace osu.Game.Beatmaps
public Storyboard Storyboard => storyboard.Value; 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; public ISkin Skin => skin.Value;
private AudioManager audioManager { get; } private AudioManager audioManager { get; }
@ -67,7 +65,8 @@ namespace osu.Game.Beatmaps
protected virtual Storyboard GetStoryboard() => new Storyboard { BeatmapInfo = BeatmapInfo }; protected virtual Storyboard GetStoryboard() => new Storyboard { BeatmapInfo = BeatmapInfo };
protected abstract IBeatmap GetBeatmap(); protected abstract IBeatmap GetBeatmap();
protected abstract Texture GetBackground(); public abstract Texture GetBackground();
public virtual Texture GetPanelBackground() => GetBackground();
protected abstract Track GetBeatmapTrack(); protected abstract Track GetBeatmapTrack();
/// <summary> /// <summary>

View File

@ -42,6 +42,7 @@ namespace osu.Game.Beatmaps
private readonly AudioManager audioManager; private readonly AudioManager audioManager;
private readonly IResourceStore<byte[]> resources; private readonly IResourceStore<byte[]> resources;
private readonly LargeTextureStore largeTextureStore; private readonly LargeTextureStore largeTextureStore;
private readonly LargeTextureStore beatmapPanelTextureStore;
private readonly ITrackStore trackStore; private readonly ITrackStore trackStore;
private readonly IResourceStore<byte[]> files; private readonly IResourceStore<byte[]> files;
@ -58,6 +59,7 @@ namespace osu.Game.Beatmaps
this.host = host; this.host = host;
this.files = files; this.files = files;
largeTextureStore = new LargeTextureStore(host?.Renderer ?? new DummyRenderer(), host?.CreateTextureLoaderStore(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; this.trackStore = trackStore;
} }
@ -110,6 +112,7 @@ namespace osu.Game.Beatmaps
#region IResourceStorageProvider #region IResourceStorageProvider
TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore; TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore;
TextureStore IBeatmapResourceProvider.BeatmapPanelTextureStore => beatmapPanelTextureStore;
ITrackStore IBeatmapResourceProvider.Tracks => trackStore; ITrackStore IBeatmapResourceProvider.Tracks => trackStore;
IRenderer IStorageResourceProvider.Renderer => host?.Renderer ?? new DummyRenderer(); IRenderer IStorageResourceProvider.Renderer => host?.Renderer ?? new DummyRenderer();
AudioManager IStorageResourceProvider.AudioManager => audioManager; 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)) if (string.IsNullOrEmpty(Metadata?.BackgroundFile))
return null; return null;
@ -168,7 +175,7 @@ namespace osu.Game.Beatmaps
try try
{ {
string fileStorePath = BeatmapSetInfo.GetPathForFile(Metadata.BackgroundFile); string fileStorePath = BeatmapSetInfo.GetPathForFile(Metadata.BackgroundFile);
var texture = resources.LargeTextureStore.Get(fileStorePath); var texture = store.Get(fileStorePath);
if (texture == null) if (texture == null)
{ {
@ -257,7 +264,7 @@ namespace osu.Game.Beatmaps
if (beatmapFileStream == null) if (beatmapFileStream == null)
{ {
Logger.Log($"Beatmap failed to load (file {BeatmapInfo.Path} not found on disk at expected location {fileStorePath})", level: LogLevel.Error); 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)) using (var reader = new LineBufferedReader(beatmapFileStream))

View File

@ -86,6 +86,7 @@ namespace osu.Game.Collections
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Size = Vector2.One, Size = Vector2.One,
CornerRadius = item_height / 2, CornerRadius = item_height / 2,
CommitOnFocusLost = true,
PlaceholderText = collection.IsManaged ? string.Empty : "Create a new collection" PlaceholderText = collection.IsManaged ? string.Empty : "Create a new collection"
}, },
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum IntroSequence public enum IntroSequence

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum ReleaseStream public enum ReleaseStream

View File

@ -6,6 +6,7 @@
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
@ -21,6 +22,7 @@ namespace osu.Game.Configuration
SetDefault(Static.LowBatteryNotificationShownOnce, false); SetDefault(Static.LowBatteryNotificationShownOnce, false);
SetDefault(Static.FeaturedArtistDisclaimerShownOnce, false); SetDefault(Static.FeaturedArtistDisclaimerShownOnce, false);
SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null); SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null);
SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null);
SetDefault<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null); SetDefault<APISeasonalBackgrounds>(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 <see cref="SettingsOverlay"/>. /// Used to debounce hover sounds game-wide to avoid volume saturation, especially in scrolling views with many UI controls like <see cref="SettingsOverlay"/>.
/// </summary> /// </summary>
LastHoverSoundPlaybackTime, LastHoverSoundPlaybackTime,
/// <summary>
/// The last playback time in milliseconds of an on/off sample (from <see cref="ModSelectPanel"/>).
/// Used to debounce <see cref="ModSelectPanel"/> on/off sounds game-wide to avoid volume saturation, especially in activating mod presets with many mods.
/// </summary>
LastModSelectPanelSamplePlaybackTime
} }
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum ToolbarClockDisplayMode public enum ToolbarClockDisplayMode

View File

@ -1,13 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
@ -21,8 +18,7 @@ namespace osu.Game.Database
/// <param name="beatmapId">The beatmap to lookup.</param> /// <param name="beatmapId">The beatmap to lookup.</param>
/// <param name="token">An optional cancellation token.</param> /// <param name="token">An optional cancellation token.</param>
/// <returns>The populated beatmap, or null if the beatmap does not exist or the request could not be satisfied.</returns> /// <returns>The populated beatmap, or null if the beatmap does not exist or the request could not be satisfied.</returns>
[ItemCanBeNull] public Task<APIBeatmap?> GetBeatmapAsync(int beatmapId, CancellationToken token = default) => LookupAsync(beatmapId, token);
public Task<APIBeatmap> GetBeatmapAsync(int beatmapId, CancellationToken token = default) => LookupAsync(beatmapId, token);
/// <summary> /// <summary>
/// Perform an API lookup on the specified beatmaps, populating a <see cref="APIBeatmap"/> model. /// Perform an API lookup on the specified beatmaps, populating a <see cref="APIBeatmap"/> model.
@ -30,10 +26,10 @@ namespace osu.Game.Database
/// <param name="beatmapIds">The beatmaps to lookup.</param> /// <param name="beatmapIds">The beatmaps to lookup.</param>
/// <param name="token">An optional cancellation token.</param> /// <param name="token">An optional cancellation token.</param>
/// <returns>The populated beatmaps. May include null results for failed retrievals.</returns> /// <returns>The populated beatmaps. May include null results for failed retrievals.</returns>
public Task<APIBeatmap[]> GetBeatmapsAsync(int[] beatmapIds, CancellationToken token = default) => LookupAsync(beatmapIds, token); public Task<APIBeatmap?[]> GetBeatmapsAsync(int[] beatmapIds, CancellationToken token = default) => LookupAsync(beatmapIds, token);
protected override GetBeatmapsRequest CreateRequest(IEnumerable<int> ids) => new GetBeatmapsRequest(ids.ToArray()); protected override GetBeatmapsRequest CreateRequest(IEnumerable<int> ids) => new GetBeatmapsRequest(ids.ToArray());
protected override IEnumerable<APIBeatmap> RetrieveResults(GetBeatmapsRequest request) => request.Response?.Beatmaps; protected override IEnumerable<APIBeatmap>? RetrieveResults(GetBeatmapsRequest request) => request.Response?.Beatmaps;
} }
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Database namespace osu.Game.Database
{ {
/// <summary> /// <summary>

View File

@ -1,13 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Statistics; using osu.Framework.Statistics;
@ -19,8 +17,9 @@ namespace osu.Game.Database
/// Currently not persisted between game sessions. /// Currently not persisted between game sessions.
/// </summary> /// </summary>
public abstract partial class MemoryCachingComponent<TLookup, TValue> : Component public abstract partial class MemoryCachingComponent<TLookup, TValue> : Component
where TLookup : notnull
{ {
private readonly ConcurrentDictionary<TLookup, TValue> cache = new ConcurrentDictionary<TLookup, TValue>(); private readonly ConcurrentDictionary<TLookup, TValue?> cache = new ConcurrentDictionary<TLookup, TValue?>();
private readonly GlobalStatistic<MemoryCachingStatistics> statistics; private readonly GlobalStatistic<MemoryCachingStatistics> statistics;
@ -37,12 +36,12 @@ namespace osu.Game.Database
/// </summary> /// </summary>
/// <param name="lookup">The lookup to retrieve.</param> /// <param name="lookup">The lookup to retrieve.</param>
/// <param name="token">An optional <see cref="CancellationToken"/> to cancel the operation.</param> /// <param name="token">An optional <see cref="CancellationToken"/> to cancel the operation.</param>
protected async Task<TValue> GetAsync([NotNull] TLookup lookup, CancellationToken token = default) protected async Task<TValue?> GetAsync(TLookup lookup, CancellationToken token = default)
{ {
if (CheckExists(lookup, out TValue performance)) if (CheckExists(lookup, out TValue? existing))
{ {
statistics.Value.HitCount++; statistics.Value.HitCount++;
return performance; return existing;
} }
var computed = await ComputeValueAsync(lookup, token).ConfigureAwait(false); var computed = await ComputeValueAsync(lookup, token).ConfigureAwait(false);
@ -73,7 +72,7 @@ namespace osu.Game.Database
statistics.Value.Usage = cache.Count; 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); cache.TryGetValue(lookup, out value);
/// <summary> /// <summary>
@ -82,7 +81,7 @@ namespace osu.Game.Database
/// <param name="lookup">The lookup to retrieve.</param> /// <param name="lookup">The lookup to retrieve.</param>
/// <param name="token">An optional <see cref="CancellationToken"/> to cancel the operation.</param> /// <param name="token">An optional <see cref="CancellationToken"/> to cancel the operation.</param>
/// <returns>The computed value.</returns> /// <returns>The computed value.</returns>
protected abstract Task<TValue> ComputeValueAsync(TLookup lookup, CancellationToken token = default); protected abstract Task<TValue?> ComputeValueAsync(TLookup lookup, CancellationToken token = default);
private class MemoryCachingStatistics private class MemoryCachingStatistics
{ {

View File

@ -1,14 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Game.Online.API; using osu.Game.Online.API;
@ -21,7 +18,7 @@ namespace osu.Game.Database
where TRequest : APIRequest where TRequest : APIRequest
{ {
[Resolved] [Resolved]
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; } = null!;
/// <summary> /// <summary>
/// Creates an <see cref="APIRequest"/> to retrieve the values for a given collection of <typeparamref name="TLookup"/>s. /// Creates an <see cref="APIRequest"/> to retrieve the values for a given collection of <typeparamref name="TLookup"/>s.
@ -32,8 +29,7 @@ namespace osu.Game.Database
/// <summary> /// <summary>
/// Retrieves a list of <typeparamref name="TValue"/>s from a successful <typeparamref name="TRequest"/> created by <see cref="CreateRequest"/>. /// Retrieves a list of <typeparamref name="TValue"/>s from a successful <typeparamref name="TRequest"/> created by <see cref="CreateRequest"/>.
/// </summary> /// </summary>
[CanBeNull] protected abstract IEnumerable<TValue>? RetrieveResults(TRequest request);
protected abstract IEnumerable<TValue> RetrieveResults(TRequest request);
/// <summary> /// <summary>
/// Perform a lookup using the specified <paramref name="id"/>, populating a <typeparamref name="TValue"/>. /// Perform a lookup using the specified <paramref name="id"/>, populating a <typeparamref name="TValue"/>.
@ -41,8 +37,7 @@ namespace osu.Game.Database
/// <param name="id">The ID to lookup.</param> /// <param name="id">The ID to lookup.</param>
/// <param name="token">An optional cancellation token.</param> /// <param name="token">An optional cancellation token.</param>
/// <returns>The populated <typeparamref name="TValue"/>, or null if the value does not exist or the request could not be satisfied.</returns> /// <returns>The populated <typeparamref name="TValue"/>, or null if the value does not exist or the request could not be satisfied.</returns>
[ItemCanBeNull] protected Task<TValue?> LookupAsync(TLookup id, CancellationToken token = default) => GetAsync(id, token);
protected Task<TValue> LookupAsync(TLookup id, CancellationToken token = default) => GetAsync(id, token);
/// <summary> /// <summary>
/// Perform an API lookup on the specified <paramref name="ids"/>, populating a <typeparamref name="TValue"/>. /// Perform an API lookup on the specified <paramref name="ids"/>, populating a <typeparamref name="TValue"/>.
@ -50,9 +45,9 @@ namespace osu.Game.Database
/// <param name="ids">The IDs to lookup.</param> /// <param name="ids">The IDs to lookup.</param>
/// <param name="token">An optional cancellation token.</param> /// <param name="token">An optional cancellation token.</param>
/// <returns>The populated values. May include null results for failed retrievals.</returns> /// <returns>The populated values. May include null results for failed retrievals.</returns>
protected Task<TValue[]> LookupAsync(TLookup[] ids, CancellationToken token = default) protected Task<TValue?[]> LookupAsync(TLookup[] ids, CancellationToken token = default)
{ {
var lookupTasks = new List<Task<TValue>>(); var lookupTasks = new List<Task<TValue?>>();
foreach (var id in ids) foreach (var id in ids)
{ {
@ -69,18 +64,18 @@ namespace osu.Game.Database
} }
// cannot be sealed due to test usages (see TestUserLookupCache). // cannot be sealed due to test usages (see TestUserLookupCache).
protected override async Task<TValue> ComputeValueAsync(TLookup lookup, CancellationToken token = default) protected override async Task<TValue?> ComputeValueAsync(TLookup lookup, CancellationToken token = default)
=> await queryValue(lookup).ConfigureAwait(false); => await queryValue(lookup).ConfigureAwait(false);
private readonly Queue<(TLookup id, TaskCompletionSource<TValue>)> pendingTasks = new Queue<(TLookup, TaskCompletionSource<TValue>)>(); private readonly Queue<(TLookup id, TaskCompletionSource<TValue?>)> pendingTasks = new Queue<(TLookup, TaskCompletionSource<TValue?>)>();
private Task pendingRequestTask; private Task? pendingRequestTask;
private readonly object taskAssignmentLock = new object(); private readonly object taskAssignmentLock = new object();
private Task<TValue> queryValue(TLookup id) private Task<TValue?> queryValue(TLookup id)
{ {
lock (taskAssignmentLock) lock (taskAssignmentLock)
{ {
var tcs = new TaskCompletionSource<TValue>(); var tcs = new TaskCompletionSource<TValue?>();
// Add to the queue. // Add to the queue.
pendingTasks.Enqueue((id, tcs)); pendingTasks.Enqueue((id, tcs));
@ -96,14 +91,14 @@ namespace osu.Game.Database
private async Task performLookup() private async Task performLookup()
{ {
// contains at most 50 unique IDs from tasks, which is used to perform the lookup. // contains at most 50 unique IDs from tasks, which is used to perform the lookup.
var nextTaskBatch = new Dictionary<TLookup, List<TaskCompletionSource<TValue>>>(); var nextTaskBatch = new Dictionary<TLookup, List<TaskCompletionSource<TValue?>>>();
// Grab at most 50 unique IDs from the queue. // Grab at most 50 unique IDs from the queue.
lock (taskAssignmentLock) lock (taskAssignmentLock)
{ {
while (pendingTasks.Count > 0 && nextTaskBatch.Count < 50) while (pendingTasks.Count > 0 && nextTaskBatch.Count < 50)
{ {
(TLookup id, TaskCompletionSource<TValue> task) next = pendingTasks.Dequeue(); (TLookup id, TaskCompletionSource<TValue?> task) next = pendingTasks.Dequeue();
// Perform a secondary check for existence, in case the value was queried in a previous batch. // Perform a secondary check for existence, in case the value was queried in a previous batch.
if (CheckExists(next.id, out var existing)) if (CheckExists(next.id, out var existing))
@ -113,7 +108,7 @@ namespace osu.Game.Database
if (nextTaskBatch.TryGetValue(next.id, out var tasks)) if (nextTaskBatch.TryGetValue(next.id, out var tasks))
tasks.Add(next.task); tasks.Add(next.task);
else else
nextTaskBatch[next.id] = new List<TaskCompletionSource<TValue>> { next.task }; nextTaskBatch[next.id] = new List<TaskCompletionSource<TValue?>> { next.task };
} }
} }
} }

View File

@ -22,12 +22,15 @@ using osu.Framework.Statistics;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.IO.Legacy;
using osu.Game.Models; using osu.Game.Models;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Skinning; using osu.Game.Skinning;
using Realms; using Realms;
using Realms.Exceptions; using Realms.Exceptions;
@ -71,8 +74,11 @@ namespace osu.Game.Database
/// 24 2022-08-22 Added MaximumStatistics to ScoreInfo. /// 24 2022-08-22 Added MaximumStatistics to ScoreInfo.
/// 25 2022-09-18 Remove skins to add with new naming. /// 25 2022-09-18 Remove skins to add with new naming.
/// 26 2023-02-05 Added BeatmapHash to ScoreInfo. /// 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.
/// </summary> /// </summary>
private const int schema_version = 26; private const int schema_version = 29;
/// <summary> /// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods. /// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
@ -719,6 +725,11 @@ namespace osu.Game.Database
private void applyMigrationsForVersion(Migration migration, ulong targetVersion) private void applyMigrationsForVersion(Migration migration, ulong targetVersion)
{ {
Logger.Log($"Running realm migration to version {targetVersion}...");
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
switch (targetVersion) switch (targetVersion)
{ {
case 7: case 7:
@ -879,6 +890,7 @@ namespace osu.Game.Database
break; break;
case 26: case 26:
{
// Add ScoreInfo.BeatmapHash property to ensure scores correspond to the correct version of beatmap. // Add ScoreInfo.BeatmapHash property to ensure scores correspond to the correct version of beatmap.
var scores = migration.NewRealm.All<ScoreInfo>(); var scores = migration.NewRealm.All<ScoreInfo>();
@ -886,7 +898,76 @@ namespace osu.Game.Database
score.BeatmapHash = score.BeatmapInfo.Hash; score.BeatmapHash = score.BeatmapInfo.Hash;
break; break;
}
case 28:
{
var files = new RealmFileStore(this, storage);
var scores = migration.NewRealm.All<ScoreInfo>();
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<ScoreInfo>()
.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) private string? getRulesetShortNameFromLegacyID(long rulesetId)

View File

@ -0,0 +1,196 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<HitResult> 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<HitResult> misses = new Queue<HitResult>(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;
}
}
}
}

View File

@ -1,13 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
@ -21,8 +18,7 @@ namespace osu.Game.Database
/// <param name="userId">The user to lookup.</param> /// <param name="userId">The user to lookup.</param>
/// <param name="token">An optional cancellation token.</param> /// <param name="token">An optional cancellation token.</param>
/// <returns>The populated user, or null if the user does not exist or the request could not be satisfied.</returns> /// <returns>The populated user, or null if the user does not exist or the request could not be satisfied.</returns>
[ItemCanBeNull] public Task<APIUser?> GetUserAsync(int userId, CancellationToken token = default) => LookupAsync(userId, token);
public Task<APIUser> GetUserAsync(int userId, CancellationToken token = default) => LookupAsync(userId, token);
/// <summary> /// <summary>
/// Perform an API lookup on the specified users, populating a <see cref="APIUser"/> model. /// Perform an API lookup on the specified users, populating a <see cref="APIUser"/> model.
@ -30,10 +26,10 @@ namespace osu.Game.Database
/// <param name="userIds">The users to lookup.</param> /// <param name="userIds">The users to lookup.</param>
/// <param name="token">An optional cancellation token.</param> /// <param name="token">An optional cancellation token.</param>
/// <returns>The populated users. May include null results for failed retrievals.</returns> /// <returns>The populated users. May include null results for failed retrievals.</returns>
public Task<APIUser[]> GetUsersAsync(int[] userIds, CancellationToken token = default) => LookupAsync(userIds, token); public Task<APIUser?[]> GetUsersAsync(int[] userIds, CancellationToken token = default) => LookupAsync(userIds, token);
protected override GetUsersRequest CreateRequest(IEnumerable<int> ids) => new GetUsersRequest(ids.ToArray()); protected override GetUsersRequest CreateRequest(IEnumerable<int> ids) => new GetUsersRequest(ids.ToArray());
protected override IEnumerable<APIUser> RetrieveResults(GetUsersRequest request) => request.Response?.Users; protected override IEnumerable<APIUser>? RetrieveResults(GetUsersRequest request) => request.Response?.Users;
} }
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Platform;
using osuTK; using osuTK;
namespace osu.Game.Extensions namespace osu.Game.Extensions
@ -43,5 +44,20 @@ namespace osu.Game.Extensions
/// <returns>The delta vector in Parent's coordinates.</returns> /// <returns>The delta vector in Parent's coordinates.</returns>
public static Vector2 ScreenSpaceDeltaToParentSpace(this Drawable drawable, Vector2 delta) => public static Vector2 ScreenSpaceDeltaToParentSpace(this Drawable drawable, Vector2 delta) =>
drawable.Parent.ToLocalSpace(drawable.Parent.ToScreenSpace(Vector2.Zero) + delta); drawable.Parent.ToLocalSpace(drawable.Parent.ToScreenSpace(Vector2.Zero) + delta);
/// <summary>
/// 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.
/// </summary>
public static void ApplyGameWideClock(this Drawable drawable, GameHost host)
{
drawable.Clock = host.UpdateThread.Clock;
drawable.ProcessCustomClock = false;
}
} }
} }

View File

@ -21,7 +21,12 @@ namespace osu.Game.Extensions
/// This is required as enum member names are not allowed to contain hyphens. /// This is required as enum member names are not allowed to contain hyphens.
/// </remarks> /// </remarks>
public static string ToCultureCode(this Language language) public static string ToCultureCode(this Language language)
=> language.ToString().Replace("_", "-"); {
if (language == Language.zh_hant)
return @"zh-tw";
return language.ToString().Replace("_", "-");
}
/// <summary> /// <summary>
/// Attempts to parse the supplied <paramref name="cultureCode"/> to a <see cref="Language"/> value. /// Attempts to parse the supplied <paramref name="cultureCode"/> to a <see cref="Language"/> value.
@ -30,7 +35,15 @@ namespace osu.Game.Extensions
/// <param name="language">The parsed <see cref="Language"/>. Valid only if the return value of the method is <see langword="true" />.</param> /// <param name="language">The parsed <see cref="Language"/>. Valid only if the return value of the method is <see langword="true" />.</param>
/// <returns>Whether the parsing succeeded.</returns> /// <returns>Whether the parsing succeeded.</returns>
public static bool TryParseCultureCode(string cultureCode, out Language language) 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);
}
/// <summary> /// <summary>
/// Parses the <see cref="Language"/> that is specified in <paramref name="frameworkLocale"/>, /// Parses the <see cref="Language"/> that is specified in <paramref name="frameworkLocale"/>,

View File

@ -24,7 +24,7 @@ namespace osu.Game.Graphics.Backgrounds
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(LargeTextureStore textures) 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) public override bool Equals(Background other)

View File

@ -1,17 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Layout; using osu.Framework.Layout;
using osu.Framework.Logging;
using osu.Framework.Threading;
using osu.Framework.Utils; using osu.Framework.Utils;
namespace osu.Game.Graphics.Containers namespace osu.Game.Graphics.Containers
@ -23,11 +22,35 @@ namespace osu.Game.Graphics.Containers
public partial class SectionsContainer<T> : Container<T> public partial class SectionsContainer<T> : Container<T>
where T : Drawable where T : Drawable
{ {
public Bindable<T> SelectedSection { get; } = new Bindable<T>(); public Bindable<T?> SelectedSection { get; } = new Bindable<T?>();
private T lastClickedSection; private T? lastClickedSection;
public Drawable ExpandableHeader protected override Container<T> 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<T> scrollContentContainer = null!;
private float? headerHeight, footerHeight;
private float? lastKnownScroll;
/// <summary>
/// The percentage of the container to consider the centre-point for deciding the active section (and scrolling to a requested section).
/// </summary>
private const float scroll_y_centre = 0.1f;
private Drawable? expandableHeader;
public Drawable? ExpandableHeader
{ {
get => expandableHeader; get => expandableHeader;
set set
@ -42,11 +65,12 @@ namespace osu.Game.Graphics.Containers
if (value == null) return; if (value == null) return;
AddInternal(expandableHeader); AddInternal(expandableHeader);
lastKnownScroll = null; lastKnownScroll = null;
} }
} }
public Drawable FixedHeader public Drawable? FixedHeader
{ {
get => fixedHeader; get => fixedHeader;
set set
@ -63,7 +87,7 @@ namespace osu.Game.Graphics.Containers
} }
} }
public Drawable Footer public Drawable? Footer
{ {
get => footer; get => footer;
set set
@ -75,16 +99,17 @@ namespace osu.Game.Graphics.Containers
footer = value; footer = value;
if (value == null) return; if (footer == null) return;
footer.Anchor |= Anchor.y2; footer.Anchor |= Anchor.y2;
footer.Origin |= Anchor.y2; footer.Origin |= Anchor.y2;
scrollContainer.Add(footer); scrollContainer.Add(footer);
lastKnownScroll = null; lastKnownScroll = null;
} }
} }
public Drawable HeaderBackground public Drawable? HeaderBackground
{ {
get => headerBackground; get => headerBackground;
set set
@ -102,23 +127,6 @@ namespace osu.Game.Graphics.Containers
} }
} }
protected override Container<T> Content => scrollContentContainer;
private readonly UserTrackingScrollContainer scrollContainer;
private readonly Container headerBackgroundContainer;
private readonly MarginPadding originalSectionsMargin;
private Drawable expandableHeader, fixedHeader, footer, headerBackground;
private FlowContainer<T> scrollContentContainer;
private float? headerHeight, footerHeight;
private float? lastKnownScroll;
/// <summary>
/// The percentage of the container to consider the centre-point for deciding the active section (and scrolling to a requested section).
/// </summary>
private const float scroll_y_centre = 0.1f;
public SectionsContainer() public SectionsContainer()
{ {
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
@ -150,31 +158,63 @@ namespace osu.Game.Graphics.Containers
footerHeight = null; footerHeight = null;
} }
private ScheduledDelegate? scrollToTargetDelegate;
public void ScrollTo(Drawable target) public void ScrollTo(Drawable target)
{ {
Logger.Log($"Scrolling to {target}..");
lastKnownScroll = null; lastKnownScroll = null;
// implementation similar to ScrollIntoView but a bit more nuanced. float scrollTarget = getScrollTargetForDrawable(target);
float top = scrollContainer.GetChildPosInContent(target);
float bottomScrollExtent = scrollContainer.ScrollableExtent; if (scrollTarget > scrollContainer.ScrollableExtent)
float scrollTarget = top - scrollContainer.DisplayableContent * scroll_y_centre;
if (scrollTarget > bottomScrollExtent)
scrollContainer.ScrollToEnd(); scrollContainer.ScrollToEnd();
else else
scrollContainer.ScrollTo(scrollTarget); scrollContainer.ScrollTo(scrollTarget);
if (target is T section) if (target is T section)
lastClickedSection = 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); public void ScrollToTop() => scrollContainer.ScrollTo(0);
[NotNull]
protected virtual UserTrackingScrollContainer CreateScrollContainer() => new UserTrackingScrollContainer(); protected virtual UserTrackingScrollContainer CreateScrollContainer() => new UserTrackingScrollContainer();
[NotNull]
protected virtual FlowContainer<T> CreateScrollContentContainer() => protected virtual FlowContainer<T> CreateScrollContentContainer() =>
new FillFlowContainer<T> new FillFlowContainer<T>
{ {

View File

@ -19,6 +19,7 @@ using osu.Framework.Platform;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Online.Multiplayer;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
@ -69,7 +70,7 @@ namespace osu.Game.Graphics
{ {
case GlobalAction.TakeScreenshot: case GlobalAction.TakeScreenshot:
shutter.Play(); shutter.Play();
TakeScreenshotAsync(); TakeScreenshotAsync().FireAndForget();
return true; return true;
} }
@ -86,70 +87,75 @@ namespace osu.Game.Graphics
{ {
Interlocked.Increment(ref screenShotTasks); Interlocked.Increment(ref screenShotTasks);
if (!captureMenuCursor.Value) try
{ {
cursorVisibility.Value = false; if (!captureMenuCursor.Value)
// 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))
{ {
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) ScheduledDelegate waitDelegate = host.DrawThread.Scheduler.AddDelayed(() =>
// ReSharper disable once AccessToDisposedClosure {
framesWaitedEvent.Set(); if (framesWaited++ >= frames_to_wait)
}, 10, true); // ReSharper disable once AccessToDisposedClosure
framesWaitedEvent.Set();
}, 10, true);
if (!framesWaitedEvent.Wait(1000)) if (!framesWaitedEvent.Wait(1000))
throw new TimeoutException("Screenshot data did not arrive in a timely fashion"); 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;
}
});
} }
} }
finally
using (var image = await host.TakeScreenshotAsync().ConfigureAwait(false))
{ {
if (Interlocked.Decrement(ref screenShotTasks) == 0 && cursorVisibility.Value == false) if (Interlocked.Decrement(ref screenShotTasks) == 0)
cursorVisibility.Value = true; 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;
}
});
} }
}); });

View File

@ -3,12 +3,19 @@
#nullable disable #nullable disable
using System;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
namespace osu.Game.Graphics.Sprites namespace osu.Game.Graphics.Sprites
{ {
public partial class OsuSpriteText : SpriteText public partial class OsuSpriteText : SpriteText
{ {
[Obsolete("Use TruncatingSpriteText instead.")]
public new bool Truncate
{
set => throw new InvalidOperationException($"Use {nameof(TruncatingSpriteText)} instead.");
}
public OsuSpriteText() public OsuSpriteText()
{ {
Shadow = true; Shadow = true;

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{
/// <summary>
/// A derived version of <see cref="OsuSpriteText"/> which automatically shows non-truncated text in tooltip when required.
/// </summary>
public sealed partial class TruncatingSpriteText : OsuSpriteText, IHasTooltip
{
/// <summary>
/// Whether a tooltip should be shown with non-truncated text on hover.
/// </summary>
public bool ShowTooltip { get; init; } = true;
public LocalisableString TooltipText => Text;
public override bool HandlePositionalInput => IsTruncated && ShowTooltip;
public TruncatingSpriteText()
{
((SpriteText)this).Truncate = true;
}
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
public enum MenuItemType public enum MenuItemType

View File

@ -335,12 +335,11 @@ namespace osu.Game.Graphics.UserInterface
{ {
new Drawable[] new Drawable[]
{ {
Text = new OsuSpriteText Text = new TruncatingSpriteText
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Truncate = true,
}, },
Icon = new SpriteIcon Icon = new SpriteIcon
{ {

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
public enum SelectionState public enum SelectionState

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
/// <summary> /// <summary>

View File

@ -101,6 +101,10 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.J }, GlobalAction.EditorFlipVertically), 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.MouseWheelDown }, GlobalAction.EditorDecreaseDistanceSpacing),
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.EditorIncreaseDistanceSpacing), 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<KeyBinding> InGameKeyBindings => new[] public IEnumerable<KeyBinding> InGameKeyBindings => new[]
@ -355,6 +359,12 @@ namespace osu.Game.Input.Bindings
ToggleProfile, ToggleProfile,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCloneSelection))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCloneSelection))]
EditorCloneSelection EditorCloneSelection,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCyclePreviousBeatSnapDivisor))]
EditorCyclePreviousBeatSnapDivisor,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCycleNextBeatSnapDivisor))]
EditorCycleNextBeatSnapDivisor,
} }
} }

Some files were not shown because too many files have changed in this diff Show More