diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 0c6b80e97e..fc61573416 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1 +1,2 @@
+github: ppy
custom: https://osu.ppy.sh/home/support
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 29cbdd2d37..2a3b2fd978 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -50,6 +50,48 @@ jobs:
name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
path: ${{github.workspace}}/TestResults/TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx
+ build-only-android:
+ name: Build only (Android)
+ runs-on: windows-latest
+ timeout-minutes: 60
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Install .NET 5.0.x
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: "5.0.x"
+
+ - name: Setup MSBuild
+ uses: microsoft/setup-msbuild@v1
+
+ - name: Build
+ run: msbuild osu.Android.slnf /restore /p:Configuration=Debug
+
+ build-only-ios:
+ # While this workflow technically *can* run, it fails as iOS builds are blocked by multiple issues.
+ # See https://github.com/ppy/osu-framework/issues/4677 for the details.
+ # The job can be unblocked once those issues are resolved and game deployments can happen again.
+ if: false
+ name: Build only (iOS)
+ runs-on: macos-latest
+ timeout-minutes: 60
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+
+ - name: Install .NET 5.0.x
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: "5.0.x"
+
+ # Contrary to seemingly any other msbuild, msbuild running on macOS/Mono
+ # cannot accept .sln(f) files as arguments.
+ # Build just the main game for now.
+ - name: Build
+ run: msbuild osu.iOS/osu.iOS.csproj /restore /p:Configuration=Debug
+
inspect-code:
name: Code Quality
runs-on: ubuntu-latest
@@ -79,9 +121,14 @@ jobs:
run: |
# TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
# FIXME: Suppress warnings from templates project
- dotnet codefilesanity | while read -r line; do
- echo "::warning::$line"
- done
+ exit_code=0
+ while read -r line; do
+ if [[ ! -z "$line" ]]; then
+ echo "::error::$line"
+ exit_code=1
+ fi
+ done <<< $(dotnet codefilesanity)
+ exit $exit_code
# Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
# - name: .NET Format (Dry Run)
diff --git a/.github/workflows/report-nunit.yml b/.github/workflows/report-nunit.yml
index e0ccd50989..358cbda17a 100644
--- a/.github/workflows/report-nunit.yml
+++ b/.github/workflows/report-nunit.yml
@@ -30,3 +30,5 @@ jobs:
name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}})
path: "*.trx"
reporter: dotnet-trx
+ list-suites: 'failed'
+ list-tests: 'failed'
diff --git a/osu.Android.props b/osu.Android.props
index 5a0e7479fa..552675d706 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,8 +51,8 @@
-
-
+
+
diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchDistanceSnapGrid.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchDistanceSnapGrid.cs
new file mode 100644
index 0000000000..2be0b7e9b2
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneCatchDistanceSnapGrid.cs
@@ -0,0 +1,91 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Events;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Catch.Edit;
+using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Tests.Editor
+{
+ public class TestSceneCatchDistanceSnapGrid : OsuManualInputManagerTestScene
+ {
+ private readonly ManualClock manualClock = new ManualClock();
+
+ [Cached(typeof(Playfield))]
+ private readonly CatchPlayfield playfield;
+
+ private ScrollingHitObjectContainer hitObjectContainer => playfield.HitObjectContainer;
+
+ private readonly CatchDistanceSnapGrid distanceGrid;
+
+ private readonly FruitOutline fruitOutline;
+
+ private readonly Fruit fruit = new Fruit();
+
+ public TestSceneCatchDistanceSnapGrid()
+ {
+ Child = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Y,
+ Width = 500,
+
+ Children = new Drawable[]
+ {
+ new ScrollingTestContainer(ScrollingDirection.Down)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = playfield = new CatchPlayfield(new BeatmapDifficulty())
+ {
+ RelativeSizeAxes = Axes.Both,
+ Clock = new FramedClock(manualClock)
+ }
+ },
+ distanceGrid = new CatchDistanceSnapGrid(new double[] { 0, -1, 1 }),
+ fruitOutline = new FruitOutline()
+ },
+ };
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ distanceGrid.StartTime = 100;
+ distanceGrid.StartX = 250;
+
+ Vector2 screenSpacePosition = InputManager.CurrentState.Mouse.Position;
+
+ var result = distanceGrid.GetSnappedPosition(screenSpacePosition);
+
+ if (result != null)
+ {
+ fruit.OriginalX = hitObjectContainer.ToLocalSpace(result.ScreenSpacePosition).X;
+
+ if (result.Time != null)
+ fruit.StartTime = result.Time.Value;
+ }
+
+ fruitOutline.Position = CatchHitObjectUtils.GetStartPosition(hitObjectContainer, fruit);
+ fruitOutline.UpdateFrom(fruit);
+ }
+
+ protected override bool OnScroll(ScrollEvent e)
+ {
+ manualClock.CurrentTime -= e.ScrollDelta.Y * 50;
+ return true;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapGrid.cs b/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapGrid.cs
new file mode 100644
index 0000000000..137ac1fc59
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/CatchDistanceSnapGrid.cs
@@ -0,0 +1,141 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using JetBrains.Annotations;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Lines;
+using osu.Framework.Graphics.Primitives;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ ///
+ /// The guide lines used in the osu!catch editor to compose patterns that can be caught with constant speed.
+ /// Currently, only forward placement (an object is snapped based on the previous object, not the opposite) is supported.
+ ///
+ public class CatchDistanceSnapGrid : CompositeDrawable
+ {
+ public double StartTime { get; set; }
+
+ public float StartX { get; set; }
+
+ private const double max_vertical_line_length_in_time = CatchPlayfield.WIDTH / Catcher.BASE_SPEED * 2;
+
+ private readonly double[] velocities;
+
+ private readonly List verticalPaths = new List();
+
+ private readonly List verticalLineVertices = new List();
+
+ [Resolved]
+ private Playfield playfield { get; set; }
+
+ private ScrollingHitObjectContainer hitObjectContainer => (ScrollingHitObjectContainer)playfield.HitObjectContainer;
+
+ public CatchDistanceSnapGrid(double[] velocities)
+ {
+ RelativeSizeAxes = Axes.Both;
+ Anchor = Anchor.BottomLeft;
+
+ this.velocities = velocities;
+
+ for (int i = 0; i < velocities.Length; i++)
+ {
+ verticalPaths.Add(new SmoothPath
+ {
+ PathRadius = 2,
+ Alpha = 0.5f,
+ });
+
+ verticalLineVertices.Add(new[] { Vector2.Zero, Vector2.Zero });
+ }
+
+ AddRangeInternal(verticalPaths);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ double currentTime = hitObjectContainer.Time.Current;
+
+ for (int i = 0; i < velocities.Length; i++)
+ {
+ double velocity = velocities[i];
+
+ // The line ends at the top of the playfield.
+ double endTime = hitObjectContainer.TimeAtPosition(-hitObjectContainer.DrawHeight, currentTime);
+
+ // Non-vertical lines are cut at the sides of the playfield.
+ // Vertical lines are cut at some reasonable length.
+ if (velocity > 0)
+ endTime = Math.Min(endTime, StartTime + (CatchPlayfield.WIDTH - StartX) / velocity);
+ else if (velocity < 0)
+ endTime = Math.Min(endTime, StartTime + StartX / -velocity);
+ else
+ endTime = Math.Min(endTime, StartTime + max_vertical_line_length_in_time);
+
+ Vector2[] lineVertices = verticalLineVertices[i];
+ lineVertices[0] = calculatePosition(velocity, StartTime);
+ lineVertices[1] = calculatePosition(velocity, endTime);
+
+ var verticalPath = verticalPaths[i];
+ verticalPath.Vertices = verticalLineVertices[i];
+ verticalPath.OriginPosition = verticalPath.PositionInBoundingBox(Vector2.Zero);
+ }
+
+ Vector2 calculatePosition(double velocity, double time)
+ {
+ // Don't draw inverted lines.
+ time = Math.Max(time, StartTime);
+
+ float x = StartX + (float)((time - StartTime) * velocity);
+ float y = hitObjectContainer.PositionAtTime(time, currentTime);
+ return new Vector2(x, y);
+ }
+ }
+
+ [CanBeNull]
+ public SnapResult GetSnappedPosition(Vector2 screenSpacePosition)
+ {
+ double time = hitObjectContainer.TimeAtScreenSpacePosition(screenSpacePosition);
+
+ // If the cursor is below the distance snap grid, snap to the origin.
+ // Not returning `null` to retain the continuous snapping behavior when the cursor is slightly below the origin.
+ // This behavior is not currently visible in the editor because editor chooses the snap start time based on the mouse position.
+ if (time <= StartTime)
+ {
+ float y = hitObjectContainer.PositionAtTime(StartTime);
+ Vector2 originPosition = hitObjectContainer.ToScreenSpace(new Vector2(StartX, y));
+ return new SnapResult(originPosition, StartTime);
+ }
+
+ return enumerateSnappingCandidates(time)
+ .OrderBy(pos => Vector2.DistanceSquared(screenSpacePosition, pos.ScreenSpacePosition))
+ .FirstOrDefault();
+ }
+
+ private IEnumerable enumerateSnappingCandidates(double time)
+ {
+ float y = hitObjectContainer.PositionAtTime(time);
+
+ foreach (double velocity in velocities)
+ {
+ float x = (float)(StartX + (time - StartTime) * velocity);
+ Vector2 screenSpacePosition = hitObjectContainer.ToScreenSpace(new Vector2(x, y + hitObjectContainer.DrawHeight));
+ yield return new SnapResult(screenSpacePosition, time);
+ }
+ }
+
+ protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
index 050c2f625d..67055fb5e0 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
@@ -2,14 +2,23 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using System.Linq;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input;
using osu.Game.Beatmaps;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@@ -17,6 +26,14 @@ namespace osu.Game.Rulesets.Catch.Edit
{
public class CatchHitObjectComposer : HitObjectComposer
{
+ private const float distance_snap_radius = 50;
+
+ private CatchDistanceSnapGrid distanceSnapGrid;
+
+ private readonly Bindable distanceSnapToggle = new Bindable();
+
+ private InputManager inputManager;
+
public CatchHitObjectComposer(CatchRuleset ruleset)
: base(ruleset)
{
@@ -30,6 +47,27 @@ namespace osu.Game.Rulesets.Catch.Edit
RelativeSizeAxes = Axes.Both,
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
});
+
+ LayerBelowRuleset.Add(distanceSnapGrid = new CatchDistanceSnapGrid(new[]
+ {
+ 0.0,
+ Catcher.BASE_SPEED, -Catcher.BASE_SPEED,
+ Catcher.BASE_SPEED / 2, -Catcher.BASE_SPEED / 2,
+ }));
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ inputManager = GetContainingInputManager();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ updateDistanceSnapGrid();
}
protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) =>
@@ -42,14 +80,95 @@ namespace osu.Game.Rulesets.Catch.Edit
new BananaShowerCompositionTool()
};
+ protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
+ {
+ new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
+ });
+
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
{
var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
- // TODO: implement position snap
result.ScreenSpacePosition.X = screenSpacePosition.X;
+
+ if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
+ Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
+ {
+ result = snapResult;
+ }
+
return result;
}
protected override ComposeBlueprintContainer CreateBlueprintContainer() => new CatchBlueprintContainer(this);
+
+ [CanBeNull]
+ private PalpableCatchHitObject getLastSnappableHitObject(double time)
+ {
+ var hitObject = EditorBeatmap.HitObjects.OfType().LastOrDefault(h => h.GetEndTime() < time && !(h is BananaShower));
+
+ switch (hitObject)
+ {
+ case Fruit fruit:
+ return fruit;
+
+ case JuiceStream juiceStream:
+ return juiceStream.NestedHitObjects.OfType().LastOrDefault(h => !(h is TinyDroplet));
+
+ default:
+ return null;
+ }
+ }
+
+ [CanBeNull]
+ private PalpableCatchHitObject getDistanceSnapGridSourceHitObject()
+ {
+ switch (BlueprintContainer.CurrentTool)
+ {
+ case SelectTool _:
+ if (EditorBeatmap.SelectedHitObjects.Count == 0)
+ return null;
+
+ double minTime = EditorBeatmap.SelectedHitObjects.Min(hitObject => hitObject.StartTime);
+ return getLastSnappableHitObject(minTime);
+
+ case FruitCompositionTool _:
+ case JuiceStreamCompositionTool _:
+ if (!CursorInPlacementArea)
+ return null;
+
+ if (EditorBeatmap.PlacementObject.Value is JuiceStream)
+ {
+ // Juice stream path is not subject to snapping.
+ return null;
+ }
+
+ double timeAtCursor = ((CatchPlayfield)Playfield).TimeAtScreenSpacePosition(inputManager.CurrentState.Mouse.Position);
+ return getLastSnappableHitObject(timeAtCursor);
+
+ default:
+ return null;
+ }
+ }
+
+ private void updateDistanceSnapGrid()
+ {
+ if (distanceSnapToggle.Value != TernaryState.True)
+ {
+ distanceSnapGrid.Hide();
+ return;
+ }
+
+ var sourceHitObject = getDistanceSnapGridSourceHitObject();
+
+ if (sourceHitObject == null)
+ {
+ distanceSnapGrid.Hide();
+ return;
+ }
+
+ distanceSnapGrid.Show();
+ distanceSnapGrid.StartTime = sourceHitObject.GetEndTime();
+ distanceSnapGrid.StartX = sourceHitObject.EffectiveX;
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 0d6925a83d..6d5a960f06 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -42,9 +42,8 @@ namespace osu.Game.Rulesets.Catch.Objects
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
- DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
- double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
+ double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate;
diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
index 538a51db5f..5ccb191a9b 100644
--- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
+++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs
@@ -13,6 +13,7 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
@@ -101,27 +102,27 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
throw new System.NotImplementedException();
}
- public override float GetBeatSnapDistanceAt(double referenceTime)
+ public override float GetBeatSnapDistanceAt(HitObject referenceObject)
{
throw new System.NotImplementedException();
}
- public override float DurationToDistance(double referenceTime, double duration)
+ public override float DurationToDistance(HitObject referenceObject, double duration)
{
throw new System.NotImplementedException();
}
- public override double DistanceToDuration(double referenceTime, float distance)
+ public override double DistanceToDuration(HitObject referenceObject, float distance)
{
throw new System.NotImplementedException();
}
- public override double GetSnappedDurationFromDistance(double referenceTime, float distance)
+ public override double GetSnappedDurationFromDistance(HitObject referenceObject, float distance)
{
throw new System.NotImplementedException();
}
- public override float GetSnappedDistanceFromDistance(double referenceTime, float distance)
+ public override float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance)
{
throw new System.NotImplementedException();
}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
index 471dad87d5..4387bc6b3b 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
@@ -388,7 +388,7 @@ namespace osu.Game.Rulesets.Mania.Tests
},
};
- beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
+ beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
}
AddStep("load player", () =>
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
index 18891f8c58..89e13acad6 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
@@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Mania.Tests
},
});
- Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
+ Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f });
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index 380efff69f..1ed045f7e0 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
Debug.Assert(distanceData != null);
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
- DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime);
+ DifficultyControlPoint difficultyPoint = hitObject.DifficultyControlPoint;
double beatLength;
#pragma warning disable 618
@@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
#pragma warning restore 618
beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
else
- beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier;
+ beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity;
SpanCount = repeatsData?.SpanCount() ?? 1;
StartTime = (int)Math.Round(hitObject.StartTime);
diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
index ac8168dfc9..8e09a01469 100644
--- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
+++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs
@@ -28,7 +28,12 @@ namespace osu.Game.Rulesets.Mania.Configuration
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
{
new TrackedSetting(ManiaRulesetSetting.ScrollTime,
- v => new SettingDescription(v, "Scroll Speed", $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / v)} ({v}ms)"))
+ scrollTime => new SettingDescription(
+ rawValue: scrollTime,
+ name: "Scroll Speed",
+ value: $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime)} ({scrollTime}ms)"
+ )
+ )
};
}
diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu
index 7c75b45e5f..ca9e5b0b85 100644
--- a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu
+++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu
@@ -13,6 +13,7 @@ SliderTickRate:1
[TimingPoints]
0,500,4,1,0,100,1,0
+10000,-150,4,1,0,100,1,0
[HitObjects]
51,192,500,128,0,1500:1:0:0:0:
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
index 3b7da8d9ba..28e970f397 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
@@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Mania.UI
// For non-mania beatmap, speed changes should only happen through timing points
if (!isForCurrentRuleset)
- p.DifficultyPoint = new DifficultyControlPoint();
+ p.EffectPoint = new EffectControlPoint();
}
BarLines.ForEach(Playfield.Add);
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
index 851be2b2f2..ef43c3a696 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
@@ -11,6 +11,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Objects;
@@ -179,15 +180,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
- public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length;
+ public float GetBeatSnapDistanceAt(HitObject referenceObject) => (float)beat_length;
- public float DurationToDistance(double referenceTime, double duration) => (float)duration;
+ public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration;
- public double DistanceToDuration(double referenceTime, float distance) => distance;
+ public double DistanceToDuration(HitObject referenceObject, float distance) => distance;
- public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;
+ public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => 0;
- public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => 0;
+ public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => 0;
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
index 0ba775e5c7..37f1a846ad 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
@@ -45,8 +45,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
{
new Spinner
{
- Duration = 2000,
- Position = OsuPlayfield.BASE_SIZE / 2
+ Duration = 6000,
+ Position = OsuPlayfield.BASE_SIZE / 2,
}
}
},
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs
index ed9da36b05..71b575abe2 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs
@@ -117,6 +117,42 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
PassCondition = checkSomeHit
});
+ [Test]
+ public void TestApproachCirclesOnly() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModHidden { OnlyFadeApproachCircles = { Value = true } },
+ Autoplay = true,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new HitCircle
+ {
+ StartTime = 1000,
+ Position = new Vector2(206, 142)
+ },
+ new HitCircle
+ {
+ StartTime = 2000,
+ Position = new Vector2(306, 142)
+ },
+ new Slider
+ {
+ StartTime = 3000,
+ Position = new Vector2(156, 242),
+ Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(200, 0), })
+ },
+ new Spinner
+ {
+ Position = new Vector2(256, 192),
+ StartTime = 7000,
+ EndTime = 9000
+ }
+ }
+ },
+ PassCondition = checkSomeHit
+ });
+
private bool checkSomeHit() => Player.ScoreProcessor.JudgedHits >= 4;
private bool objectWithIncreasedVisibilityHasIndex(int index)
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
index f09aad8b49..1f01ba601b 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs
@@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
config.SetValue(OsuSetting.AutoCursorSize, true);
gameplayState.Beatmap.Difficulty.CircleSize = val;
- Scheduler.AddOnce(() => loadContent(false));
+ Scheduler.AddOnce(loadContent);
});
AddStep("test cursor container", () => loadContent(false));
@@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep($"adjust cs to {circleSize}", () => gameplayState.Beatmap.Difficulty.CircleSize = circleSize);
AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true));
- AddStep("load content", () => loadContent());
+ AddStep("load content", loadContent);
AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize) * userScale);
@@ -98,7 +98,9 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("load content", () => loadContent(false, () => new SkinProvidingContainer(new TopLeftCursorSkin())));
}
- private void loadContent(bool automated = true, Func skinProvider = null)
+ private void loadContent() => loadContent(false);
+
+ private void loadContent(bool automated, Func skinProvider = null)
{
SetContents(_ =>
{
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs
index ececfb0586..d31e7a31f5 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs
@@ -407,8 +407,6 @@ namespace osu.Game.Rulesets.Osu.Tests
},
});
- Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
-
SelectedMods.Value = new[] { new OsuModClassic() };
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
@@ -439,6 +437,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public TestSlider()
{
+ DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f };
+
DefaultsApplied += _ =>
{
HeadCircle.HitWindows = new TestHitWindows();
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
index 81902c25af..03b4254eed 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs
@@ -13,6 +13,7 @@ using osuTK.Graphics;
using osu.Game.Rulesets.Mods;
using System.Linq;
using NUnit.Framework;
+using osu.Game.Beatmaps.Legacy;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Judgements;
@@ -328,10 +329,14 @@ namespace osu.Game.Rulesets.Osu.Tests
private Drawable createDrawable(Slider slider, float circleSize, double speedMultiplier)
{
- var cpi = new ControlPointInfo();
- cpi.Add(0, new DifficultyControlPoint { SpeedMultiplier = speedMultiplier });
+ var cpi = new LegacyControlPointInfo();
+ cpi.Add(0, new DifficultyControlPoint { SliderVelocity = speedMultiplier });
- slider.ApplyDefaults(cpi, new BeatmapDifficulty { CircleSize = circleSize, SliderTickRate = 3 });
+ slider.ApplyDefaults(cpi, new BeatmapDifficulty
+ {
+ CircleSize = circleSize,
+ SliderTickRate = 3
+ });
var drawable = CreateDrawableSlider(slider);
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
index 590d159300..f3392724ec 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs
@@ -348,6 +348,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = time_slider_start,
Position = new Vector2(0, 0),
+ DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f },
Path = new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,
@@ -362,8 +363,6 @@ namespace osu.Game.Rulesets.Osu.Tests
},
});
- Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
-
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index 9da583a073..52ab39cfbd 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -30,6 +30,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneSpinnerRotation : TestSceneOsuPlayer
{
+ private const double spinner_start_time = 100;
+ private const double spinner_duration = 6000;
+
[Resolved]
private AudioManager audioManager { get; set; }
@@ -77,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Tests
double finalTrackerRotation = 0, trackerRotationTolerance = 0;
double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
- addSeekStep(5000);
+ addSeekStep(spinner_start_time + 5000);
AddStep("retrieve disc rotation", () =>
{
finalTrackerRotation = drawableSpinner.RotationTracker.Rotation;
@@ -90,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests
});
AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation);
- addSeekStep(2500);
+ addSeekStep(spinner_start_time + 2500);
AddAssert("disc rotation rewound",
// we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in.
// due to the exponential damping applied we're allowing a larger margin of error of about 10%
@@ -102,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Tests
// cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
() => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100));
- addSeekStep(5000);
+ addSeekStep(spinner_start_time + 5000);
AddAssert("is disc rotation almost same",
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance));
AddAssert("is symbol rotation almost same",
@@ -140,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test]
public void TestSpinnerNormalBonusRewinding()
{
- addSeekStep(1000);
+ addSeekStep(spinner_start_time + 1000);
AddAssert("player score matching expected bonus score", () =>
{
@@ -201,24 +204,9 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0));
}
- private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay
- {
- Frames = scoreReplay
- .Frames
- .Cast()
- .Select(replayFrame =>
- {
- var adjustedTime = replayFrame.Time * rate;
- return new OsuReplayFrame(adjustedTime, replayFrame.Position, replayFrame.Actions.ToArray());
- })
- .Cast()
- .ToList()
- };
-
private void addSeekStep(double time)
{
AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
-
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
@@ -241,7 +229,8 @@ namespace osu.Game.Rulesets.Osu.Tests
new Spinner
{
Position = new Vector2(256, 192),
- EndTime = 6000,
+ StartTime = spinner_start_time,
+ Duration = spinner_duration
},
}
};
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs
index 1b85e0efde..2d43e1b95e 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs
@@ -369,8 +369,6 @@ namespace osu.Game.Rulesets.Osu.Tests
},
});
- Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
-
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>
@@ -399,6 +397,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public TestSlider()
{
+ DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f };
+
DefaultsApplied += _ =>
{
HeadCircle.HitWindows = new TestHitWindows();
diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
index a2fc4848af..d82186fb52 100644
--- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
@@ -11,6 +11,7 @@ using System.Linq;
using System.Threading;
using osu.Game.Rulesets.Osu.UI;
using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Game.Beatmaps.Legacy;
namespace osu.Game.Rulesets.Osu.Beatmaps
{
@@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
LegacyLastTickOffset = (original as IHasLegacyLastTickOffset)?.LegacyLastTickOffset,
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
// this results in more (or less) ticks being generated in ().Sum(s => s.NestedHitObjects.Count - 1);
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
+ int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
return new OsuDifficultyAttributes
@@ -78,6 +79,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
DrainRate = drainRate,
MaxCombo = maxCombo,
HitCircleCount = hitCirclesCount,
+ SliderCount = sliderCount,
SpinnerCount = spinnerCount,
Skills = skills
};
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index 4e4dbc02a1..4bca87204a 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private int countMeh;
private int countMiss;
+ private int effectiveMissCount;
+
public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
: base(ruleset, attributes, score)
{
@@ -39,19 +41,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty
countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
+ effectiveMissCount = calculateEffectiveMissCount();
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
// Custom multipliers for NoFail and SpunOut.
if (mods.Any(m => m is OsuModNoFail))
- multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss);
+ multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount);
if (mods.Any(m => m is OsuModSpunOut))
multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85);
if (mods.Any(h => h is OsuModRelax))
{
- countMiss += countOk + countMeh;
+ effectiveMissCount += countOk + countMeh;
multiplier *= 0.6;
}
@@ -97,8 +100,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
aimValue *= lengthBonus;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
- if (countMiss > 0)
- aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), countMiss);
+ if (effectiveMissCount > 0)
+ aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), effectiveMissCount);
// Combo scaling.
if (Attributes.MaxCombo > 0)
@@ -115,7 +118,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double approachRateBonus = 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
if (mods.Any(m => m is OsuModBlinds))
- aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * countMiss)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate);
+ aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate);
else if (mods.Any(h => h is OsuModHidden))
{
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
@@ -142,8 +145,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= lengthBonus;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
- if (countMiss > 0)
- speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875));
+ if (effectiveMissCount > 0)
+ speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
// Combo scaling.
if (Attributes.MaxCombo > 0)
@@ -231,8 +234,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
flashlightValue *= 1.3;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
- if (countMiss > 0)
- flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875));
+ if (effectiveMissCount > 0)
+ flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
// Combo scaling.
if (Attributes.MaxCombo > 0)
@@ -250,6 +253,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return flashlightValue;
}
+ private int calculateEffectiveMissCount()
+ {
+ // guess the number of misses + slider breaks from combo
+ double comboBasedMissCount = 0.0;
+
+ if (Attributes.SliderCount > 0)
+ {
+ double fullComboThreshold = Attributes.MaxCombo - 0.1 * Attributes.SliderCount;
+ if (scoreMaxCombo < fullComboThreshold)
+ comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
+ }
+
+ // we're clamping misscount because since its derived from combo it can be higher than total hits and that breaks some calculations
+ comboBasedMissCount = Math.Min(comboBasedMissCount, totalHits);
+
+ return Math.Max(countMiss, (int)Math.Floor(comboBasedMissCount));
+ }
+
private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalSuccessfulHits => countGreat + countOk + countMeh;
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index 8e8f9bc06e..5e5993aefe 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -54,6 +54,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
private void setDistances()
{
+ // We don't need to calculate either angle or distance when one of the last->curr objects is a spinner
+ if (BaseObject is Spinner || lastObject is Spinner)
+ return;
+
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
float scalingFactor = normalized_radius / (float)BaseObject.Radius;
@@ -71,11 +75,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
Vector2 lastCursorPosition = getEndCursorPosition(lastObject);
- // Don't need to jump to reach spinners
- if (!(BaseObject is Spinner))
- JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
+ JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
- if (lastLastObject != null)
+ if (lastLastObject != null && !(lastLastObject is Spinner))
{
Vector2 lastLastCursorPosition = getEndCursorPosition(lastLastObject);
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
index b9e4ed6fcb..07b6a1bdc2 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
@@ -8,12 +8,14 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Events;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
+using osu.Game.Screens.Edit;
using osuTK;
using osuTK.Input;
@@ -67,6 +69,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
inputManager = GetContainingInputManager();
}
+ [Resolved]
+ private EditorBeatmap editorBeatmap { get; set; }
+
public override void UpdateTimeAndPosition(SnapResult result)
{
base.UpdateTimeAndPosition(result);
@@ -75,6 +80,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
case SliderPlacementState.Initial:
BeginPlacement();
+
+ var nearestDifficultyPoint = editorBeatmap.HitObjects.LastOrDefault(h => h.GetEndTime() < HitObject.StartTime)?.DifficultyControlPoint?.DeepClone() as DifficultyControlPoint;
+
+ HitObject.DifficultyControlPoint = nearestDifficultyPoint ?? new DifficultyControlPoint();
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
break;
@@ -212,7 +221,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updateSlider()
{
- HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
+ HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
bodyPiece.UpdateFrom(HitObject);
headCirclePiece.UpdateFrom(HitObject.HeadCircle);
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index 89724876fa..a7fadfb67f 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -230,7 +230,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updatePath()
{
- HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
+ HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
editorBeatmap?.Update(HitObject);
}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs
index ff3be97427..8a561f962a 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs
@@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Edit
public class OsuDistanceSnapGrid : CircularDistanceSnapGrid
{
public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null)
- : base(hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime)
+ : base(hitObject, hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime)
{
Masking = true;
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
index 9c7784a00a..e162f805a1 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
@@ -5,6 +5,8 @@ using System;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Graphics;
+using osu.Framework.Bindables;
+using osu.Game.Configuration;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@@ -17,6 +19,9 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModHidden : ModHidden, IHidesApproachCircles
{
+ [SettingSource("Only fade approach circles", "The main object body will not fade when enabled.")]
+ public Bindable OnlyFadeApproachCircles { get; } = new BindableBool();
+
public override string Description => @"Play with no approach circles and fading circles/sliders.";
public override double ScoreMultiplier => 1.06;
@@ -44,15 +49,15 @@ namespace osu.Game.Rulesets.Osu.Mods
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
- applyState(hitObject, true);
+ applyHiddenState(hitObject, true);
}
protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
- applyState(hitObject, false);
+ applyHiddenState(hitObject, false);
}
- private void applyState(DrawableHitObject drawableObject, bool increaseVisibility)
+ private void applyHiddenState(DrawableHitObject drawableObject, bool increaseVisibility)
{
if (!(drawableObject is DrawableOsuHitObject drawableOsuObject))
return;
@@ -61,6 +66,24 @@ namespace osu.Game.Rulesets.Osu.Mods
(double fadeStartTime, double fadeDuration) = getFadeOutParameters(drawableOsuObject);
+ // process approach circle hiding first (to allow for early return below).
+ if (!increaseVisibility)
+ {
+ if (drawableObject is DrawableHitCircle circle)
+ {
+ using (circle.BeginAbsoluteSequence(hitObject.StartTime - hitObject.TimePreempt))
+ circle.ApproachCircle.Hide();
+ }
+ else if (drawableObject is DrawableSpinner spinner)
+ {
+ spinner.Body.OnSkinChanged += () => hideSpinnerApproachCircle(spinner);
+ hideSpinnerApproachCircle(spinner);
+ }
+ }
+
+ if (OnlyFadeApproachCircles.Value)
+ return;
+
switch (drawableObject)
{
case DrawableSliderTail _:
@@ -84,12 +107,6 @@ namespace osu.Game.Rulesets.Osu.Mods
// only fade the circle piece (not the approach circle) for the increased visibility object.
fadeTarget = circle.CirclePiece;
}
- else
- {
- // we don't want to see the approach circle
- using (circle.BeginAbsoluteSequence(hitObject.StartTime - hitObject.TimePreempt))
- circle.ApproachCircle.Hide();
- }
using (drawableObject.BeginAbsoluteSequence(fadeStartTime))
fadeTarget.FadeOut(fadeDuration);
@@ -111,9 +128,6 @@ namespace osu.Game.Rulesets.Osu.Mods
// hide elements we don't care about.
// todo: hide background
- spinner.Body.OnSkinChanged += () => hideSpinnerApproachCircle(spinner);
- hideSpinnerApproachCircle(spinner);
-
using (spinner.BeginAbsoluteSequence(fadeStartTime))
spinner.FadeOut(fadeDuration);
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
new file mode 100644
index 0000000000..c48cbd9992
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
@@ -0,0 +1,76 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.Mods;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Bindables;
+using osu.Framework.Localisation;
+using osu.Framework.Utils;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Configuration;
+using osu.Game.Overlays.Settings;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Scoring;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ public class OsuModNoScope : Mod, IUpdatableByPlayfield, IApplicableToScoreProcessor
+ {
+ ///
+ /// Slightly higher than the cutoff for .
+ ///
+ private const float min_alpha = 0.0002f;
+
+ private const float transition_duration = 100;
+
+ public override string Name => "No Scope";
+ public override string Acronym => "NS";
+ public override ModType Type => ModType.Fun;
+ public override IconUsage? Icon => FontAwesome.Solid.EyeSlash;
+ public override string Description => "Where's the cursor?";
+ public override double ScoreMultiplier => 1;
+
+ private BindableNumber currentCombo;
+
+ private float targetAlpha;
+
+ [SettingSource(
+ "Hidden at combo",
+ "The combo count at which the cursor becomes completely hidden",
+ SettingControlType = typeof(SettingsSlider)
+ )]
+ public BindableInt HiddenComboCount { get; } = new BindableInt
+ {
+ Default = 10,
+ Value = 10,
+ MinValue = 0,
+ MaxValue = 50,
+ };
+
+ public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
+
+ public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
+ {
+ if (HiddenComboCount.Value == 0) return;
+
+ currentCombo = scoreProcessor.Combo.GetBoundCopy();
+ currentCombo.BindValueChanged(combo =>
+ {
+ targetAlpha = Math.Max(min_alpha, 1 - (float)combo.NewValue / HiddenComboCount.Value);
+ }, true);
+ }
+
+ public virtual void Update(Playfield playfield)
+ {
+ playfield.Cursor.Alpha = (float)Interpolation.Lerp(playfield.Cursor.Alpha, targetAlpha, Math.Clamp(playfield.Time.Elapsed / transition_duration, 0, 1));
+ }
+ }
+
+ public class HiddenComboSlider : OsuSliderBar
+ {
+ public override LocalisableString TooltipText => Current.Value == 0 ? "always hidden" : base.TooltipText;
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index 1d2666f46b..ba817d2e40 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -140,9 +140,8 @@ namespace osu.Game.Rulesets.Osu.Objects
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
- DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
- double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
+ double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
@@ -152,8 +151,9 @@ namespace osu.Game.Rulesets.Osu.Objects
{
base.CreateNestedHitObjects(cancellationToken);
- foreach (var e in
- SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken))
+ var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken);
+
+ foreach (var e in sliderEvents)
{
switch (e.Type)
{
@@ -175,7 +175,6 @@ namespace osu.Game.Rulesets.Osu.Objects
StartTime = e.Time,
Position = Position,
StackHeight = StackHeight,
- SampleControlPoint = SampleControlPoint,
});
break;
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index f4a93a571d..ee4712c3b8 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -192,6 +192,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModBarrelRoll(),
new OsuModApproachDifferent(),
new OsuModMuted(),
+ new OsuModNoScope(),
};
case ModType.System:
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index 50c0ca7f55..32aad6c36a 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
double distance = distanceData.Distance * spans * LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
- DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime);
+ DifficultyControlPoint difficultyPoint = obj.DifficultyControlPoint;
double beatLength;
#pragma warning disable 618
@@ -162,7 +162,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
#pragma warning restore 618
beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
else
- beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier;
+ beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity;
double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.Difficulty.SliderMultiplier / beatmap.Difficulty.SliderTickRate;
diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
index 0318e32991..0e93ad7e73 100644
--- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs
@@ -63,9 +63,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
- DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
- double scoringDistance = base_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
+ double scoringDistance = base_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity;
Velocity = scoringDistance / timingPoint.BeatLength;
tickSpacing = timingPoint.BeatLength / TickRate;
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index b5e1fa204f..d37e09aa29 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -192,15 +192,15 @@ namespace osu.Game.Tests.Beatmaps.Formats
var difficultyPoint = controlPoints.DifficultyPointAt(0);
Assert.AreEqual(0, difficultyPoint.Time);
- Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier);
+ Assert.AreEqual(1.0, difficultyPoint.SliderVelocity);
difficultyPoint = controlPoints.DifficultyPointAt(48428);
Assert.AreEqual(0, difficultyPoint.Time);
- Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier);
+ Assert.AreEqual(1.0, difficultyPoint.SliderVelocity);
difficultyPoint = controlPoints.DifficultyPointAt(116999);
Assert.AreEqual(116999, difficultyPoint.Time);
- Assert.AreEqual(0.75, difficultyPoint.SpeedMultiplier, 0.1);
+ Assert.AreEqual(0.75, difficultyPoint.SliderVelocity, 0.1);
var soundPoint = controlPoints.SamplePointAt(0);
Assert.AreEqual(956, soundPoint.Time);
@@ -227,7 +227,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsTrue(effectPoint.KiaiMode);
Assert.IsFalse(effectPoint.OmitFirstBarLine);
- effectPoint = controlPoints.EffectPointAt(119637);
+ effectPoint = controlPoints.EffectPointAt(116637);
Assert.AreEqual(95901, effectPoint.Time);
Assert.IsFalse(effectPoint.KiaiMode);
Assert.IsFalse(effectPoint.OmitFirstBarLine);
@@ -249,10 +249,10 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(controlPoints.EffectPoints.Count, Is.EqualTo(3));
Assert.That(controlPoints.SamplePoints.Count, Is.EqualTo(3));
- Assert.That(controlPoints.DifficultyPointAt(500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1));
- Assert.That(controlPoints.DifficultyPointAt(1500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1));
- Assert.That(controlPoints.DifficultyPointAt(2500).SpeedMultiplier, Is.EqualTo(0.75).Within(0.1));
- Assert.That(controlPoints.DifficultyPointAt(3500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1));
+ Assert.That(controlPoints.DifficultyPointAt(500).SliderVelocity, Is.EqualTo(1.5).Within(0.1));
+ Assert.That(controlPoints.DifficultyPointAt(1500).SliderVelocity, Is.EqualTo(1.5).Within(0.1));
+ Assert.That(controlPoints.DifficultyPointAt(2500).SliderVelocity, Is.EqualTo(0.75).Within(0.1));
+ Assert.That(controlPoints.DifficultyPointAt(3500).SliderVelocity, Is.EqualTo(1.5).Within(0.1));
Assert.That(controlPoints.EffectPointAt(500).KiaiMode, Is.True);
Assert.That(controlPoints.EffectPointAt(1500).KiaiMode, Is.True);
@@ -279,10 +279,10 @@ namespace osu.Game.Tests.Beatmaps.Formats
using (var resStream = TestResources.OpenResource("timingpoint-speedmultiplier-reset.osu"))
using (var stream = new LineBufferedReader(resStream))
{
- var controlPoints = decoder.Decode(stream).ControlPointInfo;
+ var controlPoints = (LegacyControlPointInfo)decoder.Decode(stream).ControlPointInfo;
- Assert.That(controlPoints.DifficultyPointAt(0).SpeedMultiplier, Is.EqualTo(0.5).Within(0.1));
- Assert.That(controlPoints.DifficultyPointAt(2000).SpeedMultiplier, Is.EqualTo(1).Within(0.1));
+ Assert.That(controlPoints.DifficultyPointAt(0).SliderVelocity, Is.EqualTo(0.5).Within(0.1));
+ Assert.That(controlPoints.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1).Within(0.1));
}
}
@@ -394,12 +394,12 @@ namespace osu.Game.Tests.Beatmaps.Formats
using (var resStream = TestResources.OpenResource("controlpoint-difficulty-multiplier.osu"))
using (var stream = new LineBufferedReader(resStream))
{
- var controlPointInfo = decoder.Decode(stream).ControlPointInfo;
+ var controlPointInfo = (LegacyControlPointInfo)decoder.Decode(stream).ControlPointInfo;
- Assert.That(controlPointInfo.DifficultyPointAt(5).SpeedMultiplier, Is.EqualTo(1));
- Assert.That(controlPointInfo.DifficultyPointAt(1000).SpeedMultiplier, Is.EqualTo(10));
- Assert.That(controlPointInfo.DifficultyPointAt(2000).SpeedMultiplier, Is.EqualTo(1.8518518518518519d));
- Assert.That(controlPointInfo.DifficultyPointAt(3000).SpeedMultiplier, Is.EqualTo(0.5));
+ Assert.That(controlPointInfo.DifficultyPointAt(5).SliderVelocity, Is.EqualTo(1));
+ Assert.That(controlPointInfo.DifficultyPointAt(1000).SliderVelocity, Is.EqualTo(10));
+ Assert.That(controlPointInfo.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1.8518518518518519d));
+ Assert.That(controlPointInfo.DifficultyPointAt(3000).SliderVelocity, Is.EqualTo(0.5));
}
}
@@ -775,5 +775,22 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(seventh.ControlPoints[4].Type == null);
}
}
+
+ [Test]
+ public void TestSliderLengthExtensionEdgeCase()
+ {
+ var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
+
+ using (var resStream = TestResources.OpenResource("duplicate-last-position-slider.osu"))
+ using (var stream = new LineBufferedReader(resStream))
+ {
+ var decoded = decoder.Decode(stream);
+
+ var path = ((IHasPath)decoded.HitObjects[0]).Path;
+
+ Assert.That(path.ExpectedDistance.Value, Is.EqualTo(2));
+ Assert.That(path.Distance, Is.EqualTo(1));
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
index 896aa53f82..d12da1a22f 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs
@@ -46,8 +46,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
sort(decoded.beatmap);
sort(decodedAfterEncode.beatmap);
- Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize()));
- Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration));
+ compareBeatmaps(decoded, decodedAfterEncode);
}
[TestCaseSource(nameof(allBeatmaps))]
@@ -62,8 +61,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
sort(decoded.beatmap);
sort(decodedAfterEncode.beatmap);
- Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize()));
- Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration));
+ compareBeatmaps(decoded, decodedAfterEncode);
}
[TestCaseSource(nameof(allBeatmaps))]
@@ -77,12 +75,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded), name);
- // in this process, we may lose some detail in the control points section.
- // let's focus on only the hitobjects.
- var originalHitObjects = decoded.beatmap.HitObjects.Serialize();
- var newHitObjects = decodedAfterEncode.beatmap.HitObjects.Serialize();
-
- Assert.That(newHitObjects, Is.EqualTo(originalHitObjects));
+ compareBeatmaps(decoded, decodedAfterEncode);
ControlPointInfo removeLegacyControlPointTypes(ControlPointInfo controlPointInfo)
{
@@ -97,7 +90,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
// completely ignore "legacy" types, which have been moved to HitObjects.
// even though these would mostly be ignored by the Add call, they will still be available in groups,
// which isn't what we want to be testing here.
- if (point is SampleControlPoint)
+ if (point is SampleControlPoint || point is DifficultyControlPoint)
continue;
newControlPoints.Add(point.Time, point.DeepClone());
@@ -107,6 +100,19 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
+ private void compareBeatmaps((IBeatmap beatmap, TestLegacySkin skin) expected, (IBeatmap beatmap, TestLegacySkin skin) actual)
+ {
+ // Check all control points that are still considered to be at a global level.
+ Assert.That(expected.beatmap.ControlPointInfo.TimingPoints.Serialize(), Is.EqualTo(actual.beatmap.ControlPointInfo.TimingPoints.Serialize()));
+ Assert.That(expected.beatmap.ControlPointInfo.EffectPoints.Serialize(), Is.EqualTo(actual.beatmap.ControlPointInfo.EffectPoints.Serialize()));
+
+ // Check all hitobjects.
+ Assert.That(expected.beatmap.HitObjects.Serialize(), Is.EqualTo(actual.beatmap.HitObjects.Serialize()));
+
+ // Check skin.
+ Assert.IsTrue(areComboColoursEqual(expected.skin.Configuration, actual.skin.Configuration));
+ }
+
[Test]
public void TestEncodeMultiSegmentSliderWithFloatingPointError()
{
@@ -156,7 +162,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
- private (IBeatmap beatmap, TestLegacySkin beatmapSkin) decodeFromLegacy(Stream stream, string name)
+ private (IBeatmap beatmap, TestLegacySkin skin) decodeFromLegacy(Stream stream, string name)
{
using (var reader = new LineBufferedReader(stream))
{
@@ -174,7 +180,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
- private MemoryStream encodeToLegacy((IBeatmap beatmap, ISkin beatmapSkin) fullBeatmap)
+ private MemoryStream encodeToLegacy((IBeatmap beatmap, ISkin skin) fullBeatmap)
{
var (beatmap, beatmapSkin) = fullBeatmap;
var stream = new MemoryStream();
diff --git a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs
index 022b2c1a59..2c1e39c2cf 100644
--- a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs
+++ b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs
@@ -77,8 +77,7 @@ namespace osu.Game.Tests.Beatmaps.IO
{
var reader = new ZipArchiveReader(osz);
- using (var stream = new StreamReader(
- reader.GetStream("Soleily - Renatus (Deif) [Platter].osu")))
+ using (var stream = new StreamReader(reader.GetStream("Soleily - Renatus (Deif) [Platter].osu")))
{
Assert.AreEqual("osu file format v13", stream.ReadLine()?.Trim());
}
diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs
index 2c2c4dc24e..af87fc17ad 100644
--- a/osu.Game.Tests/Chat/MessageFormatterTests.cs
+++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs
@@ -509,5 +509,17 @@ namespace osu.Game.Tests.Chat
Assert.AreEqual(LinkAction.External, result.Action);
Assert.AreEqual("/relative", result.Argument);
}
+
+ [TestCase("https://dev.ppy.sh/home/changelog", "")]
+ [TestCase("https://dev.ppy.sh/home/changelog/lazer/2021.1012", "lazer/2021.1012")]
+ public void TestChangelogLinks(string link, string expectedArg)
+ {
+ MessageFormatter.WebsiteRootUrl = "dev.ppy.sh";
+
+ LinkDetails result = MessageFormatter.GetLinkDetails(link);
+
+ Assert.AreEqual(LinkAction.OpenChangelog, result.Action);
+ Assert.AreEqual(expectedArg, result.Argument);
+ }
}
}
diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs
new file mode 100644
index 0000000000..4cdcf507b6
--- /dev/null
+++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs
@@ -0,0 +1,820 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using osu.Framework.Extensions;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Logging;
+using osu.Game.Beatmaps;
+using osu.Game.Database;
+using osu.Game.IO.Archives;
+using osu.Game.Models;
+using osu.Game.Stores;
+using osu.Game.Tests.Resources;
+using Realms;
+using SharpCompress.Archives;
+using SharpCompress.Archives.Zip;
+using SharpCompress.Common;
+using SharpCompress.Writers.Zip;
+
+#nullable enable
+
+namespace osu.Game.Tests.Database
+{
+ [TestFixture]
+ public class BeatmapImporterTests : RealmTest
+ {
+ [Test]
+ public void TestImportBeatmapThenCleanup()
+ {
+ RunTestWithRealmAsync(async (realmFactory, storage) =>
+ {
+ using (var importer = new BeatmapImporter(realmFactory, storage))
+ using (new RealmRulesetStore(realmFactory, storage))
+ {
+ ILive? imported;
+
+ using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
+ imported = await importer.Import(reader);
+
+ Assert.AreEqual(1, realmFactory.Context.All().Count());
+
+ Assert.NotNull(imported);
+ Debug.Assert(imported != null);
+
+ imported.PerformWrite(s => s.DeletePending = true);
+
+ Assert.AreEqual(1, realmFactory.Context.All().Count(s => s.DeletePending));
+ }
+ });
+
+ Logger.Log("Running with no work to purge pending deletions");
+
+ RunTestWithRealm((realmFactory, _) => { Assert.AreEqual(0, realmFactory.Context.All().Count()); });
+ }
+
+ [Test]
+ public void TestImportWhenClosed()
+ {
+ RunTestWithRealmAsync(async (realmFactory, storage) =>
+ {
+ using var importer = new BeatmapImporter(realmFactory, storage);
+ using var store = new RealmRulesetStore(realmFactory, storage);
+
+ await LoadOszIntoStore(importer, realmFactory.Context);
+ });
+ }
+
+ [Test]
+ public void TestImportThenDelete()
+ {
+ RunTestWithRealmAsync(async (realmFactory, storage) =>
+ {
+ using var importer = new BeatmapImporter(realmFactory, storage);
+ using var store = new RealmRulesetStore(realmFactory, storage);
+
+ var imported = await LoadOszIntoStore(importer, realmFactory.Context);
+
+ deleteBeatmapSet(imported, realmFactory.Context);
+ });
+ }
+
+ [Test]
+ public void TestImportThenDeleteFromStream()
+ {
+ RunTestWithRealmAsync(async (realmFactory, storage) =>
+ {
+ using var importer = new BeatmapImporter(realmFactory, storage);
+ using var store = new RealmRulesetStore(realmFactory, storage);
+
+ var tempPath = TestResources.GetTestBeatmapForImport();
+
+ ILive? importedSet;
+
+ using (var stream = File.OpenRead(tempPath))
+ {
+ importedSet = await importer.Import(new ImportTask(stream, Path.GetFileName(tempPath)));
+ ensureLoaded(realmFactory.Context);
+ }
+
+ Assert.NotNull(importedSet);
+ Debug.Assert(importedSet != null);
+
+ Assert.IsTrue(File.Exists(tempPath), "Stream source file somehow went missing");
+ File.Delete(tempPath);
+
+ var imported = realmFactory.Context.All().First(beatmapSet => beatmapSet.ID == importedSet.ID);
+
+ deleteBeatmapSet(imported, realmFactory.Context);
+ });
+ }
+
+ [Test]
+ public void TestImportThenImport()
+ {
+ RunTestWithRealmAsync(async (realmFactory, storage) =>
+ {
+ using var importer = new BeatmapImporter(realmFactory, storage);
+ using var store = new RealmRulesetStore(realmFactory, storage);
+
+ var imported = await LoadOszIntoStore(importer, realmFactory.Context);
+ var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context);
+
+ // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
+ Assert.IsTrue(imported.ID == importedSecondTime.ID);
+ Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
+
+ checkBeatmapSetCount(realmFactory.Context, 1);
+ checkSingleReferencedFileCount(realmFactory.Context, 18);
+ });
+ }
+
+ [Test]
+ public void TestImportThenImportWithReZip()
+ {
+ RunTestWithRealmAsync(async (realmFactory, storage) =>
+ {
+ using var importer = new BeatmapImporter(realmFactory, storage);
+ using var store = new RealmRulesetStore(realmFactory, storage);
+
+ var temp = TestResources.GetTestBeatmapForImport();
+
+ string extractedFolder = $"{temp}_extracted";
+ Directory.CreateDirectory(extractedFolder);
+
+ try
+ {
+ var imported = await LoadOszIntoStore(importer, realmFactory.Context);
+
+ string hashBefore = hashFile(temp);
+
+ using (var zip = ZipArchive.Open(temp))
+ zip.WriteToDirectory(extractedFolder);
+
+ using (var zip = ZipArchive.Create())
+ {
+ zip.AddAllFromDirectory(extractedFolder);
+ zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
+ }
+
+ // zip files differ because different compression or encoder.
+ Assert.AreNotEqual(hashBefore, hashFile(temp));
+
+ var importedSecondTime = await importer.Import(new ImportTask(temp));
+
+ ensureLoaded(realmFactory.Context);
+
+ Assert.NotNull(importedSecondTime);
+ Debug.Assert(importedSecondTime != null);
+
+ // but contents doesn't, so existing should still be used.
+ Assert.IsTrue(imported.ID == importedSecondTime.ID);
+ Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.PerformRead(s => s.Beatmaps.First().ID));
+ }
+ finally
+ {
+ Directory.Delete(extractedFolder, true);
+ }
+ });
+ }
+
+ [Test]
+ public void TestImportThenImportWithChangedHashedFile()
+ {
+ RunTestWithRealmAsync(async (realmFactory, storage) =>
+ {
+ using var importer = new BeatmapImporter(realmFactory, storage);
+ using var store = new RealmRulesetStore(realmFactory, storage);
+
+ var temp = TestResources.GetTestBeatmapForImport();
+
+ string extractedFolder = $"{temp}_extracted";
+ Directory.CreateDirectory(extractedFolder);
+
+ try
+ {
+ var imported = await LoadOszIntoStore(importer, realmFactory.Context);
+
+ await createScoreForBeatmap(realmFactory.Context, imported.Beatmaps.First());
+
+ using (var zip = ZipArchive.Open(temp))
+ zip.WriteToDirectory(extractedFolder);
+
+ // arbitrary write to hashed file
+ // this triggers the special BeatmapManager.PreImport deletion/replacement flow.
+ using (var sw = new FileInfo(Directory.GetFiles(extractedFolder, "*.osu").First()).AppendText())
+ await sw.WriteLineAsync("// changed");
+
+ using (var zip = ZipArchive.Create())
+ {
+ zip.AddAllFromDirectory(extractedFolder);
+ zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
+ }
+
+ var importedSecondTime = await importer.Import(new ImportTask(temp));
+
+ ensureLoaded(realmFactory.Context);
+
+ // check the newly "imported" beatmap is not the original.
+ Assert.NotNull(importedSecondTime);
+ Debug.Assert(importedSecondTime != null);
+
+ Assert.IsTrue(imported.ID != importedSecondTime.ID);
+ Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.PerformRead(s => s.Beatmaps.First().ID));
+ }
+ finally
+ {
+ Directory.Delete(extractedFolder, true);
+ }
+ });
+ }
+
+ [Test]
+ [Ignore("intentionally broken by import optimisations")]
+ public void TestImportThenImportWithChangedFile()
+ {
+ RunTestWithRealmAsync(async (realmFactory, storage) =>
+ {
+ using var importer = new BeatmapImporter(realmFactory, storage);
+ using var store = new RealmRulesetStore(realmFactory, storage);
+
+ var temp = TestResources.GetTestBeatmapForImport();
+
+ string extractedFolder = $"{temp}_extracted";
+ Directory.CreateDirectory(extractedFolder);
+
+ try
+ {
+ var imported = await LoadOszIntoStore(importer, realmFactory.Context);
+
+ using (var zip = ZipArchive.Open(temp))
+ zip.WriteToDirectory(extractedFolder);
+
+ // arbitrary write to non-hashed file
+ using (var sw = new FileInfo(Directory.GetFiles(extractedFolder, "*.mp3").First()).AppendText())
+ await sw.WriteLineAsync("text");
+
+ using (var zip = ZipArchive.Create())
+ {
+ zip.AddAllFromDirectory(extractedFolder);
+ zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
+ }
+
+ var importedSecondTime = await importer.Import(new ImportTask(temp));
+
+ ensureLoaded(realmFactory.Context);
+
+ Assert.NotNull(importedSecondTime);
+ Debug.Assert(importedSecondTime != null);
+
+ // check the newly "imported" beatmap is not the original.
+ Assert.IsTrue(imported.ID != importedSecondTime.ID);
+ Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.PerformRead(s => s.Beatmaps.First().ID));
+ }
+ finally
+ {
+ Directory.Delete(extractedFolder, true);
+ }
+ });
+ }
+
+ [Test]
+ public void TestImportThenImportWithDifferentFilename()
+ {
+ RunTestWithRealmAsync(async (realmFactory, storage) =>
+ {
+ using var importer = new BeatmapImporter(realmFactory, storage);
+ using var store = new RealmRulesetStore(realmFactory, storage);
+
+ var temp = TestResources.GetTestBeatmapForImport();
+
+ string extractedFolder = $"{temp}_extracted";
+ Directory.CreateDirectory(extractedFolder);
+
+ try
+ {
+ var imported = await LoadOszIntoStore(importer, realmFactory.Context);
+
+ using (var zip = ZipArchive.Open(temp))
+ zip.WriteToDirectory(extractedFolder);
+
+ // change filename
+ var firstFile = new FileInfo(Directory.GetFiles(extractedFolder).First());
+ firstFile.MoveTo(Path.Combine(firstFile.DirectoryName.AsNonNull(), $"{firstFile.Name}-changed{firstFile.Extension}"));
+
+ using (var zip = ZipArchive.Create())
+ {
+ zip.AddAllFromDirectory(extractedFolder);
+ zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
+ }
+
+ var importedSecondTime = await importer.Import(new ImportTask(temp));
+
+ ensureLoaded(realmFactory.Context);
+
+ Assert.NotNull(importedSecondTime);
+ Debug.Assert(importedSecondTime != null);
+
+ // check the newly "imported" beatmap is not the original.
+ Assert.IsTrue(imported.ID != importedSecondTime.ID);
+ Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.PerformRead(s => s.Beatmaps.First().ID));
+ }
+ finally
+ {
+ Directory.Delete(extractedFolder, true);
+ }
+ });
+ }
+
+ [Test]
+ [Ignore("intentionally broken by import optimisations")]
+ public void TestImportCorruptThenImport()
+ {
+ RunTestWithRealmAsync(async (realmFactory, storage) =>
+ {
+ using var importer = new BeatmapImporter(realmFactory, storage);
+ using var store = new RealmRulesetStore(realmFactory, storage);
+
+ var imported = await LoadOszIntoStore(importer, realmFactory.Context);
+
+ var firstFile = imported.Files.First();
+
+ long originalLength;
+ using (var stream = storage.GetStream(firstFile.File.StoragePath))
+ originalLength = stream.Length;
+
+ using (var stream = storage.GetStream(firstFile.File.StoragePath, FileAccess.Write, FileMode.Create))
+ stream.WriteByte(0);
+
+ var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context);
+
+ using (var stream = storage.GetStream(firstFile.File.StoragePath))
+ Assert.AreEqual(stream.Length, originalLength, "Corruption was not fixed on second import");
+
+ // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
+ Assert.IsTrue(imported.ID == importedSecondTime.ID);
+ Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
+
+ checkBeatmapSetCount(realmFactory.Context, 1);
+ checkSingleReferencedFileCount(realmFactory.Context, 18);
+ });
+ }
+
+ [Test]
+ public void TestRollbackOnFailure()
+ {
+ RunTestWithRealmAsync(async (realmFactory, storage) =>
+ {
+ int loggedExceptionCount = 0;
+
+ Logger.NewEntry += l =>
+ {
+ if (l.Target == LoggingTarget.Database && l.Exception != null)
+ Interlocked.Increment(ref loggedExceptionCount);
+ };
+
+ using var importer = new BeatmapImporter(realmFactory, storage);
+ using var store = new RealmRulesetStore(realmFactory, storage);
+
+ var imported = await LoadOszIntoStore(importer, realmFactory.Context);
+
+ realmFactory.Context.Write(() => imported.Hash += "-changed");
+
+ checkBeatmapSetCount(realmFactory.Context, 1);
+ checkBeatmapCount(realmFactory.Context, 12);
+ checkSingleReferencedFileCount(realmFactory.Context, 18);
+
+ var brokenTempFilename = TestResources.GetTestBeatmapForImport();
+
+ MemoryStream brokenOsu = new MemoryStream();
+ MemoryStream brokenOsz = new MemoryStream(await File.ReadAllBytesAsync(brokenTempFilename));
+
+ File.Delete(brokenTempFilename);
+
+ using (var outStream = File.Open(brokenTempFilename, FileMode.CreateNew))
+ using (var zip = ZipArchive.Open(brokenOsz))
+ {
+ zip.AddEntry("broken.osu", brokenOsu, false);
+ zip.SaveTo(outStream, CompressionType.Deflate);
+ }
+
+ // this will trigger purging of the existing beatmap (online set id match) but should rollback due to broken osu.
+ try
+ {
+ await importer.Import(new ImportTask(brokenTempFilename));
+ }
+ catch
+ {
+ }
+
+ checkBeatmapSetCount(realmFactory.Context, 1);
+ checkBeatmapCount(realmFactory.Context, 12);
+
+ checkSingleReferencedFileCount(realmFactory.Context, 18);
+
+ Assert.AreEqual(1, loggedExceptionCount);
+
+ File.Delete(brokenTempFilename);
+ });
+ }
+
+ [Test]
+ public void TestImportThenDeleteThenImport()
+ {
+ RunTestWithRealmAsync(async (realmFactory, storage) =>
+ {
+ using var importer = new BeatmapImporter(realmFactory, storage);
+ using var store = new RealmRulesetStore(realmFactory, storage);
+
+ var imported = await LoadOszIntoStore(importer, realmFactory.Context);
+
+ deleteBeatmapSet(imported, realmFactory.Context);
+
+ var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context);
+
+ // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
+ Assert.IsTrue(imported.ID == importedSecondTime.ID);
+ Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
+ });
+ }
+
+ [Test]
+ public void TestImportThenDeleteThenImportWithOnlineIDsMissing()
+ {
+ RunTestWithRealmAsync(async (realmFactory, storage) =>
+ {
+ using var importer = new BeatmapImporter(realmFactory, storage);
+ using var store = new RealmRulesetStore(realmFactory, storage);
+
+ var imported = await LoadOszIntoStore(importer, realmFactory.Context);
+
+ realmFactory.Context.Write(() =>
+ {
+ foreach (var b in imported.Beatmaps)
+ b.OnlineID = -1;
+ });
+
+ deleteBeatmapSet(imported, realmFactory.Context);
+
+ var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context);
+
+ // check the newly "imported" beatmap has been reimported due to mismatch (even though hashes matched)
+ Assert.IsTrue(imported.ID != importedSecondTime.ID);
+ Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID);
+ });
+ }
+
+ [Test]
+ public void TestImportWithDuplicateBeatmapIDs()
+ {
+ RunTestWithRealmAsync(async (realmFactory, storage) =>
+ {
+ using var importer = new BeatmapImporter(realmFactory, storage);
+ using var store = new RealmRulesetStore(realmFactory, storage);
+
+ var metadata = new RealmBeatmapMetadata
+ {
+ Artist = "SomeArtist",
+ Author = "SomeAuthor"
+ };
+
+ var ruleset = realmFactory.Context.All().First();
+
+ var toImport = new RealmBeatmapSet
+ {
+ OnlineID = 1,
+ Beatmaps =
+ {
+ new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata)
+ {
+ OnlineID = 2,
+ },
+ new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata)
+ {
+ OnlineID = 2,
+ Status = BeatmapSetOnlineStatus.Loved,
+ }
+ }
+ };
+
+ var imported = await importer.Import(toImport);
+
+ Assert.NotNull(imported);
+ Debug.Assert(imported != null);
+
+ Assert.AreEqual(-1, imported.PerformRead(s => s.Beatmaps[0].OnlineID));
+ Assert.AreEqual(-1, imported.PerformRead(s => s.Beatmaps[1].OnlineID));
+ });
+ }
+
+ [Test]
+ public void TestImportWhenFileOpen()
+ {
+ RunTestWithRealmAsync(async (realmFactory, storage) =>
+ {
+ using var importer = new BeatmapImporter(realmFactory, storage);
+ using var store = new RealmRulesetStore(realmFactory, storage);
+
+ var temp = TestResources.GetTestBeatmapForImport();
+ using (File.OpenRead(temp))
+ await importer.Import(temp);
+ ensureLoaded(realmFactory.Context);
+ File.Delete(temp);
+ Assert.IsFalse(File.Exists(temp), "We likely held a read lock on the file when we shouldn't");
+ });
+ }
+
+ [Test]
+ public void TestImportWithDuplicateHashes()
+ {
+ RunTestWithRealmAsync(async (realmFactory, storage) =>
+ {
+ using var importer = new BeatmapImporter(realmFactory, storage);
+ using var store = new RealmRulesetStore(realmFactory, storage);
+
+ var temp = TestResources.GetTestBeatmapForImport();
+
+ string extractedFolder = $"{temp}_extracted";
+ Directory.CreateDirectory(extractedFolder);
+
+ try
+ {
+ using (var zip = ZipArchive.Open(temp))
+ zip.WriteToDirectory(extractedFolder);
+
+ using (var zip = ZipArchive.Create())
+ {
+ zip.AddAllFromDirectory(extractedFolder);
+ zip.AddEntry("duplicate.osu", Directory.GetFiles(extractedFolder, "*.osu").First());
+ zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
+ }
+
+ await importer.Import(temp);
+
+ ensureLoaded(realmFactory.Context);
+ }
+ finally
+ {
+ Directory.Delete(extractedFolder, true);
+ }
+ });
+ }
+
+ [Test]
+ public void TestImportNestedStructure()
+ {
+ RunTestWithRealmAsync(async (realmFactory, storage) =>
+ {
+ using var importer = new BeatmapImporter(realmFactory, storage);
+ using var store = new RealmRulesetStore(realmFactory, storage);
+
+ var temp = TestResources.GetTestBeatmapForImport();
+
+ string extractedFolder = $"{temp}_extracted";
+ string subfolder = Path.Combine(extractedFolder, "subfolder");
+
+ Directory.CreateDirectory(subfolder);
+
+ try
+ {
+ using (var zip = ZipArchive.Open(temp))
+ zip.WriteToDirectory(subfolder);
+
+ using (var zip = ZipArchive.Create())
+ {
+ zip.AddAllFromDirectory(extractedFolder);
+ zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
+ }
+
+ var imported = await importer.Import(new ImportTask(temp));
+
+ Assert.NotNull(imported);
+ Debug.Assert(imported != null);
+
+ ensureLoaded(realmFactory.Context);
+
+ Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("subfolder"))), "Files contain common subfolder");
+ }
+ finally
+ {
+ Directory.Delete(extractedFolder, true);
+ }
+ });
+ }
+
+ [Test]
+ public void TestImportWithIgnoredDirectoryInArchive()
+ {
+ RunTestWithRealmAsync(async (realmFactory, storage) =>
+ {
+ using var importer = new BeatmapImporter(realmFactory, storage);
+ using var store = new RealmRulesetStore(realmFactory, storage);
+
+ var temp = TestResources.GetTestBeatmapForImport();
+
+ string extractedFolder = $"{temp}_extracted";
+ string dataFolder = Path.Combine(extractedFolder, "actual_data");
+ string resourceForkFolder = Path.Combine(extractedFolder, "__MACOSX");
+ string resourceForkFilePath = Path.Combine(resourceForkFolder, ".extracted");
+
+ Directory.CreateDirectory(dataFolder);
+ Directory.CreateDirectory(resourceForkFolder);
+
+ using (var resourceForkFile = File.CreateText(resourceForkFilePath))
+ {
+ await resourceForkFile.WriteLineAsync("adding content so that it's not empty");
+ }
+
+ try
+ {
+ using (var zip = ZipArchive.Open(temp))
+ zip.WriteToDirectory(dataFolder);
+
+ using (var zip = ZipArchive.Create())
+ {
+ zip.AddAllFromDirectory(extractedFolder);
+ zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
+ }
+
+ var imported = await importer.Import(new ImportTask(temp));
+
+ Assert.NotNull(imported);
+ Debug.Assert(imported != null);
+
+ ensureLoaded(realmFactory.Context);
+
+ Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("__MACOSX"))), "Files contain resource fork folder, which should be ignored");
+ Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("actual_data"))), "Files contain common subfolder");
+ }
+ finally
+ {
+ Directory.Delete(extractedFolder, true);
+ }
+ });
+ }
+
+ [Test]
+ public void TestUpdateBeatmapInfo()
+ {
+ RunTestWithRealmAsync(async (realmFactory, storage) =>
+ {
+ using var importer = new BeatmapImporter(realmFactory, storage);
+ using var store = new RealmRulesetStore(realmFactory, storage);
+
+ var temp = TestResources.GetTestBeatmapForImport();
+ await importer.Import(temp);
+
+ // Update via the beatmap, not the beatmap info, to ensure correct linking
+ RealmBeatmapSet setToUpdate = realmFactory.Context.All().First();
+
+ var beatmapToUpdate = setToUpdate.Beatmaps.First();
+
+ realmFactory.Context.Write(() => beatmapToUpdate.DifficultyName = "updated");
+
+ RealmBeatmap updatedInfo = realmFactory.Context.All().First(b => b.ID == beatmapToUpdate.ID);
+ Assert.That(updatedInfo.DifficultyName, Is.EqualTo("updated"));
+ });
+ }
+
+ public static async Task LoadQuickOszIntoOsu(BeatmapImporter importer, Realm realm)
+ {
+ var temp = TestResources.GetQuickTestBeatmapForImport();
+
+ var importedSet = await importer.Import(new ImportTask(temp));
+
+ Assert.NotNull(importedSet);
+
+ ensureLoaded(realm);
+
+ waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000);
+
+ return realm.All().FirstOrDefault(beatmapSet => beatmapSet.ID == importedSet!.ID);
+ }
+
+ public static async Task LoadOszIntoStore(BeatmapImporter importer, Realm realm, string? path = null, bool virtualTrack = false)
+ {
+ var temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack);
+
+ var importedSet = await importer.Import(new ImportTask(temp));
+
+ Assert.NotNull(importedSet);
+ Debug.Assert(importedSet != null);
+
+ ensureLoaded(realm);
+
+ waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000);
+
+ return realm.All().First(beatmapSet => beatmapSet.ID == importedSet.ID);
+ }
+
+ private void deleteBeatmapSet(RealmBeatmapSet imported, Realm realm)
+ {
+ realm.Write(() => imported.DeletePending = true);
+
+ checkBeatmapSetCount(realm, 0);
+ checkBeatmapSetCount(realm, 1, true);
+
+ Assert.IsTrue(realm.All().First(_ => true).DeletePending);
+ }
+
+ private static Task createScoreForBeatmap(Realm realm, RealmBeatmap beatmap)
+ {
+ // TODO: reimplement when we have score support in realm.
+ // return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo
+ // {
+ // OnlineScoreID = 2,
+ // Beatmap = beatmap,
+ // BeatmapInfoID = beatmap.ID
+ // }, new ImportScoreTest.TestArchiveReader());
+
+ return Task.CompletedTask;
+ }
+
+ private static void checkBeatmapSetCount(Realm realm, int expected, bool includeDeletePending = false)
+ {
+ Assert.AreEqual(expected, includeDeletePending
+ ? realm.All().Count()
+ : realm.All().Count(s => !s.DeletePending));
+ }
+
+ private static string hashFile(string filename)
+ {
+ using (var s = File.OpenRead(filename))
+ return s.ComputeMD5Hash();
+ }
+
+ private static void checkBeatmapCount(Realm realm, int expected)
+ {
+ Assert.AreEqual(expected, realm.All().Where(_ => true).ToList().Count);
+ }
+
+ private static void checkSingleReferencedFileCount(Realm realm, int expected)
+ {
+ int singleReferencedCount = 0;
+
+ foreach (var f in realm.All())
+ {
+ if (f.BacklinksCount == 1)
+ singleReferencedCount++;
+ }
+
+ Assert.AreEqual(expected, singleReferencedCount);
+ }
+
+ private static void ensureLoaded(Realm realm, int timeout = 60000)
+ {
+ IQueryable? resultSets = null;
+
+ waitForOrAssert(() => (resultSets = realm.All().Where(s => !s.DeletePending && s.OnlineID == 241526)).Any(),
+ @"BeatmapSet did not import to the database in allocated time.", timeout);
+
+ // ensure we were stored to beatmap database backing...
+ Assert.IsTrue(resultSets?.Count() == 1, $@"Incorrect result count found ({resultSets?.Count()} but should be 1).");
+
+ IEnumerable queryBeatmapSets() => realm.All().Where(s => !s.DeletePending && s.OnlineID == 241526);
+
+ var set = queryBeatmapSets().First();
+
+ // ReSharper disable once PossibleUnintendedReferenceComparison
+ IEnumerable queryBeatmaps() => realm.All().Where(s => s.BeatmapSet != null && s.BeatmapSet == set);
+
+ waitForOrAssert(() => queryBeatmaps().Count() == 12, @"Beatmaps did not import to the database in allocated time", timeout);
+ waitForOrAssert(() => queryBeatmapSets().Count() == 1, @"BeatmapSet did not import to the database in allocated time", timeout);
+
+ int countBeatmapSetBeatmaps = 0;
+ int countBeatmaps = 0;
+
+ waitForOrAssert(() =>
+ (countBeatmapSetBeatmaps = queryBeatmapSets().First().Beatmaps.Count) ==
+ (countBeatmaps = queryBeatmaps().Count()),
+ $@"Incorrect database beatmap count post-import ({countBeatmaps} but should be {countBeatmapSetBeatmaps}).", timeout);
+
+ foreach (RealmBeatmap b in set.Beatmaps)
+ Assert.IsTrue(set.Beatmaps.Any(c => c.OnlineID == b.OnlineID));
+ Assert.IsTrue(set.Beatmaps.Count > 0);
+ }
+
+ private static void waitForOrAssert(Func result, string failureMessage, int timeout = 60000)
+ {
+ const int sleep = 200;
+
+ while (timeout > 0)
+ {
+ Thread.Sleep(sleep);
+ timeout -= sleep;
+
+ if (result())
+ return;
+ }
+
+ Assert.Fail(failureMessage);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Database/FileStoreTests.cs b/osu.Game.Tests/Database/FileStoreTests.cs
new file mode 100644
index 0000000000..861de5303d
--- /dev/null
+++ b/osu.Game.Tests/Database/FileStoreTests.cs
@@ -0,0 +1,114 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Logging;
+using osu.Game.Models;
+using osu.Game.Stores;
+
+#nullable enable
+
+namespace osu.Game.Tests.Database
+{
+ public class FileStoreTests : RealmTest
+ {
+ [Test]
+ public void TestImportFile()
+ {
+ RunTestWithRealm((realmFactory, storage) =>
+ {
+ var realm = realmFactory.Context;
+ var files = new RealmFileStore(realmFactory, storage);
+
+ var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 });
+
+ realm.Write(() => files.Add(testData, realm));
+
+ Assert.True(files.Storage.Exists("0/05/054edec1d0211f624fed0cbca9d4f9400b0e491c43742af2c5b0abebf0c990d8"));
+ Assert.True(files.Storage.Exists(realm.All().First().StoragePath));
+ });
+ }
+
+ [Test]
+ public void TestImportSameFileTwice()
+ {
+ RunTestWithRealm((realmFactory, storage) =>
+ {
+ var realm = realmFactory.Context;
+ var files = new RealmFileStore(realmFactory, storage);
+
+ var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 });
+
+ realm.Write(() => files.Add(testData, realm));
+ realm.Write(() => files.Add(testData, realm));
+
+ Assert.AreEqual(1, realm.All().Count());
+ });
+ }
+
+ [Test]
+ public void TestDontPurgeReferenced()
+ {
+ RunTestWithRealm((realmFactory, storage) =>
+ {
+ var realm = realmFactory.Context;
+ var files = new RealmFileStore(realmFactory, storage);
+
+ var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm));
+
+ var timer = new Stopwatch();
+ timer.Start();
+
+ realm.Write(() =>
+ {
+ // attach the file to an arbitrary beatmap
+ var beatmapSet = CreateBeatmapSet(CreateRuleset());
+
+ beatmapSet.Files.Add(new RealmNamedFileUsage(file, "arbitrary.resource"));
+
+ realm.Add(beatmapSet);
+ });
+
+ Logger.Log($"Import complete at {timer.ElapsedMilliseconds}");
+
+ string path = file.StoragePath;
+
+ Assert.True(realm.All().Any());
+ Assert.True(files.Storage.Exists(path));
+
+ files.Cleanup();
+ Logger.Log($"Cleanup complete at {timer.ElapsedMilliseconds}");
+
+ Assert.True(realm.All().Any());
+ Assert.True(file.IsValid);
+ Assert.True(files.Storage.Exists(path));
+ });
+ }
+
+ [Test]
+ public void TestPurgeUnreferenced()
+ {
+ RunTestWithRealm((realmFactory, storage) =>
+ {
+ var realm = realmFactory.Context;
+ var files = new RealmFileStore(realmFactory, storage);
+
+ var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm));
+
+ string path = file.StoragePath;
+
+ Assert.True(realm.All().Any());
+ Assert.True(files.Storage.Exists(path));
+
+ files.Cleanup();
+
+ Assert.False(realm.All().Any());
+ Assert.False(file.IsValid);
+ Assert.False(files.Storage.Exists(path));
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs
index 245981cd9b..3e8b6091fd 100644
--- a/osu.Game.Tests/Database/GeneralUsageTests.cs
+++ b/osu.Game.Tests/Database/GeneralUsageTests.cs
@@ -1,3 +1,6 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
using System;
using System.Threading;
using System.Threading.Tasks;
diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs
new file mode 100644
index 0000000000..33aa1afb89
--- /dev/null
+++ b/osu.Game.Tests/Database/RealmLiveTests.cs
@@ -0,0 +1,213 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Database;
+using osu.Game.Models;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Tests.Database
+{
+ public class RealmLiveTests : RealmTest
+ {
+ [Test]
+ public void TestLiveCastability()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ RealmLive beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive();
+
+ ILive iBeatmap = beatmap;
+
+ Assert.AreEqual(0, iBeatmap.Value.Length);
+ });
+ }
+
+ [Test]
+ public void TestValueAccessWithOpenContext()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ RealmLive? liveBeatmap = null;
+ Task.Factory.StartNew(() =>
+ {
+ using (var threadContext = realmFactory.CreateContext())
+ {
+ var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ liveBeatmap = beatmap.ToLive();
+ }
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+
+ Debug.Assert(liveBeatmap != null);
+
+ Task.Factory.StartNew(() =>
+ {
+ Assert.DoesNotThrow(() =>
+ {
+ using (realmFactory.CreateContext())
+ {
+ var resolved = liveBeatmap.Value;
+
+ Assert.IsTrue(resolved.Realm.IsClosed);
+ Assert.IsTrue(resolved.IsValid);
+
+ // can access properties without a crash.
+ Assert.IsFalse(resolved.Hidden);
+ }
+ });
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+ });
+ }
+
+ [Test]
+ public void TestScopedReadWithoutContext()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ RealmLive? liveBeatmap = null;
+ Task.Factory.StartNew(() =>
+ {
+ using (var threadContext = realmFactory.CreateContext())
+ {
+ var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ liveBeatmap = beatmap.ToLive();
+ }
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+
+ Debug.Assert(liveBeatmap != null);
+
+ Task.Factory.StartNew(() =>
+ {
+ liveBeatmap.PerformRead(beatmap =>
+ {
+ Assert.IsTrue(beatmap.IsValid);
+ Assert.IsFalse(beatmap.Hidden);
+ });
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+ });
+ }
+
+ [Test]
+ public void TestScopedWriteWithoutContext()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ RealmLive? liveBeatmap = null;
+ Task.Factory.StartNew(() =>
+ {
+ using (var threadContext = realmFactory.CreateContext())
+ {
+ var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ liveBeatmap = beatmap.ToLive();
+ }
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+
+ Debug.Assert(liveBeatmap != null);
+
+ Task.Factory.StartNew(() =>
+ {
+ liveBeatmap.PerformWrite(beatmap => { beatmap.Hidden = true; });
+ liveBeatmap.PerformRead(beatmap => { Assert.IsTrue(beatmap.Hidden); });
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+ });
+ }
+
+ [Test]
+ public void TestValueAccessWithoutOpenContextFails()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ RealmLive? liveBeatmap = null;
+ Task.Factory.StartNew(() =>
+ {
+ using (var threadContext = realmFactory.CreateContext())
+ {
+ var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ liveBeatmap = beatmap.ToLive();
+ }
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+
+ Debug.Assert(liveBeatmap != null);
+
+ Task.Factory.StartNew(() =>
+ {
+ Assert.Throws(() =>
+ {
+ var unused = liveBeatmap.Value;
+ });
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+ });
+ }
+
+ [Test]
+ public void TestLiveAssumptions()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ int changesTriggered = 0;
+
+ using (var updateThreadContext = realmFactory.CreateContext())
+ {
+ updateThreadContext.All().SubscribeForNotifications(gotChange);
+ RealmLive? liveBeatmap = null;
+
+ Task.Factory.StartNew(() =>
+ {
+ using (var threadContext = realmFactory.CreateContext())
+ {
+ var ruleset = CreateRuleset();
+ var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ // add a second beatmap to ensure that a full refresh occurs below.
+ // not just a refresh from the resolved Live.
+ threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ liveBeatmap = beatmap.ToLive();
+ }
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+
+ Debug.Assert(liveBeatmap != null);
+
+ // not yet seen by main context
+ Assert.AreEqual(0, updateThreadContext.All().Count());
+ Assert.AreEqual(0, changesTriggered);
+
+ var resolved = liveBeatmap.Value;
+
+ // retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
+ Assert.AreEqual(2, updateThreadContext.All().Count());
+ Assert.AreEqual(1, changesTriggered);
+
+ // even though the realm that this instance was resolved for was closed, it's still valid.
+ Assert.IsTrue(resolved.Realm.IsClosed);
+ Assert.IsTrue(resolved.IsValid);
+
+ // can access properties without a crash.
+ Assert.IsFalse(resolved.Hidden);
+
+ updateThreadContext.Write(r =>
+ {
+ // can use with the main context.
+ r.Remove(resolved);
+ });
+ }
+
+ void gotChange(IRealmCollection sender, ChangeSet changes, Exception error)
+ {
+ changesTriggered++;
+ }
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs
index 576f901c1a..04c9f2577a 100644
--- a/osu.Game.Tests/Database/RealmTest.cs
+++ b/osu.Game.Tests/Database/RealmTest.cs
@@ -4,12 +4,13 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
-using Nito.AsyncEx;
using NUnit.Framework;
+using osu.Framework.Extensions;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Database;
+using osu.Game.Models;
#nullable enable
@@ -28,42 +29,109 @@ namespace osu.Game.Tests.Database
protected void RunTestWithRealm(Action testAction, [CallerMemberName] string caller = "")
{
- AsyncContext.Run(() =>
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(caller))
{
- var testStorage = storage.GetStorageForDirectory(caller);
-
- using (var realmFactory = new RealmContextFactory(testStorage, caller))
+ host.Run(new RealmTestGame(() =>
{
- Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
- testAction(realmFactory, testStorage);
+ var testStorage = storage.GetStorageForDirectory(caller);
- realmFactory.Dispose();
+ using (var realmFactory = new RealmContextFactory(testStorage, caller))
+ {
+ Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
+ testAction(realmFactory, testStorage);
- Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}");
- realmFactory.Compact();
- Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}");
- }
- });
+ realmFactory.Dispose();
+
+ Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}");
+ realmFactory.Compact();
+ Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}");
+ }
+ }));
+ }
}
protected void RunTestWithRealmAsync(Func testAction, [CallerMemberName] string caller = "")
{
- AsyncContext.Run(async () =>
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(caller))
{
- var testStorage = storage.GetStorageForDirectory(caller);
-
- using (var realmFactory = new RealmContextFactory(testStorage, caller))
+ host.Run(new RealmTestGame(async () =>
{
- Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
- await testAction(realmFactory, testStorage);
+ var testStorage = storage.GetStorageForDirectory(caller);
- realmFactory.Dispose();
+ using (var realmFactory = new RealmContextFactory(testStorage, caller))
+ {
+ Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}");
+ await testAction(realmFactory, testStorage);
- Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}");
- realmFactory.Compact();
- Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}");
+ realmFactory.Dispose();
+
+ Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}");
+ realmFactory.Compact();
+ }
+ }));
+ }
+ }
+
+ protected static RealmBeatmapSet CreateBeatmapSet(RealmRuleset ruleset)
+ {
+ RealmFile createRealmFile() => new RealmFile { Hash = Guid.NewGuid().ToString().ComputeSHA2Hash() };
+
+ var metadata = new RealmBeatmapMetadata
+ {
+ Title = "My Love",
+ Artist = "Kuba Oms"
+ };
+
+ var beatmapSet = new RealmBeatmapSet
+ {
+ Beatmaps =
+ {
+ new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Easy", },
+ new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Normal", },
+ new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Hard", },
+ new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Insane", }
+ },
+ Files =
+ {
+ new RealmNamedFileUsage(createRealmFile(), "test [easy].osu"),
+ new RealmNamedFileUsage(createRealmFile(), "test [normal].osu"),
+ new RealmNamedFileUsage(createRealmFile(), "test [hard].osu"),
+ new RealmNamedFileUsage(createRealmFile(), "test [insane].osu"),
}
- });
+ };
+
+ for (int i = 0; i < 8; i++)
+ beatmapSet.Files.Add(new RealmNamedFileUsage(createRealmFile(), $"hitsound{i}.mp3"));
+
+ foreach (var b in beatmapSet.Beatmaps)
+ b.BeatmapSet = beatmapSet;
+
+ return beatmapSet;
+ }
+
+ protected static RealmRuleset CreateRuleset() =>
+ new RealmRuleset(0, "osu!", "osu", true);
+
+ private class RealmTestGame : Framework.Game
+ {
+ public RealmTestGame(Func work)
+ {
+ // ReSharper disable once AsyncVoidLambda
+ Scheduler.Add(async () =>
+ {
+ await work().ConfigureAwait(true);
+ Exit();
+ });
+ }
+
+ public RealmTestGame(Action work)
+ {
+ Scheduler.Add(() =>
+ {
+ work();
+ Exit();
+ });
+ }
}
private static long getFileSize(Storage testStorage, RealmContextFactory realmFactory)
diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs
new file mode 100644
index 0000000000..f4e0838be1
--- /dev/null
+++ b/osu.Game.Tests/Database/RulesetStoreTests.cs
@@ -0,0 +1,54 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Game.Models;
+using osu.Game.Stores;
+
+namespace osu.Game.Tests.Database
+{
+ public class RulesetStoreTests : RealmTest
+ {
+ [Test]
+ public void TestCreateStore()
+ {
+ RunTestWithRealm((realmFactory, storage) =>
+ {
+ var rulesets = new RealmRulesetStore(realmFactory, storage);
+
+ Assert.AreEqual(4, rulesets.AvailableRulesets.Count());
+ Assert.AreEqual(4, realmFactory.Context.All().Count());
+ });
+ }
+
+ [Test]
+ public void TestCreateStoreTwiceDoesntAddRulesetsAgain()
+ {
+ RunTestWithRealm((realmFactory, storage) =>
+ {
+ var rulesets = new RealmRulesetStore(realmFactory, storage);
+ var rulesets2 = new RealmRulesetStore(realmFactory, storage);
+
+ Assert.AreEqual(4, rulesets.AvailableRulesets.Count());
+ Assert.AreEqual(4, rulesets2.AvailableRulesets.Count());
+
+ Assert.AreEqual(rulesets.AvailableRulesets.First(), rulesets2.AvailableRulesets.First());
+ Assert.AreEqual(4, realmFactory.Context.All().Count());
+ });
+ }
+
+ [Test]
+ public void TestRetrievedRulesetsAreDetached()
+ {
+ RunTestWithRealm((realmFactory, storage) =>
+ {
+ var rulesets = new RealmRulesetStore(realmFactory, storage);
+
+ Assert.IsTrue((rulesets.AvailableRulesets.First() as RealmRuleset)?.IsManaged == false);
+ Assert.IsTrue((rulesets.GetRuleset(0) as RealmRuleset)?.IsManaged == false);
+ Assert.IsTrue((rulesets.GetRuleset("mania") as RealmRuleset)?.IsManaged == false);
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs b/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs
new file mode 100644
index 0000000000..f3a4f10210
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs
@@ -0,0 +1,104 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Moq;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Storyboards;
+using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Resources;
+using FileInfo = osu.Game.IO.FileInfo;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ [TestFixture]
+ public class CheckAudioInVideoTest
+ {
+ private CheckAudioInVideo check;
+ private IBeatmap beatmap;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckAudioInVideo();
+ beatmap = new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ BeatmapSet = new BeatmapSetInfo
+ {
+ Files = new List(new[]
+ {
+ new BeatmapSetFileInfo
+ {
+ Filename = "abc123.mp4",
+ FileInfo = new FileInfo { Hash = "abcdef" }
+ }
+ })
+ }
+ }
+ };
+ }
+
+ [Test]
+ public void TestRegularVideoFile()
+ {
+ using (var resourceStream = TestResources.OpenResource("Videos/test-video.mp4"))
+ Assert.IsEmpty(check.Run(getContext(resourceStream)));
+ }
+
+ [Test]
+ public void TestVideoFileWithAudio()
+ {
+ using (var resourceStream = TestResources.OpenResource("Videos/test-video-with-audio.mp4"))
+ {
+ var issues = check.Run(getContext(resourceStream)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateHasAudioTrack);
+ }
+ }
+
+ [Test]
+ public void TestVideoFileWithTrackButNoAudio()
+ {
+ using (var resourceStream = TestResources.OpenResource("Videos/test-video-with-track-but-no-audio.mp4"))
+ {
+ var issues = check.Run(getContext(resourceStream)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateHasAudioTrack);
+ }
+ }
+
+ [Test]
+ public void TestMissingFile()
+ {
+ beatmap.BeatmapInfo.BeatmapSet.Files.Clear();
+
+ var issues = check.Run(getContext(null)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateMissingFile);
+ }
+
+ private BeatmapVerifierContext getContext(Stream resourceStream)
+ {
+ var storyboard = new Storyboard();
+ var layer = storyboard.GetLayer("Video");
+ layer.Add(new StoryboardVideo("abc123.mp4", 0));
+
+ var mockWorkingBeatmap = new Mock(beatmap, null, null);
+ mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(resourceStream);
+ mockWorkingBeatmap.As().SetupGet(w => w.Storyboard).Returns(storyboard);
+
+ return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs
new file mode 100644
index 0000000000..9b090591bc
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs
@@ -0,0 +1,128 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using ManagedBass;
+using Moq;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Resources;
+using osuTK.Audio;
+using FileInfo = osu.Game.IO.FileInfo;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ [TestFixture]
+ public class CheckTooShortAudioFilesTest
+ {
+ private CheckTooShortAudioFiles check;
+ private IBeatmap beatmap;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckTooShortAudioFiles();
+ beatmap = new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ BeatmapSet = new BeatmapSetInfo
+ {
+ Files = new List(new[]
+ {
+ new BeatmapSetFileInfo
+ {
+ Filename = "abc123.wav",
+ FileInfo = new FileInfo { Hash = "abcdef" }
+ }
+ })
+ }
+ }
+ };
+
+ // 0 = No output device. This still allows decoding.
+ if (!Bass.Init(0) && Bass.LastError != Errors.Already)
+ throw new AudioException("Could not initialize Bass.");
+ }
+
+ [Test]
+ public void TestDifferentExtension()
+ {
+ beatmap.BeatmapInfo.BeatmapSet.Files.Clear();
+ beatmap.BeatmapInfo.BeatmapSet.Files.Add(new BeatmapSetFileInfo
+ {
+ Filename = "abc123.jpg",
+ FileInfo = new FileInfo { Hash = "abcdef" }
+ });
+
+ // Should fail to load, but not produce an error due to the extension not being expected to load.
+ Assert.IsEmpty(check.Run(getContext(null, allowMissing: true)));
+ }
+
+ [Test]
+ public void TestRegularAudioFile()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/test-sample.mp3"))
+ {
+ Assert.IsEmpty(check.Run(getContext(resourceStream)));
+ }
+ }
+
+ [Test]
+ public void TestBlankAudioFile()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/blank.wav"))
+ {
+ // This is a 0 ms duration audio file, commonly used to silence sliderslides/ticks, and so should be fine.
+ Assert.IsEmpty(check.Run(getContext(resourceStream)));
+ }
+ }
+
+ [Test]
+ public void TestTooShortAudioFile()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/test-sample-cut.mp3"))
+ {
+ var issues = check.Run(getContext(resourceStream)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckTooShortAudioFiles.IssueTemplateTooShort);
+ }
+ }
+
+ [Test]
+ public void TestMissingAudioFile()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/missing.mp3"))
+ {
+ Assert.IsEmpty(check.Run(getContext(resourceStream, allowMissing: true)));
+ }
+ }
+
+ [Test]
+ public void TestCorruptAudioFile()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav"))
+ {
+ var issues = check.Run(getContext(resourceStream)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckTooShortAudioFiles.IssueTemplateBadFormat);
+ }
+ }
+
+ private BeatmapVerifierContext getContext(Stream resourceStream, bool allowMissing = false)
+ {
+ var mockWorkingBeatmap = new Mock(beatmap, null, null);
+ mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(resourceStream);
+
+ return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/Checks/CheckZeroByteFilesTest.cs b/osu.Game.Tests/Editing/Checks/CheckZeroByteFilesTest.cs
new file mode 100644
index 0000000000..c9adc030c1
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckZeroByteFilesTest.cs
@@ -0,0 +1,86 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Moq;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using FileInfo = osu.Game.IO.FileInfo;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ [TestFixture]
+ public class CheckZeroByteFilesTest
+ {
+ private CheckZeroByteFiles check;
+ private IBeatmap beatmap;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckZeroByteFiles();
+ beatmap = new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ BeatmapSet = new BeatmapSetInfo
+ {
+ Files = new List(new[]
+ {
+ new BeatmapSetFileInfo
+ {
+ Filename = "abc123.jpg",
+ FileInfo = new FileInfo { Hash = "abcdef" }
+ }
+ })
+ }
+ }
+ };
+ }
+
+ [Test]
+ public void TestNonZeroBytes()
+ {
+ Assert.IsEmpty(check.Run(getContext(byteLength: 44)));
+ }
+
+ [Test]
+ public void TestZeroBytes()
+ {
+ var issues = check.Run(getContext(byteLength: 0)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckZeroByteFiles.IssueTemplateZeroBytes);
+ }
+
+ [Test]
+ public void TestMissing()
+ {
+ Assert.IsEmpty(check.Run(getContextMissing()));
+ }
+
+ private BeatmapVerifierContext getContext(long byteLength)
+ {
+ var mockStream = new Mock();
+ mockStream.Setup(s => s.Length).Returns(byteLength);
+
+ var mockWorkingBeatmap = new Mock();
+ mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(mockStream.Object);
+
+ return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
+ }
+
+ private BeatmapVerifierContext getContextMissing()
+ {
+ var mockWorkingBeatmap = new Mock();
+ mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns((Stream)null);
+
+ return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
index a40a6dac4c..8eb9452736 100644
--- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
+++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
@@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Edit;
@@ -55,8 +56,6 @@ namespace osu.Game.Tests.Editing
composer.EditorBeatmap.Difficulty.SliderMultiplier = 1;
composer.EditorBeatmap.ControlPointInfo.Clear();
-
- composer.EditorBeatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 1 });
composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 });
});
@@ -73,13 +72,13 @@ namespace osu.Game.Tests.Editing
[TestCase(2)]
public void TestSpeedMultiplier(float multiplier)
{
- AddStep($"set multiplier = {multiplier}", () =>
+ assertSnapDistance(100 * multiplier, new HitObject
{
- composer.EditorBeatmap.ControlPointInfo.Clear();
- composer.EditorBeatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = multiplier });
+ DifficultyControlPoint = new DifficultyControlPoint
+ {
+ SliderVelocity = multiplier
+ }
});
-
- assertSnapDistance(100 * multiplier);
}
[TestCase(1)]
@@ -197,20 +196,20 @@ namespace osu.Game.Tests.Editing
assertSnappedDistance(400, 400);
}
- private void assertSnapDistance(float expectedDistance)
- => AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(0) == expectedDistance);
+ private void assertSnapDistance(float expectedDistance, HitObject hitObject = null)
+ => AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(hitObject ?? new HitObject()) == expectedDistance);
private void assertDurationToDistance(double duration, float expectedDistance)
- => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(0, duration) == expectedDistance);
+ => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(new HitObject(), duration) == expectedDistance);
private void assertDistanceToDuration(float distance, double expectedDuration)
- => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(0, distance) == expectedDuration);
+ => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(new HitObject(), distance) == expectedDuration);
private void assertSnappedDuration(float distance, double expectedDuration)
- => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.GetSnappedDurationFromDistance(0, distance) == expectedDuration);
+ => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.GetSnappedDurationFromDistance(new HitObject(), distance) == expectedDuration);
private void assertSnappedDistance(float distance, float expectedDistance)
- => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.GetSnappedDistanceFromDistance(0, distance) == expectedDistance);
+ => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.GetSnappedDistanceFromDistance(new HitObject(), distance) == expectedDistance);
private class TestHitObjectComposer : OsuHitObjectComposer
{
diff --git a/osu.Game.Tests/ImportTest.cs b/osu.Game.Tests/ImportTest.cs
index e888f51e98..dbeb453d4d 100644
--- a/osu.Game.Tests/ImportTest.cs
+++ b/osu.Game.Tests/ImportTest.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Tests
protected virtual TestOsuGameBase LoadOsuIntoHost(GameHost host, bool withBeatmap = false)
{
var osu = new TestOsuGameBase(withBeatmap);
- Task.Run(() => host.Run(osu))
+ Task.Factory.StartNew(() => host.Run(osu), TaskCreationOptions.LongRunning)
.ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted);
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
index fabb016d5f..cfda4f6422 100644
--- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
+++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
@@ -46,7 +46,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestAddRedundantDifficulty()
{
- var cpi = new ControlPointInfo();
+ var cpi = new LegacyControlPointInfo();
cpi.Add(0, new DifficultyControlPoint()); // is redundant
cpi.Add(1000, new DifficultyControlPoint()); // is redundant
@@ -55,7 +55,7 @@ namespace osu.Game.Tests.NonVisual
Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(0));
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0));
- cpi.Add(1000, new DifficultyControlPoint { SpeedMultiplier = 2 }); // is not redundant
+ cpi.Add(1000, new DifficultyControlPoint { SliderVelocity = 2 }); // is not redundant
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(1));
@@ -159,7 +159,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestAddControlPointToGroup()
{
- var cpi = new ControlPointInfo();
+ var cpi = new LegacyControlPointInfo();
var group = cpi.GroupAt(1000, true);
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
@@ -174,23 +174,23 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestAddDuplicateControlPointToGroup()
{
- var cpi = new ControlPointInfo();
+ var cpi = new LegacyControlPointInfo();
var group = cpi.GroupAt(1000, true);
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
group.Add(new DifficultyControlPoint());
- group.Add(new DifficultyControlPoint { SpeedMultiplier = 2 });
+ group.Add(new DifficultyControlPoint { SliderVelocity = 2 });
Assert.That(group.ControlPoints.Count, Is.EqualTo(1));
Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(1));
- Assert.That(cpi.DifficultyPoints.First().SpeedMultiplier, Is.EqualTo(2));
+ Assert.That(cpi.DifficultyPoints.First().SliderVelocity, Is.EqualTo(2));
}
[Test]
public void TestRemoveControlPointFromGroup()
{
- var cpi = new ControlPointInfo();
+ var cpi = new LegacyControlPointInfo();
var group = cpi.GroupAt(1000, true);
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
@@ -208,14 +208,14 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestOrdering()
{
- var cpi = new ControlPointInfo();
+ var cpi = new LegacyControlPointInfo();
cpi.Add(0, new TimingControlPoint());
cpi.Add(1000, new TimingControlPoint { BeatLength = 500 });
cpi.Add(10000, new TimingControlPoint { BeatLength = 200 });
cpi.Add(5000, new TimingControlPoint { BeatLength = 100 });
- cpi.Add(3000, new DifficultyControlPoint { SpeedMultiplier = 2 });
- cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SpeedMultiplier = 4 });
+ cpi.Add(3000, new DifficultyControlPoint { SliderVelocity = 2 });
+ cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SliderVelocity = 4 });
cpi.GroupAt(1000).Add(new SampleControlPoint { SampleVolume = 0 });
cpi.GroupAt(8000, true).Add(new EffectControlPoint { KiaiMode = true });
@@ -230,14 +230,14 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestClear()
{
- var cpi = new ControlPointInfo();
+ var cpi = new LegacyControlPointInfo();
cpi.Add(0, new TimingControlPoint());
cpi.Add(1000, new TimingControlPoint { BeatLength = 500 });
cpi.Add(10000, new TimingControlPoint { BeatLength = 200 });
cpi.Add(5000, new TimingControlPoint { BeatLength = 100 });
- cpi.Add(3000, new DifficultyControlPoint { SpeedMultiplier = 2 });
- cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SpeedMultiplier = 4 });
+ cpi.Add(3000, new DifficultyControlPoint { SliderVelocity = 2 });
+ cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SliderVelocity = 4 });
cpi.GroupAt(1000).Add(new SampleControlPoint { SampleVolume = 0 });
cpi.GroupAt(8000, true).Add(new EffectControlPoint { KiaiMode = true });
diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
index 79767bc671..558b874234 100644
--- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
+++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs
@@ -168,14 +168,14 @@ namespace osu.Game.Tests.Online
return new TestBeatmapModelManager(this, storage, contextFactory, rulesets, api, host);
}
- protected override BeatmapModelDownloader CreateBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider api, GameHost host)
+ protected override BeatmapModelDownloader CreateBeatmapModelDownloader(IBeatmapModelManager manager, IAPIProvider api, GameHost host)
{
- return new TestBeatmapModelDownloader(modelManager, api, host);
+ return new TestBeatmapModelDownloader(manager, api, host);
}
internal class TestBeatmapModelDownloader : BeatmapModelDownloader
{
- public TestBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider apiProvider, GameHost gameHost)
+ public TestBeatmapModelDownloader(IBeatmapModelManager modelManager, IAPIProvider apiProvider, GameHost gameHost)
: base(modelManager, apiProvider, gameHost)
{
}
diff --git a/osu.Game.Tests/Resources/Samples/blank.wav b/osu.Game.Tests/Resources/Samples/blank.wav
new file mode 100644
index 0000000000..878bf23cea
Binary files /dev/null and b/osu.Game.Tests/Resources/Samples/blank.wav differ
diff --git a/osu.Game.Tests/Resources/Samples/corrupt.wav b/osu.Game.Tests/Resources/Samples/corrupt.wav
new file mode 100644
index 0000000000..87c7de4b7b
Binary files /dev/null and b/osu.Game.Tests/Resources/Samples/corrupt.wav differ
diff --git a/osu.Game.Tests/Resources/Samples/test-sample-cut.mp3 b/osu.Game.Tests/Resources/Samples/test-sample-cut.mp3
new file mode 100644
index 0000000000..003fe23dca
Binary files /dev/null and b/osu.Game.Tests/Resources/Samples/test-sample-cut.mp3 differ
diff --git a/osu.Game.Tests/Resources/Videos/test-video-with-audio.mp4 b/osu.Game.Tests/Resources/Videos/test-video-with-audio.mp4
new file mode 100644
index 0000000000..5d380ab50c
Binary files /dev/null and b/osu.Game.Tests/Resources/Videos/test-video-with-audio.mp4 differ
diff --git a/osu.Game.Tests/Resources/Videos/test-video-with-track-but-no-audio.mp4 b/osu.Game.Tests/Resources/Videos/test-video-with-track-but-no-audio.mp4
new file mode 100644
index 0000000000..7cdd1939e9
Binary files /dev/null and b/osu.Game.Tests/Resources/Videos/test-video-with-track-but-no-audio.mp4 differ
diff --git a/osu.Game.Tests/Resources/Videos/test-video.mp4 b/osu.Game.Tests/Resources/Videos/test-video.mp4
new file mode 100644
index 0000000000..795483c096
Binary files /dev/null and b/osu.Game.Tests/Resources/Videos/test-video.mp4 differ
diff --git a/osu.Game.Tests/Resources/duplicate-last-position-slider.osu b/osu.Game.Tests/Resources/duplicate-last-position-slider.osu
new file mode 100644
index 0000000000..782dd4263e
--- /dev/null
+++ b/osu.Game.Tests/Resources/duplicate-last-position-slider.osu
@@ -0,0 +1,19 @@
+osu file format v14
+
+[Difficulty]
+HPDrainRate:7
+CircleSize:10
+OverallDifficulty:9
+ApproachRate:10
+SliderMultiplier:0.4
+SliderTickRate:1
+
+[TimingPoints]
+382,923.076923076923,3,2,1,75,1,0
+382,-1000,3,2,1,75,0,0
+
+[HitObjects]
+
+// Importantly, the last position is specified twice.
+// In this case, osu-stable doesn't extend the slider length even when the "expected" length is higher than the actual.
+261,171,25305,6,0,B|262:171|262:171|262:171,1,2,8|0,0:0|0:0,0:0:0:0:
diff --git a/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs b/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs
index ab47067411..ffb3d41d18 100644
--- a/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs
+++ b/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs
@@ -6,7 +6,6 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
-using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
@@ -65,10 +64,9 @@ namespace osu.Game.Tests.Skins
public new void TriggerSourceChanged() => base.TriggerSourceChanged();
- protected override void OnSourceChanged()
+ protected override void RefreshSources()
{
- ResetSources();
- sources.ForEach(AddSource);
+ SetSources(sources);
}
}
diff --git a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs
index 211543a881..99be72e958 100644
--- a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs
+++ b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs
@@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@@ -18,16 +19,19 @@ namespace osu.Game.Tests.Visual.Audio
{
public class TestSceneAudioFilter : OsuTestScene
{
- private OsuSpriteText lowpassText;
- private AudioFilter lowpassFilter;
+ private OsuSpriteText lowPassText;
+ private AudioFilter lowPassFilter;
- private OsuSpriteText highpassText;
- private AudioFilter highpassFilter;
+ private OsuSpriteText highPassText;
+ private AudioFilter highPassFilter;
private Track track;
private WaveformTestBeatmap beatmap;
+ private OsuSliderBar lowPassSlider;
+ private OsuSliderBar highPassSlider;
+
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
@@ -38,53 +42,89 @@ namespace osu.Game.Tests.Visual.Audio
{
Children = new Drawable[]
{
- lowpassFilter = new AudioFilter(audio.TrackMixer),
- highpassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
- lowpassText = new OsuSpriteText
+ lowPassFilter = new AudioFilter(audio.TrackMixer),
+ highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
+ lowPassText = new OsuSpriteText
{
Padding = new MarginPadding(20),
- Text = $"Low Pass: {lowpassFilter.Cutoff.Value}hz",
+ Text = $"Low Pass: {lowPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40)
},
- new OsuSliderBar
+ lowPassSlider = new OsuSliderBar
{
Width = 500,
Height = 50,
Padding = new MarginPadding(20),
- Current = { BindTarget = lowpassFilter.Cutoff }
+ Current = new BindableInt
+ {
+ MinValue = 0,
+ MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF,
+ }
},
- highpassText = new OsuSpriteText
+ highPassText = new OsuSpriteText
{
Padding = new MarginPadding(20),
- Text = $"High Pass: {highpassFilter.Cutoff.Value}hz",
+ Text = $"High Pass: {highPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40)
},
- new OsuSliderBar
+ highPassSlider = new OsuSliderBar
{
Width = 500,
Height = 50,
Padding = new MarginPadding(20),
- Current = { BindTarget = highpassFilter.Cutoff }
+ Current = new BindableInt
+ {
+ MinValue = 0,
+ MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF,
+ }
}
}
});
- lowpassFilter.Cutoff.ValueChanged += e => lowpassText.Text = $"Low Pass: {e.NewValue}hz";
- highpassFilter.Cutoff.ValueChanged += e => highpassText.Text = $"High Pass: {e.NewValue}hz";
+
+ lowPassSlider.Current.ValueChanged += e =>
+ {
+ lowPassText.Text = $"Low Pass: {e.NewValue}hz";
+ lowPassFilter.Cutoff = e.NewValue;
+ };
+
+ highPassSlider.Current.ValueChanged += e =>
+ {
+ highPassText.Text = $"High Pass: {e.NewValue}hz";
+ highPassFilter.Cutoff = e.NewValue;
+ };
}
+ #region Overrides of Drawable
+
+ protected override void Update()
+ {
+ base.Update();
+ highPassSlider.Current.Value = highPassFilter.Cutoff;
+ lowPassSlider.Current.Value = lowPassFilter.Cutoff;
+ }
+
+ #endregion
+
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Play Track", () => track.Start());
+
+ AddStep("Reset filters", () =>
+ {
+ lowPassFilter.Cutoff = AudioFilter.MAX_LOWPASS_CUTOFF;
+ highPassFilter.Cutoff = 0;
+ });
+
waitTrackPlay();
}
[Test]
- public void TestLowPass()
+ public void TestLowPassSweep()
{
AddStep("Filter Sweep", () =>
{
- lowpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
+ lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
.CutoffTo(0, 2000, Easing.OutCubic);
});
@@ -92,7 +132,7 @@ namespace osu.Game.Tests.Visual.Audio
AddStep("Filter Sweep (reverse)", () =>
{
- lowpassFilter.CutoffTo(0).Then()
+ lowPassFilter.CutoffTo(0).Then()
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
});
@@ -101,11 +141,11 @@ namespace osu.Game.Tests.Visual.Audio
}
[Test]
- public void TestHighPass()
+ public void TestHighPassSweep()
{
AddStep("Filter Sweep", () =>
{
- highpassFilter.CutoffTo(0).Then()
+ highPassFilter.CutoffTo(0).Then()
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
});
@@ -113,7 +153,7 @@ namespace osu.Game.Tests.Visual.Audio
AddStep("Filter Sweep (reverse)", () =>
{
- highpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
+ highPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
.CutoffTo(0, 2000, Easing.OutCubic);
});
@@ -123,5 +163,11 @@ namespace osu.Game.Tests.Visual.Audio
}
private void waitTrackPlay() => AddWaitStep("Let track play", 10);
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ track?.Dispose();
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs
new file mode 100644
index 0000000000..c48b63ac89
--- /dev/null
+++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapSetOnlineStatusPill.cs
@@ -0,0 +1,53 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.Drawables;
+using osu.Game.Overlays;
+using osu.Game.Tests.Visual.UserInterface;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Beatmaps
+{
+ public class TestSceneBeatmapSetOnlineStatusPill : ThemeComparisonTestScene
+ {
+ protected override Drawable CreateContent() => new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 10),
+ ChildrenEnumerable = Enum.GetValues(typeof(BeatmapSetOnlineStatus)).Cast().Select(status => new BeatmapSetOnlineStatusPill
+ {
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Status = status
+ })
+ };
+
+ private IEnumerable statusPills => this.ChildrenOfType();
+
+ [Test]
+ public void TestFixedWidth()
+ {
+ AddStep("create themed content", () => CreateThemedContent(OverlayColourScheme.Red));
+
+ AddStep("set fixed width", () => statusPills.ForEach(pill =>
+ {
+ pill.AutoSizeAxes = Axes.Y;
+ pill.Width = 90;
+ }));
+ AddStep("unset fixed width", () => statusPills.ForEach(pill => pill.AutoSizeAxes = Axes.Both));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs b/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs
index 9a999a4931..89e20043fb 100644
--- a/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs
+++ b/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs
@@ -224,7 +224,7 @@ namespace osu.Game.Tests.Visual.Components
public new PreviewTrack CurrentTrack => base.CurrentTrack;
- protected override TrackManagerPreviewTrack CreatePreviewTrack(BeatmapSetInfo beatmapSetInfo, ITrackStore trackStore) => new TestPreviewTrack(beatmapSetInfo, trackStore);
+ protected override TrackManagerPreviewTrack CreatePreviewTrack(IBeatmapSetInfo beatmapSetInfo, ITrackStore trackStore) => new TestPreviewTrack(beatmapSetInfo, trackStore);
public override bool UpdateSubTree()
{
@@ -240,7 +240,7 @@ namespace osu.Game.Tests.Visual.Components
public new Track Track => base.Track;
- public TestPreviewTrack(BeatmapSetInfo beatmapSetInfo, ITrackStore trackManager)
+ public TestPreviewTrack(IBeatmapSetInfo beatmapSetInfo, ITrackStore trackManager)
: base(beatmapSetInfo, trackManager)
{
this.trackManager = trackManager;
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
index 11830ebe35..d1efd22d6f 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
@@ -81,7 +82,7 @@ namespace osu.Game.Tests.Visual.Editing
public new float DistanceSpacing => base.DistanceSpacing;
public TestDistanceSnapGrid(double? endTime = null)
- : base(grid_position, 0, endTime)
+ : base(new HitObject(), grid_position, 0, endTime)
{
}
@@ -158,15 +159,15 @@ namespace osu.Game.Tests.Visual.Editing
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
- public float GetBeatSnapDistanceAt(double referenceTime) => 10;
+ public float GetBeatSnapDistanceAt(HitObject referenceObject) => 10;
- public float DurationToDistance(double referenceTime, double duration) => (float)duration;
+ public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration;
- public double DistanceToDuration(double referenceTime, float distance) => distance;
+ public double DistanceToDuration(HitObject referenceObject, float distance) => distance;
- public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0;
+ public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => 0;
- public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => 0;
+ public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => 0;
}
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
index 2258a209e2..af3d9beb69 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs
@@ -8,6 +8,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit;
+using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osuTK.Input;
@@ -30,22 +31,41 @@ namespace osu.Game.Tests.Visual.Editing
PushAndConfirm(() => new EditorLoader());
- AddUntilStep("wait for editor load", () => editor != null);
+ AddUntilStep("wait for editor load", () => editor?.IsLoaded == true);
- AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
+ AddUntilStep("wait for metadata screen load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
+
+ // We intentionally switch away from the metadata screen, else there is a feedback loop with the textbox handling which causes metadata changes below to get overwritten.
AddStep("Enter compose mode", () => InputManager.Key(Key.F1));
AddUntilStep("Wait for compose mode load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
+ AddStep("Set overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty = 7);
+ AddStep("Set artist and title", () =>
+ {
+ editorBeatmap.BeatmapInfo.Metadata.Artist = "artist";
+ editorBeatmap.BeatmapInfo.Metadata.Title = "title";
+ });
+ AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.Version = "difficulty");
+
+ AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
+
AddStep("Change to placement mode", () => InputManager.Key(Key.Number2));
AddStep("Move to playfield", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre));
AddStep("Place single hitcircle", () => InputManager.Click(MouseButton.Left));
- AddStep("Save and exit", () =>
- {
- InputManager.Keys(PlatformAction.Save);
- InputManager.Key(Key.Escape);
- });
+ checkMutations();
+
+ // After placement these must be non-default as defaults are read-only.
+ AddAssert("Placed object has non-default control points", () =>
+ editorBeatmap.HitObjects[0].SampleControlPoint != SampleControlPoint.DEFAULT &&
+ editorBeatmap.HitObjects[0].DifficultyControlPoint != DifficultyControlPoint.DEFAULT);
+
+ AddStep("Save", () => InputManager.Keys(PlatformAction.Save));
+
+ checkMutations();
+
+ AddStep("Exit", () => InputManager.Key(Key.Escape));
AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
@@ -56,7 +76,16 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Enter editor", () => InputManager.Key(Key.Number5));
AddUntilStep("Wait for editor load", () => editor != null);
+
+ checkMutations();
+ }
+
+ private void checkMutations()
+ {
AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1);
+ AddAssert("Beatmap has correct overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty == 7);
+ AddAssert("Beatmap has correct metadata", () => editorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && editorBeatmap.BeatmapInfo.Metadata.Title == "title");
+ AddAssert("Beatmap has correct difficulty name", () => editorBeatmap.BeatmapInfo.Version == "difficulty");
}
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs
index 00b5c38e20..c5ab3974a4 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAllRulesetPlayers.cs
@@ -20,14 +20,15 @@ namespace osu.Game.Tests.Visual.Gameplay
///
public abstract class TestSceneAllRulesetPlayers : RateAdjustedBeatmapTestScene
{
- protected Player Player;
+ protected Player Player { get; private set; }
+
+ protected OsuConfigManager Config { get; private set; }
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
- OsuConfigManager manager;
- Dependencies.Cache(manager = new OsuConfigManager(LocalStorage));
- manager.GetBindable(OsuSetting.DimLevel).Value = 1.0;
+ Dependencies.Cache(Config = new OsuConfigManager(LocalStorage));
+ Config.GetBindable(OsuSetting.DimLevel).Value = 1.0;
}
[Test]
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
index 85aaf20a19..36fc6812bd 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs
@@ -2,7 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using NUnit.Framework;
using osu.Framework.Graphics.Containers;
+using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Play;
@@ -17,6 +19,14 @@ namespace osu.Game.Tests.Visual.Gameplay
return new FailPlayer();
}
+ [Test]
+ public void TestOsuWithoutRedTint()
+ {
+ AddStep("Disable red tint", () => Config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, false));
+ TestOsu();
+ AddStep("Enable red tint", () => Config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, true));
+ }
+
protected override void AddCheckSteps()
{
AddUntilStep("wait for fail", () => Player.HasFailed);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs
index 5eb71e92c2..ae0decaee1 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs
@@ -103,6 +103,30 @@ namespace osu.Game.Tests.Visual.Gameplay
checkFrameCount(0);
}
+ [Test]
+ public void TestRatePreservedWhenTimeNotProgressing()
+ {
+ AddStep("set manual clock rate", () => manualClock.Rate = 1);
+ seekManualTo(5000);
+ createStabilityContainer();
+ checkRate(1);
+
+ seekManualTo(10000);
+ checkRate(1);
+
+ AddWaitStep("wait some", 3);
+ checkRate(1);
+
+ seekManualTo(5000);
+ checkRate(-1);
+
+ AddWaitStep("wait some", 3);
+ checkRate(-1);
+
+ seekManualTo(10000);
+ checkRate(1);
+ }
+
private const int max_frames_catchup = 50;
private void createStabilityContainer(double gameplayStartTime = double.MinValue) => AddStep("create container", () =>
@@ -116,6 +140,9 @@ namespace osu.Game.Tests.Visual.Gameplay
private void checkFrameCount(int frames) =>
AddAssert($"elapsed frames is {frames}", () => consumer.ElapsedFrames == frames);
+ private void checkRate(double rate) =>
+ AddAssert($"clock rate is {rate}", () => consumer.Clock.Rate == rate);
+
public class ClockConsumingChild : CompositeDrawable
{
private readonly OsuSpriteText text;
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
index aee15a145c..ba0ee5ac6e 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
@@ -291,7 +291,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
- AddAssert($"epilepsy warning {(warning ? "present" : "absent")}", () => this.ChildrenOfType().Any() == warning);
+ AddAssert($"epilepsy warning {(warning ? "present" : "absent")}", () => (getWarning() != null) == warning);
if (warning)
{
@@ -335,12 +335,17 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
- AddUntilStep("wait for epilepsy warning", () => loader.ChildrenOfType().Single().Alpha > 0);
+ AddUntilStep("wait for epilepsy warning", () => getWarning().Alpha > 0);
+ AddUntilStep("warning is shown", () => getWarning().State.Value == Visibility.Visible);
+
AddStep("exit early", () => loader.Exit());
+ AddUntilStep("warning is hidden", () => getWarning().State.Value == Visibility.Hidden);
AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1);
}
+ private EpilepsyWarning getWarning() => loader.ChildrenOfType().SingleOrDefault();
+
private class TestPlayerLoader : PlayerLoader
{
public new VisualSettings VisualSettings => base.VisualSettings;
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
index 5ff2e9c439..bf864f844c 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerScoreSubmission.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
+using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
@@ -10,10 +11,13 @@ using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Online.Solo;
using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Taiko;
+using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
using osu.Game.Tests.Beatmaps;
@@ -32,7 +36,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override bool HasCustomSteps => true;
- protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false);
+ protected override TestPlayer CreatePlayer(Ruleset ruleset) => new NonImportingPlayer(false);
protected override Ruleset CreatePlayerRuleset() => createCustomRuleset?.Invoke() ?? new OsuRuleset();
@@ -86,6 +90,46 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("ensure passing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == true);
}
+ [Test]
+ public void TestSubmissionForDifferentRuleset()
+ {
+ prepareTokenResponse(true);
+
+ createPlayerTest(createRuleset: () => new TaikoRuleset());
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
+
+ addFakeHit();
+
+ AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
+
+ AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
+ AddAssert("ensure passing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == true);
+ AddAssert("submitted score has correct ruleset ID", () => Player.SubmittedScore?.ScoreInfo.RulesetID == new TaikoRuleset().RulesetInfo.ID);
+ }
+
+ [Test]
+ public void TestSubmissionForConvertedBeatmap()
+ {
+ prepareTokenResponse(true);
+
+ createPlayerTest(createRuleset: () => new ManiaRuleset(), createBeatmap: _ => createTestBeatmap(new OsuRuleset().RulesetInfo));
+
+ AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
+
+ AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
+
+ addFakeHit();
+
+ AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
+
+ AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
+ AddAssert("ensure passing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == true);
+ AddAssert("submitted score has correct ruleset ID", () => Player.SubmittedScore?.ScoreInfo.RulesetID == new ManiaRuleset().RulesetInfo.ID);
+ }
+
[Test]
public void TestNoSubmissionOnExitWithNoToken()
{
@@ -183,12 +227,13 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
}
- [Test]
- public void TestNoSubmissionOnCustomRuleset()
+ [TestCase(null)]
+ [TestCase(10)]
+ public void TestNoSubmissionOnCustomRuleset(int? rulesetId)
{
prepareTokenResponse(true);
- createPlayerTest(false, createRuleset: () => new OsuRuleset { RulesetInfo = { ID = 10 } });
+ createPlayerTest(false, createRuleset: () => new OsuRuleset { RulesetInfo = { ID = rulesetId } });
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
@@ -242,5 +287,33 @@ namespace osu.Game.Tests.Visual.Gameplay
});
});
}
+
+ private class NonImportingPlayer : TestPlayer
+ {
+ public NonImportingPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false)
+ : base(allowPause, showResults, pauseOnFocusLost)
+ {
+ }
+
+ protected override Task ImportScore(Score score)
+ {
+ // It was discovered that Score members could sometimes be half-populated.
+ // In particular, the RulesetID property could be set to 0 even on non-osu! maps.
+ // We want to test that the state of that property is consistent in this test.
+ // EF makes this impossible.
+ //
+ // First off, because of the EF navigational property-explicit foreign key field duality,
+ // it can happen that - for example - the Ruleset navigational property is correctly initialised to mania,
+ // but the RulesetID foreign key property is not initialised and remains 0.
+ // EF silently bypasses this by prioritising the Ruleset navigational property over the RulesetID foreign key one.
+ //
+ // Additionally, adding an entity to an EF DbSet CAUSES SIDE EFFECTS with regard to the foreign key property.
+ // In the above instance, if a ScoreInfo with Ruleset = {mania} and RulesetID = 0 is attached to an EF context,
+ // RulesetID WILL BE SILENTLY SET TO THE CORRECT VALUE of 3.
+ //
+ // For the above reasons, importing is disabled in this test.
+ return Task.CompletedTask;
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
index 2f15e549f7..283fe594ea 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
@@ -93,9 +93,9 @@ namespace osu.Game.Tests.Visual.Gameplay
private IList testControlPoints => new List
{
- new MultiplierControlPoint(time_range) { DifficultyPoint = { SpeedMultiplier = 1.25 } },
- new MultiplierControlPoint(1.5 * time_range) { DifficultyPoint = { SpeedMultiplier = 1 } },
- new MultiplierControlPoint(2 * time_range) { DifficultyPoint = { SpeedMultiplier = 1.5 } }
+ new MultiplierControlPoint(time_range) { EffectPoint = { ScrollSpeed = 1.25 } },
+ new MultiplierControlPoint(1.5 * time_range) { EffectPoint = { ScrollSpeed = 1 } },
+ new MultiplierControlPoint(2 * time_range) { EffectPoint = { ScrollSpeed = 1.5 } }
};
[Test]
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs
new file mode 100644
index 0000000000..89fea1f92d
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorHost.cs
@@ -0,0 +1,51 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Game.Online.API;
+using osu.Game.Online.Spectator;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mania;
+using osu.Game.Tests.Visual.Spectator;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneSpectatorHost : PlayerTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
+
+ [Cached(typeof(SpectatorClient))]
+ private TestSpectatorClient spectatorClient { get; } = new TestSpectatorClient();
+
+ private DummyAPIAccess dummyAPIAccess => (DummyAPIAccess)API;
+ private const int dummy_user_id = 42;
+
+ public override void SetUpSteps()
+ {
+ AddStep("set dummy user", () => dummyAPIAccess.LocalUser.Value = new User
+ {
+ Id = dummy_user_id,
+ Username = "DummyUser"
+ });
+ AddStep("add test spectator client", () => Add(spectatorClient));
+ AddStep("add watching user", () => spectatorClient.WatchUser(dummy_user_id));
+ base.SetUpSteps();
+ }
+
+ [Test]
+ public void TestClientSendsCorrectRuleset()
+ {
+ AddUntilStep("spectator client sending frames", () => spectatorClient.PlayingUserStates.ContainsKey(dummy_user_id));
+ AddAssert("spectator client sent correct ruleset", () => spectatorClient.PlayingUserStates[dummy_user_id].RulesetID == Ruleset.Value.ID);
+ }
+
+ public override void TearDownSteps()
+ {
+ base.TearDownSteps();
+ AddStep("stop watching user", () => spectatorClient.StopWatchingUser(dummy_user_id));
+ AddStep("remove test spectator client", () => Remove(spectatorClient));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
index 3ed274690e..48a97d54f7 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs
@@ -90,8 +90,12 @@ namespace osu.Game.Tests.Visual.Gameplay
CreateTest(() =>
{
AddStep("fail on first judgement", () => currentFailConditions = (_, __) => true);
- AddStep("set storyboard duration to 1.3s", () => currentStoryboardDuration = 1300);
+
+ // Fail occurs at 164ms with the provided beatmap.
+ // Fail animation runs for 2.5s realtime but the gameplay time change is *variable* due to the frequency transform being applied, so we need a bit of lenience.
+ AddStep("set storyboard duration to 0.6s", () => currentStoryboardDuration = 600);
});
+
AddUntilStep("wait for fail", () => Player.HasFailed);
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs
new file mode 100644
index 0000000000..ea895a23d2
--- /dev/null
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableLoungeRoom.cs
@@ -0,0 +1,167 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Moq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Testing;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.Rooms;
+using osu.Game.Overlays;
+using osu.Game.Screens.OnlinePlay.Lounge;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Multiplayer
+{
+ public class TestSceneDrawableLoungeRoom : OsuManualInputManagerTestScene
+ {
+ private readonly Room room = new Room
+ {
+ HasPassword = { Value = true }
+ };
+
+ [Cached]
+ protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
+
+ private DrawableLoungeRoom drawableRoom;
+ private SearchTextBox searchTextBox;
+
+ private readonly ManualResetEventSlim allowResponseCallback = new ManualResetEventSlim();
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ var mockLounge = new Mock();
+ mockLounge
+ .Setup(l => l.Join(It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny>()))
+ .Callback, Action>((a, b, c, d) =>
+ {
+ Task.Run(() =>
+ {
+ allowResponseCallback.Wait();
+ allowResponseCallback.Reset();
+ Schedule(() => d?.Invoke("Incorrect password"));
+ });
+ });
+
+ Dependencies.CacheAs(mockLounge.Object);
+ }
+
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("create drawable", () =>
+ {
+ Child = new PopoverContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ searchTextBox = new SearchTextBox
+ {
+ HoldFocus = true,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Margin = new MarginPadding(50),
+ Width = 500,
+ Depth = float.MaxValue
+ },
+ drawableRoom = new DrawableLoungeRoom(room)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }
+ }
+ };
+ });
+ }
+
+ [Test]
+ public void TestFocusViaKeyboardCommit()
+ {
+ DrawableLoungeRoom.PasswordEntryPopover popover = null;
+
+ AddAssert("search textbox has focus", () => checkFocus(searchTextBox));
+ AddStep("click room twice", () =>
+ {
+ InputManager.MoveMouseTo(drawableRoom);
+ InputManager.Click(MouseButton.Left);
+ InputManager.Click(MouseButton.Left);
+ });
+ AddUntilStep("wait for popover", () => (popover = InputManager.ChildrenOfType().SingleOrDefault()) != null);
+
+ AddAssert("textbox has focus", () => checkFocus(popover.ChildrenOfType().Single()));
+
+ AddStep("enter password", () => popover.ChildrenOfType().Single().Text = "password");
+ AddStep("commit via enter", () => InputManager.Key(Key.Enter));
+
+ AddAssert("popover has focus", () => checkFocus(popover));
+
+ AddStep("attempt another enter", () => InputManager.Key(Key.Enter));
+
+ AddAssert("popover still has focus", () => checkFocus(popover));
+
+ AddStep("unblock response", () => allowResponseCallback.Set());
+
+ AddUntilStep("wait for textbox refocus", () => checkFocus(popover.ChildrenOfType().Single()));
+
+ AddStep("press escape", () => InputManager.Key(Key.Escape));
+ AddStep("press escape", () => InputManager.Key(Key.Escape));
+
+ AddUntilStep("search textbox has focus", () => checkFocus(searchTextBox));
+ }
+
+ [Test]
+ public void TestFocusViaMouseCommit()
+ {
+ DrawableLoungeRoom.PasswordEntryPopover popover = null;
+
+ AddAssert("search textbox has focus", () => checkFocus(searchTextBox));
+ AddStep("click room twice", () =>
+ {
+ InputManager.MoveMouseTo(drawableRoom);
+ InputManager.Click(MouseButton.Left);
+ InputManager.Click(MouseButton.Left);
+ });
+ AddUntilStep("wait for popover", () => (popover = InputManager.ChildrenOfType().SingleOrDefault()) != null);
+
+ AddAssert("textbox has focus", () => checkFocus(popover.ChildrenOfType().Single()));
+
+ AddStep("enter password", () => popover.ChildrenOfType().Single().Text = "password");
+
+ AddStep("commit via click button", () =>
+ {
+ var button = popover.ChildrenOfType().Single();
+ InputManager.MoveMouseTo(button);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddAssert("popover has focus", () => checkFocus(popover));
+
+ AddStep("attempt another click", () => InputManager.Click(MouseButton.Left));
+
+ AddAssert("popover still has focus", () => checkFocus(popover));
+
+ AddStep("unblock response", () => allowResponseCallback.Set());
+
+ AddUntilStep("wait for textbox refocus", () => checkFocus(popover.ChildrenOfType().Single()));
+
+ AddStep("click away", () =>
+ {
+ InputManager.MoveMouseTo(searchTextBox);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddUntilStep("search textbox has focus", () => checkFocus(searchTextBox));
+ }
+
+ private bool checkFocus(Drawable expected) =>
+ InputManager.FocusedDrawable == expected;
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneGameplayChatDisplay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneGameplayChatDisplay.cs
index a3a1cacb0d..512d206a06 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneGameplayChatDisplay.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneGameplayChatDisplay.cs
@@ -92,6 +92,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
assertChatFocused(true);
}
+ [Test]
+ public void TestFocusLostOnBackKey()
+ {
+ setLocalUserPlaying(true);
+
+ assertChatFocused(false);
+ AddStep("press tab", () => InputManager.Key(Key.Tab));
+ assertChatFocused(true);
+ AddStep("press escape", () => InputManager.Key(Key.Escape));
+ assertChatFocused(false);
+ }
+
[Test]
public void TestFocusOnTabKeyWhenNotExpanded()
{
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
index 99b530c2a2..1bdf3c2750 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs
@@ -5,6 +5,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
using osu.Framework.Testing;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Catch;
@@ -25,12 +26,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUp]
public new void Setup() => Schedule(() =>
{
- Child = container = new RoomsContainer
+ Child = new PopoverContainer
{
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
- SelectedRoom = { BindTarget = SelectedRoom }
+
+ Child = container = new RoomsContainer
+ {
+ SelectedRoom = { BindTarget = SelectedRoom }
+ }
};
});
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
index 0b70703870..2bb77395ef 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs
@@ -564,11 +564,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
- AddRepeatStep("click spectate button", () =>
+ AddUntilStep("wait for ready button to be enabled", () => readyButton.ChildrenOfType().Single().Enabled.Value);
+
+ AddStep("click ready button", () =>
{
- InputManager.MoveMouseTo(this.ChildrenOfType().Single());
+ InputManager.MoveMouseTo(readyButton);
InputManager.Click(MouseButton.Left);
- }, 2);
+ });
+
+ AddUntilStep("wait for player to be ready", () => client.Room?.Users[0].State == MultiplayerUserState.Ready);
+ AddUntilStep("wait for ready button to be enabled", () => readyButton.ChildrenOfType().Single().Enabled.Value);
+
+ AddStep("click start button", () => InputManager.Click(MouseButton.Left));
AddUntilStep("wait for player", () => Stack.CurrentScreen is Player);
@@ -582,6 +589,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for results", () => Stack.CurrentScreen is ResultsScreen);
}
+ private MultiplayerReadyButton readyButton => this.ChildrenOfType().Single();
+
private void createRoom(Func room)
{
AddUntilStep("wait for lounge", () => multiplayerScreen.ChildrenOfType().SingleOrDefault()?.IsLoaded == true);
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
index c4ebc13245..d1980b03c7 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs
@@ -275,6 +275,68 @@ namespace osu.Game.Tests.Visual.Multiplayer
var state = i;
AddStep($"set state: {state}", () => Client.ChangeUserState(0, state));
}
+
+ AddStep("set state: downloading", () => Client.ChangeUserBeatmapAvailability(0, BeatmapAvailability.Downloading(0)));
+
+ AddStep("set state: locally available", () => Client.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable()));
+ }
+
+ [Test]
+ public void TestModOverlap()
+ {
+ AddStep("add dummy mods", () =>
+ {
+ Client.ChangeUserMods(new Mod[]
+ {
+ new OsuModNoFail(),
+ new OsuModDoubleTime()
+ });
+ });
+
+ AddStep("add user with mods", () =>
+ {
+ Client.AddUser(new User
+ {
+ Id = 0,
+ Username = "Baka",
+ RulesetsStatistics = new Dictionary
+ {
+ {
+ Ruleset.Value.ShortName,
+ new UserStatistics { GlobalRank = RNG.Next(1, 100000), }
+ }
+ },
+ CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
+ });
+ Client.ChangeUserMods(0, new Mod[]
+ {
+ new OsuModHardRock(),
+ new OsuModDoubleTime()
+ });
+ });
+
+ AddStep("set 0 ready", () => Client.ChangeState(MultiplayerUserState.Ready));
+
+ AddStep("set 1 spectate", () => Client.ChangeUserState(0, MultiplayerUserState.Spectating));
+
+ // Have to set back to idle due to status priority.
+ AddStep("set 0 no map, 1 ready", () =>
+ {
+ Client.ChangeState(MultiplayerUserState.Idle);
+ Client.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded());
+ Client.ChangeUserState(0, MultiplayerUserState.Ready);
+ });
+
+ AddStep("set 0 downloading", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
+
+ AddStep("set 0 spectate", () => Client.ChangeUserState(0, MultiplayerUserState.Spectating));
+
+ AddStep("make both default", () =>
+ {
+ Client.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable());
+ Client.ChangeUserState(0, MultiplayerUserState.Idle);
+ Client.ChangeState(MultiplayerUserState.Idle);
+ });
}
private void createNewParticipantsList()
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs
new file mode 100644
index 0000000000..bd723eeed6
--- /dev/null
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs
@@ -0,0 +1,31 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Overlays.Notifications;
+using osu.Game.Tests.Resources;
+
+namespace osu.Game.Tests.Visual.Navigation
+{
+ public class TestSceneStartupImport : OsuGameTestScene
+ {
+ private string importFilename;
+
+ protected override TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API, new[] { importFilename });
+
+ public override void SetUpSteps()
+ {
+ AddStep("Prepare import beatmap", () => importFilename = TestResources.GetTestBeatmapForImport());
+
+ base.SetUpSteps();
+ }
+
+ [Test]
+ public void TestImportCreatedNotification()
+ {
+ AddUntilStep("Import notification was presented", () => Game.Notifications.ChildrenOfType().Count() == 1);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapAvailability.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapAvailability.cs
index fe94165777..6f9744ca73 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapAvailability.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapAvailability.cs
@@ -3,6 +3,7 @@
using NUnit.Framework;
using osu.Game.Beatmaps;
+using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapSet;
namespace osu.Game.Tests.Visual.Online
@@ -22,7 +23,7 @@ namespace osu.Game.Tests.Visual.Online
{
AddStep("set undownloadable beatmapset with link", () => container.BeatmapSet = new BeatmapSetInfo
{
- OnlineInfo = new BeatmapSetOnlineInfo
+ OnlineInfo = new APIBeatmapSet
{
Availability = new BeatmapSetOnlineAvailability
{
@@ -40,7 +41,7 @@ namespace osu.Game.Tests.Visual.Online
{
AddStep("set undownloadable beatmapset without link", () => container.BeatmapSet = new BeatmapSetInfo
{
- OnlineInfo = new BeatmapSetOnlineInfo
+ OnlineInfo = new APIBeatmapSet
{
Availability = new BeatmapSetOnlineAvailability
{
@@ -57,7 +58,7 @@ namespace osu.Game.Tests.Visual.Online
{
AddStep("set parts-removed beatmapset with link", () => container.BeatmapSet = new BeatmapSetInfo
{
- OnlineInfo = new BeatmapSetOnlineInfo
+ OnlineInfo = new APIBeatmapSet
{
Availability = new BeatmapSetOnlineAvailability
{
@@ -75,7 +76,7 @@ namespace osu.Game.Tests.Visual.Online
{
AddStep("set normal beatmapset", () => container.BeatmapSet = new BeatmapSetInfo
{
- OnlineInfo = new BeatmapSetOnlineInfo
+ OnlineInfo = new APIBeatmapSet
{
Availability = new BeatmapSetOnlineAvailability
{
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
index 963809ebe1..7042f1e4fe 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
@@ -7,14 +7,12 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
-using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
-using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Users;
using osuTK.Input;
@@ -92,7 +90,7 @@ namespace osu.Game.Tests.Visual.Online
{
AddAssert("is visible", () => overlay.State.Value == Visibility.Visible);
- AddStep("show many results", () => fetchFor(Enumerable.Repeat(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet, 100).ToArray()));
+ AddStep("show many results", () => fetchFor(Enumerable.Repeat(CreateAPIBeatmapSet(Ruleset.Value), 10).ToArray()));
AddUntilStep("placeholder hidden", () => !overlay.ChildrenOfType().Any(d => d.IsPresent));
@@ -114,7 +112,7 @@ namespace osu.Game.Tests.Visual.Online
AddStep("fetch for 0 beatmaps", () => fetchFor());
AddUntilStep("placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true);
- AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet));
+ AddStep("fetch for 1 beatmap", () => fetchFor(CreateAPIBeatmapSet(Ruleset.Value)));
AddUntilStep("placeholder hidden", () => !overlay.ChildrenOfType().Any(d => d.IsPresent));
AddStep("fetch for 0 beatmaps", () => fetchFor());
@@ -188,7 +186,7 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestUserWithoutSupporterUsesSupporterOnlyFiltersWithResults()
{
- AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet));
+ AddStep("fetch for 1 beatmap", () => fetchFor(CreateAPIBeatmapSet(Ruleset.Value)));
AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false);
// only Rank Achieved filter
@@ -218,7 +216,7 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestUserWithSupporterUsesSupporterOnlyFiltersWithResults()
{
- AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet));
+ AddStep("fetch for 1 beatmap", () => fetchFor(CreateAPIBeatmapSet(Ruleset.Value)));
AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true);
// only Rank Achieved filter
@@ -247,10 +245,10 @@ namespace osu.Game.Tests.Visual.Online
private static int searchCount;
- private void fetchFor(params BeatmapSetInfo[] beatmaps)
+ private void fetchFor(params APIBeatmapSet[] beatmaps)
{
setsForResponse.Clear();
- setsForResponse.AddRange(beatmaps.Select(b => new TestAPIBeatmapSet(b)));
+ setsForResponse.AddRange(beatmaps);
// trigger arbitrary change for fetching.
searchControl.Query.Value = $"search {searchCount++}";
@@ -286,17 +284,5 @@ namespace osu.Game.Tests.Visual.Online
!overlay.ChildrenOfType().Any(d => d.IsPresent)
&& !overlay.ChildrenOfType().Any(d => d.IsPresent));
}
-
- private class TestAPIBeatmapSet : APIBeatmapSet
- {
- private readonly BeatmapSetInfo beatmapSet;
-
- public TestAPIBeatmapSet(BeatmapSetInfo beatmapSet)
- {
- this.beatmapSet = beatmapSet;
- }
-
- public override BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets) => beatmapSet;
- }
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
index 453e26ef96..7f9b56e873 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs
@@ -11,6 +11,7 @@ using osu.Game.Users;
using System;
using System.Collections.Generic;
using System.Linq;
+using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Tests.Visual.Online
{
@@ -63,7 +64,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 3,
},
},
- OnlineInfo = new BeatmapSetOnlineInfo
+ OnlineInfo = new APIBeatmapSet
{
Preview = @"https://b.ppy.sh/preview/12345.mp3",
PlayCount = 123,
@@ -72,10 +73,10 @@ namespace osu.Game.Tests.Visual.Online
Ranked = DateTime.Now,
BPM = 111,
HasVideo = true,
+ Ratings = Enumerable.Range(0, 11).ToArray(),
HasStoryboard = true,
Covers = new BeatmapSetOnlineCovers(),
},
- Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() },
Beatmaps = new List
{
new BeatmapInfo
@@ -91,17 +92,17 @@ namespace osu.Game.Tests.Visual.Online
OverallDifficulty = 4.5f,
ApproachRate = 6,
},
- OnlineInfo = new BeatmapOnlineInfo
+ OnlineInfo = new APIBeatmap
{
CircleCount = 111,
SliderCount = 12,
PlayCount = 222,
PassCount = 21,
- },
- Metrics = new BeatmapMetrics
- {
- Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
- Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
+ FailTimes = new APIFailTimes
+ {
+ Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
+ Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
+ },
},
},
},
@@ -134,7 +135,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 3,
},
},
- OnlineInfo = new BeatmapSetOnlineInfo
+ OnlineInfo = new APIBeatmapSet
{
Availability = new BeatmapSetOnlineAvailability
{
@@ -152,8 +153,8 @@ namespace osu.Game.Tests.Visual.Online
Covers = new BeatmapSetOnlineCovers(),
Language = new BeatmapSetOnlineLanguage { Id = 3, Name = "English" },
Genre = new BeatmapSetOnlineGenre { Id = 4, Name = "Rock" },
+ Ratings = Enumerable.Range(0, 11).ToArray(),
},
- Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() },
Beatmaps = new List
{
new BeatmapInfo
@@ -169,17 +170,17 @@ namespace osu.Game.Tests.Visual.Online
OverallDifficulty = 7,
ApproachRate = 6,
},
- OnlineInfo = new BeatmapOnlineInfo
+ OnlineInfo = new APIBeatmap
{
CircleCount = 123,
SliderCount = 45,
PlayCount = 567,
PassCount = 89,
- },
- Metrics = new BeatmapMetrics
- {
- Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
- Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
+ FailTimes = new APIFailTimes
+ {
+ Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
+ Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
+ },
},
},
},
@@ -203,12 +204,14 @@ namespace osu.Game.Tests.Visual.Online
Version = ruleset.Name,
Ruleset = ruleset,
BaseDifficulty = new BeatmapDifficulty(),
- OnlineInfo = new BeatmapOnlineInfo(),
- Metrics = new BeatmapMetrics
+ OnlineInfo = new APIBeatmap
{
- Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
- Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
- },
+ FailTimes = new APIFailTimes
+ {
+ Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
+ Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
+ },
+ }
});
}
@@ -224,11 +227,11 @@ namespace osu.Game.Tests.Visual.Online
Id = 3,
}
},
- OnlineInfo = new BeatmapSetOnlineInfo
+ OnlineInfo = new APIBeatmapSet
{
Covers = new BeatmapSetOnlineCovers(),
+ Ratings = Enumerable.Range(0, 11).ToArray(),
},
- Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() },
Beatmaps = beatmaps
});
});
@@ -287,12 +290,14 @@ namespace osu.Game.Tests.Visual.Online
{
OverallDifficulty = 3.5f,
},
- OnlineInfo = new BeatmapOnlineInfo(),
- Metrics = new BeatmapMetrics
+ OnlineInfo = new APIBeatmap
{
- Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(),
- Retries = Enumerable.Range(-2, 100).Select(j => j % 12 - 6).ToArray(),
- },
+ FailTimes = new APIFailTimes
+ {
+ Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(),
+ Retries = Enumerable.Range(-2, 100).Select(j => j % 12 - 6).ToArray(),
+ },
+ }
});
}
@@ -309,14 +314,14 @@ namespace osu.Game.Tests.Visual.Online
Id = 3,
},
},
- OnlineInfo = new BeatmapSetOnlineInfo
+ OnlineInfo = new APIBeatmapSet
{
Preview = @"https://b.ppy.sh/preview/123.mp3",
HasVideo = true,
HasStoryboard = true,
Covers = new BeatmapSetOnlineCovers(),
+ Ratings = Enumerable.Range(0, 11).ToArray(),
},
- Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() },
Beatmaps = beatmaps,
};
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlayDetails.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlayDetails.cs
index f7099b0615..d14f9f47d1 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlayDetails.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlayDetails.cs
@@ -8,6 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapSet;
using osu.Game.Screens.Select.Details;
@@ -38,27 +39,30 @@ namespace osu.Game.Tests.Visual.Online
var secondSet = createSet();
AddStep("set first set", () => details.BeatmapSet = firstSet);
- AddAssert("ratings set", () => details.Ratings.Metrics == firstSet.Metrics);
+ AddAssert("ratings set", () => details.Ratings.Ratings == firstSet.Ratings);
AddStep("set second set", () => details.BeatmapSet = secondSet);
- AddAssert("ratings set", () => details.Ratings.Metrics == secondSet.Metrics);
+ AddAssert("ratings set", () => details.Ratings.Ratings == secondSet.Ratings);
static BeatmapSetInfo createSet() => new BeatmapSetInfo
{
- Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).Select(_ => RNG.Next(10)).ToArray() },
Beatmaps = new List
{
new BeatmapInfo
{
- Metrics = new BeatmapMetrics
+ OnlineInfo = new APIBeatmap
{
- Fails = Enumerable.Range(1, 100).Select(_ => RNG.Next(10)).ToArray(),
- Retries = Enumerable.Range(-2, 100).Select(_ => RNG.Next(10)).ToArray(),
- },
+ FailTimes = new APIFailTimes
+ {
+ Fails = Enumerable.Range(1, 100).Select(_ => RNG.Next(10)).ToArray(),
+ Retries = Enumerable.Range(-2, 100).Select(_ => RNG.Next(10)).ToArray(),
+ },
+ }
}
},
- OnlineInfo = new BeatmapSetOnlineInfo
+ OnlineInfo = new APIBeatmapSet
{
+ Ratings = Enumerable.Range(0, 11).Select(_ => RNG.Next(10)).ToArray(),
Status = BeatmapSetOnlineStatus.Ranked
}
};
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs
index fe8e33f783..b3b67fcbca 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs
@@ -11,6 +11,7 @@ using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapSet;
using osu.Game.Screens.Select.Details;
@@ -59,17 +60,20 @@ namespace osu.Game.Tests.Visual.Online
var secondBeatmap = createBeatmap();
AddStep("set first set", () => successRate.BeatmapInfo = firstBeatmap);
- AddAssert("ratings set", () => successRate.Graph.Metrics == firstBeatmap.Metrics);
+ AddAssert("ratings set", () => successRate.Graph.FailTimes == firstBeatmap.FailTimes);
AddStep("set second set", () => successRate.BeatmapInfo = secondBeatmap);
- AddAssert("ratings set", () => successRate.Graph.Metrics == secondBeatmap.Metrics);
+ AddAssert("ratings set", () => successRate.Graph.FailTimes == secondBeatmap.FailTimes);
static BeatmapInfo createBeatmap() => new BeatmapInfo
{
- Metrics = new BeatmapMetrics
+ OnlineInfo = new APIBeatmap
{
- Fails = Enumerable.Range(1, 100).Select(_ => RNG.Next(10)).ToArray(),
- Retries = Enumerable.Range(-2, 100).Select(_ => RNG.Next(10)).ToArray(),
+ FailTimes = new APIFailTimes
+ {
+ Fails = Enumerable.Range(1, 100).Select(_ => RNG.Next(10)).ToArray(),
+ Retries = Enumerable.Range(-2, 100).Select(_ => RNG.Next(10)).ToArray(),
+ }
}
};
}
@@ -79,13 +83,16 @@ namespace osu.Game.Tests.Visual.Online
{
AddStep("set beatmap", () => successRate.BeatmapInfo = new BeatmapInfo
{
- Metrics = new BeatmapMetrics
+ OnlineInfo = new APIBeatmap
{
- Fails = Enumerable.Range(1, 100).ToArray(),
+ FailTimes = new APIFailTimes
+ {
+ Fails = Enumerable.Range(1, 100).ToArray(),
+ }
}
});
- AddAssert("graph max values correct",
- () => successRate.ChildrenOfType().All(graph => graph.MaxValue == 100));
+
+ AddAssert("graph max values correct", () => successRate.ChildrenOfType().All(graph => graph.MaxValue == 100));
}
[Test]
@@ -93,11 +100,13 @@ namespace osu.Game.Tests.Visual.Online
{
AddStep("set beatmap", () => successRate.BeatmapInfo = new BeatmapInfo
{
- Metrics = new BeatmapMetrics()
+ OnlineInfo = new APIBeatmap
+ {
+ FailTimes = new APIFailTimes(),
+ }
});
- AddAssert("graph max values correct",
- () => successRate.ChildrenOfType().All(graph => graph.MaxValue == 0));
+ AddAssert("graph max values correct", () => successRate.ChildrenOfType().All(graph => graph.MaxValue == 0));
}
private class GraphExposingSuccessRate : SuccessRate
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
index 9562b41363..ab4e1b4457 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Net;
using JetBrains.Annotations;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -105,7 +106,7 @@ namespace osu.Game.Tests.Visual.Online
}
else
{
- getUser.TriggerFailure(new Exception());
+ getUser.TriggerFailure(new WebException());
}
return true;
diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs
index 3fc894da0d..bb7fcc2fce 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs
@@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Online;
+using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapListing.Panels;
using osu.Game.Rulesets.Osu;
using osu.Game.Tests.Resources;
@@ -74,7 +75,7 @@ namespace osu.Game.Tests.Visual.Online
{
ID = 1,
OnlineBeatmapSetID = 241526,
- OnlineInfo = new BeatmapSetOnlineInfo
+ OnlineInfo = new APIBeatmapSet
{
Availability = new BeatmapSetOnlineAvailability
{
diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs
index 722010ace2..6caca2a67c 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs
@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Beatmaps;
+using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapListing.Panels;
using osu.Game.Rulesets;
using osu.Game.Users;
@@ -31,7 +32,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 3,
},
},
- OnlineInfo = new BeatmapSetOnlineInfo
+ OnlineInfo = new APIBeatmapSet
{
Availability = new BeatmapSetOnlineAvailability
{
@@ -86,7 +87,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 3,
}
},
- OnlineInfo = new BeatmapSetOnlineInfo
+ OnlineInfo = new APIBeatmapSet
{
HasVideo = true,
HasStoryboard = true,
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs
index 1e9d62f379..b5d2d15392 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfilePreviousUsernames.cs
@@ -3,11 +3,7 @@
using System;
using NUnit.Framework;
-using osu.Framework.Allocation;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
-using osu.Game.Online.API;
-using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Users;
@@ -16,48 +12,50 @@ namespace osu.Game.Tests.Visual.Online
[TestFixture]
public class TestSceneUserProfilePreviousUsernames : OsuTestScene
{
- [Resolved]
- private IAPIProvider api { get; set; }
+ private PreviousUsernames container;
- private readonly Bindable user = new Bindable();
-
- public TestSceneUserProfilePreviousUsernames()
+ [SetUp]
+ public void SetUp() => Schedule(() =>
{
- Child = new PreviousUsernames
+ Child = container = new PreviousUsernames
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- User = { BindTarget = user },
};
+ });
- User[] users =
- {
- new User { PreviousUsernames = new[] { "username1" } },
- new User { PreviousUsernames = new[] { "longusername", "longerusername" } },
- new User { PreviousUsernames = new[] { "test", "angelsim", "verylongusername" } },
- new User { PreviousUsernames = new[] { "ihavenoidea", "howcani", "makethistext", "anylonger" } },
- new User { PreviousUsernames = Array.Empty() },
- null
- };
-
- AddStep("single username", () => user.Value = users[0]);
- AddStep("two usernames", () => user.Value = users[1]);
- AddStep("three usernames", () => user.Value = users[2]);
- AddStep("four usernames", () => user.Value = users[3]);
- AddStep("no username", () => user.Value = users[4]);
- AddStep("null user", () => user.Value = users[5]);
- }
-
- protected override void LoadComplete()
+ [Test]
+ public void TestVisibility()
{
- base.LoadComplete();
+ AddAssert("Is Hidden", () => container.Alpha == 0);
- AddStep("online user (Angelsim)", () =>
- {
- var request = new GetUserRequest(1777162);
- request.Success += user => this.user.Value = user;
- api.Queue(request);
- });
+ AddStep("1 username", () => container.User.Value = users[0]);
+ AddUntilStep("Is visible", () => container.Alpha == 1);
+
+ AddStep("2 usernames", () => container.User.Value = users[1]);
+ AddUntilStep("Is visible", () => container.Alpha == 1);
+
+ AddStep("3 usernames", () => container.User.Value = users[2]);
+ AddUntilStep("Is visible", () => container.Alpha == 1);
+
+ AddStep("4 usernames", () => container.User.Value = users[3]);
+ AddUntilStep("Is visible", () => container.Alpha == 1);
+
+ AddStep("No username", () => container.User.Value = users[4]);
+ AddUntilStep("Is hidden", () => container.Alpha == 0);
+
+ AddStep("Null user", () => container.User.Value = users[5]);
+ AddUntilStep("Is hidden", () => container.Alpha == 0);
}
+
+ private static readonly User[] users =
+ {
+ new User { Id = 1, PreviousUsernames = new[] { "username1" } },
+ new User { Id = 2, PreviousUsernames = new[] { "longusername", "longerusername" } },
+ new User { Id = 3, PreviousUsernames = new[] { "test", "angelsim", "verylongusername" } },
+ new User { Id = 4, PreviousUsernames = new[] { "ihavenoidea", "howcani", "makethistext", "anylonger" } },
+ new User { Id = 5, PreviousUsernames = Array.Empty() },
+ null
+ };
}
}
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
index 4bcc887b9f..d948aebbbf 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
@@ -32,12 +32,14 @@ namespace osu.Game.Tests.Visual.Playlists
private TestResultsScreen resultsScreen;
private int currentScoreId;
private bool requestComplete;
+ private int totalCount;
[SetUp]
public void Setup() => Schedule(() =>
{
currentScoreId = 0;
requestComplete = false;
+ totalCount = 0;
bindHandler();
});
@@ -53,7 +55,6 @@ namespace osu.Game.Tests.Visual.Playlists
});
createResults(() => userScore);
- waitForDisplay();
AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded);
}
@@ -62,7 +63,6 @@ namespace osu.Game.Tests.Visual.Playlists
public void TestShowNullUserScore()
{
createResults();
- waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
}
@@ -79,7 +79,6 @@ namespace osu.Game.Tests.Visual.Playlists
});
createResults(() => userScore);
- waitForDisplay();
AddAssert("more than 1 panel displayed", () => this.ChildrenOfType().Count() > 1);
AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded);
@@ -91,7 +90,6 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("bind delayed handler", () => bindHandler(true));
createResults();
- waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
}
@@ -100,7 +98,6 @@ namespace osu.Game.Tests.Visual.Playlists
public void TestFetchWhenScrolledToTheRight()
{
createResults();
- waitForDisplay();
AddStep("bind delayed handler", () => bindHandler(true));
@@ -131,7 +128,6 @@ namespace osu.Game.Tests.Visual.Playlists
});
createResults(() => userScore);
- waitForDisplay();
AddStep("bind delayed handler", () => bindHandler(true));
@@ -161,13 +157,15 @@ namespace osu.Game.Tests.Visual.Playlists
}));
});
- AddUntilStep("wait for load", () => resultsScreen.ChildrenOfType().FirstOrDefault()?.AllPanelsVisible == true);
+ waitForDisplay();
}
private void waitForDisplay()
{
- AddUntilStep("wait for request to complete", () => requestComplete);
- AddUntilStep("wait for panels to be visible", () => resultsScreen.ChildrenOfType().FirstOrDefault()?.AllPanelsVisible == true);
+ AddUntilStep("wait for load to complete", () =>
+ requestComplete
+ && resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount
+ && resultsScreen.ScorePanelList.AllPanelsVisible);
AddWaitStep("wait for display", 5);
}
@@ -203,6 +201,7 @@ namespace osu.Game.Tests.Visual.Playlists
triggerFail(s);
else
triggerSuccess(s, createUserResponse(userScore));
+
break;
case IndexPlaylistScoresRequest i:
@@ -248,6 +247,8 @@ namespace osu.Game.Tests.Visual.Playlists
}
};
+ totalCount++;
+
for (int i = 1; i <= scores_per_result; i++)
{
multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore
@@ -285,6 +286,8 @@ namespace osu.Game.Tests.Visual.Playlists
},
Statistics = userScore.Statistics
});
+
+ totalCount += 2;
}
addCursor(multiplayerUserScore.ScoresAround.Lower);
@@ -325,6 +328,8 @@ namespace osu.Game.Tests.Visual.Playlists
{ HitResult.Great, 300 }
}
});
+
+ totalCount++;
}
addCursor(result);
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneRestoreDefaultValueButton.cs b/osu.Game.Tests/Visual/Settings/TestSceneRestoreDefaultValueButton.cs
new file mode 100644
index 0000000000..3eb7a77600
--- /dev/null
+++ b/osu.Game.Tests/Visual/Settings/TestSceneRestoreDefaultValueButton.cs
@@ -0,0 +1,63 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Overlays;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Settings
+{
+ public class TestSceneRestoreDefaultValueButton : OsuTestScene
+ {
+ [Resolved]
+ private OsuColour colours { get; set; }
+
+ private float scale = 1;
+
+ private readonly Bindable current = new Bindable
+ {
+ Default = default,
+ Value = 1,
+ };
+
+ [Test]
+ public void TestBasic()
+ {
+ RestoreDefaultValueButton restoreDefaultValueButton = null;
+
+ AddStep("create button", () => Child = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colours.GreySeafoam
+ },
+ restoreDefaultValueButton = new RestoreDefaultValueButton
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Scale = new Vector2(scale),
+ Current = current,
+ }
+ }
+ });
+ AddSliderStep("set scale", 1, 4, 1, scale =>
+ {
+ this.scale = scale;
+ if (restoreDefaultValueButton != null)
+ restoreDefaultValueButton.Scale = new Vector2(scale);
+ });
+ AddToggleStep("toggle default state", state => current.Value = state ? default : 1);
+ AddToggleStep("toggle disabled state", state => current.Disabled = state);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs
index d9cce69ee3..83265e13ad 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs
@@ -5,6 +5,9 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays;
@@ -29,9 +32,10 @@ namespace osu.Game.Tests.Visual.Settings
Value = "test"
}
};
-
- restoreDefaultValueButton = textBox.ChildrenOfType>().Single();
});
+ AddUntilStep("wait for loaded", () => textBox.IsLoaded);
+ AddStep("retrieve restore default button", () => restoreDefaultValueButton = textBox.ChildrenOfType>().Single());
+
AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
AddStep("change value from default", () => textBox.Current.Value = "non-default");
@@ -41,6 +45,48 @@ namespace osu.Game.Tests.Visual.Settings
AddUntilStep("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
}
+ [Test]
+ public void TestSetAndClearLabelText()
+ {
+ SettingsTextBox textBox = null;
+ RestoreDefaultValueButton restoreDefaultValueButton = null;
+ OsuTextBox control = null;
+
+ AddStep("create settings item", () =>
+ {
+ Child = textBox = new SettingsTextBox
+ {
+ Current = new Bindable
+ {
+ Default = "test",
+ Value = "test"
+ }
+ };
+ });
+ AddUntilStep("wait for loaded", () => textBox.IsLoaded);
+ AddStep("retrieve components", () =>
+ {
+ restoreDefaultValueButton = textBox.ChildrenOfType>().Single();
+ control = textBox.ChildrenOfType().Single();
+ });
+
+ AddStep("set non-default value", () => restoreDefaultValueButton.Current.Value = "non-default");
+ AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
+
+ AddStep("set label", () => textBox.LabelText = "label text");
+ AddAssert("default value button centre aligned to label size", () =>
+ {
+ var label = textBox.ChildrenOfType().Single(spriteText => spriteText.Text == "label text");
+ return Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, label.DrawHeight, 1);
+ });
+
+ AddStep("clear label", () => textBox.LabelText = default);
+ AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
+
+ AddStep("set warning text", () => textBox.WarningText = "This is some very important warning text! Hopefully it doesn't break the alignment of the default value indicator...");
+ AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
+ }
+
///
/// Ensures that the reset to default button uses the correct implementation of IsDefault to determine whether it should be shown or not.
/// Values have been chosen so that after being set, Value != Default (but they are close enough that the difference is negligible compared to Precision).
@@ -64,9 +110,9 @@ namespace osu.Game.Tests.Visual.Settings
Precision = 0.1f,
}
};
-
- restoreDefaultValueButton = sliderBar.ChildrenOfType>().Single();
});
+ AddUntilStep("wait for loaded", () => sliderBar.IsLoaded);
+ AddStep("retrieve restore default button", () => restoreDefaultValueButton = sliderBar.ChildrenOfType>().Single());
AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
index 997eac709d..dc5b0e0d77 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
@@ -3,6 +3,7 @@
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Handlers.Tablet;
@@ -21,6 +22,9 @@ namespace osu.Game.Tests.Visual.Settings
private TestTabletHandler tabletHandler;
private TabletSettings settings;
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
+
[SetUpSteps]
public void SetUpSteps()
{
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs
index d5b4fb9a80..1125e16d91 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs
@@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
+using osu.Game.Online.API.Requests.Responses;
using osu.Game.Screens.Select;
namespace osu.Game.Tests.Visual.SongSelect
@@ -34,7 +35,10 @@ namespace osu.Game.Tests.Visual.SongSelect
{
BeatmapSet = new BeatmapSetInfo
{
- Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() }
+ OnlineInfo = new APIBeatmapSet
+ {
+ Ratings = Enumerable.Range(0, 11).ToArray(),
+ }
},
Version = "All Metrics",
Metadata = new BeatmapMetadata
@@ -50,11 +54,14 @@ namespace osu.Game.Tests.Visual.SongSelect
ApproachRate = 3.5f,
},
StarDifficulty = 5.3f,
- Metrics = new BeatmapMetrics
+ OnlineInfo = new APIBeatmap
{
- Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
- Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
- },
+ FailTimes = new APIFailTimes
+ {
+ Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
+ Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
+ },
+ }
});
}
@@ -65,7 +72,10 @@ namespace osu.Game.Tests.Visual.SongSelect
{
BeatmapSet = new BeatmapSetInfo
{
- Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() }
+ OnlineInfo = new APIBeatmapSet
+ {
+ Ratings = Enumerable.Range(0, 11).ToArray(),
+ }
},
Version = "All Metrics",
Metadata = new BeatmapMetadata
@@ -80,11 +90,14 @@ namespace osu.Game.Tests.Visual.SongSelect
ApproachRate = 3.5f,
},
StarDifficulty = 5.3f,
- Metrics = new BeatmapMetrics
+ OnlineInfo = new APIBeatmap
{
- Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
- Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
- },
+ FailTimes = new APIFailTimes
+ {
+ Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
+ Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
+ },
+ }
});
}
@@ -95,7 +108,10 @@ namespace osu.Game.Tests.Visual.SongSelect
{
BeatmapSet = new BeatmapSetInfo
{
- Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() }
+ OnlineInfo = new APIBeatmapSet
+ {
+ Ratings = Enumerable.Range(0, 11).ToArray(),
+ }
},
Version = "Only Ratings",
Metadata = new BeatmapMetadata
@@ -133,11 +149,14 @@ namespace osu.Game.Tests.Visual.SongSelect
ApproachRate = 7,
},
StarDifficulty = 2.91f,
- Metrics = new BeatmapMetrics
+ OnlineInfo = new APIBeatmap
{
- Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
- Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
- },
+ FailTimes = new APIFailTimes
+ {
+ Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
+ Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
+ },
+ }
});
}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs
index a5b90e6655..0ae4e0c5dc 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs
@@ -205,7 +205,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) =>
AddAssert($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'",
// A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872
- () => shouldContain == (getCollectionDropdownItems().Any(i => i.ChildrenOfType().OfType().First().Text == collectionName)));
+ () => shouldContain == (getCollectionDropdownItems().Any(i => i.ChildrenOfType().OfType().First().Text == collectionName)));
private IconButton getAddOrRemoveButton(int index)
=> getCollectionDropdownItems().ElementAt(index).ChildrenOfType().Single();
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index 067f1cabb4..4811fc979e 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -142,6 +142,8 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("store selected beatmap", () => selected = Beatmap.Value);
+ AddUntilStep("wait for beatmaps to load", () => songSelect.Carousel.ChildrenOfType().Any());
+
AddStep("select next and enter", () =>
{
InputManager.MoveMouseTo(songSelect.Carousel.ChildrenOfType()
@@ -599,10 +601,10 @@ namespace osu.Game.Tests.Visual.SongSelect
});
FilterableDifficultyIcon difficultyIcon = null;
- AddStep("Find an icon", () =>
+ AddUntilStep("Find an icon", () =>
{
- difficultyIcon = set.ChildrenOfType()
- .First(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex());
+ return (difficultyIcon = set.ChildrenOfType()
+ .FirstOrDefault(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex())) != null;
});
AddStep("Click on a difficulty", () =>
@@ -765,10 +767,10 @@ namespace osu.Game.Tests.Visual.SongSelect
});
FilterableGroupedDifficultyIcon groupIcon = null;
- AddStep("Find group icon for different ruleset", () =>
+ AddUntilStep("Find group icon for different ruleset", () =>
{
- groupIcon = set.ChildrenOfType()
- .First(icon => icon.Items.First().BeatmapInfo.Ruleset.ID == 3);
+ return (groupIcon = set.ChildrenOfType()
+ .FirstOrDefault(icon => icon.Items.First().BeatmapInfo.Ruleset.ID == 3)) != null;
});
AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0);
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs
index 008d91f649..a9fe7ed7d8 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.Sprites;
+using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
using osuTK;
@@ -111,7 +112,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private static readonly BeatmapSetInfo beatmap_set = new BeatmapSetInfo
{
- OnlineInfo = new BeatmapSetOnlineInfo
+ OnlineInfo = new APIBeatmapSet
{
Covers = new BeatmapSetOnlineCovers
{
@@ -122,7 +123,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private static readonly BeatmapSetInfo no_cover_beatmap_set = new BeatmapSetInfo
{
- OnlineInfo = new BeatmapSetOnlineInfo
+ OnlineInfo = new APIBeatmapSet
{
Covers = new BeatmapSetOnlineCovers
{
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs
index c51204eaba..6727c7560b 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs
@@ -11,6 +11,7 @@ using osu.Game.Users;
using System;
using osu.Framework.Graphics.Shapes;
using System.Collections.Generic;
+using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Tests.Visual.UserInterface
{
@@ -69,7 +70,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Id = 100
}
},
- OnlineInfo = new BeatmapSetOnlineInfo
+ OnlineInfo = new APIBeatmapSet
{
Covers = new BeatmapSetOnlineCovers
{
@@ -90,7 +91,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Id = 100
}
},
- OnlineInfo = new BeatmapSetOnlineInfo
+ OnlineInfo = new APIBeatmapSet
{
Covers = new BeatmapSetOnlineCovers
{
@@ -115,7 +116,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Id = 100
}
},
- OnlineInfo = new BeatmapSetOnlineInfo
+ OnlineInfo = new APIBeatmapSet
{
Covers = new BeatmapSetOnlineCovers
{
@@ -136,7 +137,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Id = 100
}
},
- OnlineInfo = new BeatmapSetOnlineInfo
+ OnlineInfo = new APIBeatmapSet
{
Covers = new BeatmapSetOnlineCovers
{
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
index 189b143a35..9a75d3c309 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs
@@ -163,7 +163,6 @@ namespace osu.Game.Tests.Visual.UserInterface
});
AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
-
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scoreBeingDeleted.OnlineScoreID));
}
@@ -171,6 +170,7 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestDeleteViaDatabase()
{
AddStep("delete top score", () => scoreManager.Delete(importedScores[0]));
+ AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != importedScores[0].OnlineScoreID));
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs
index 393420e700..1b7f65f9a0 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs
@@ -1,11 +1,15 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using System.Linq;
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.UserInterface
{
@@ -19,28 +23,62 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep("create component", () =>
{
- LabelledSliderBar component;
+ FillFlowContainer flow;
- Child = new Container
+ Child = flow = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 500,
AutoSizeAxes = Axes.Y,
- Child = component = new LabelledSliderBar
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
{
- Current = new BindableDouble(5)
+ new LabelledSliderBar
{
- MinValue = 0,
- MaxValue = 10,
- Precision = 1,
- }
- }
+ Current = new BindableDouble(5)
+ {
+ MinValue = 0,
+ MaxValue = 10,
+ Precision = 1,
+ },
+ Label = "a sample component",
+ Description = hasDescription ? "this text describes the component" : string.Empty,
+ },
+ },
};
- component.Label = "a sample component";
- component.Description = hasDescription ? "this text describes the component" : string.Empty;
+ foreach (var colour in Enum.GetValues(typeof(OverlayColourScheme)).OfType())
+ {
+ flow.Add(new OverlayColourContainer(colour)
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Child = new LabelledSliderBar
+ {
+ Current = new BindableDouble(5)
+ {
+ MinValue = 0,
+ MaxValue = 10,
+ Precision = 1,
+ },
+ Label = "a sample component",
+ Description = hasDescription ? "this text describes the component" : string.Empty,
+ }
+ });
+ }
});
}
+
+ private class OverlayColourContainer : Container
+ {
+ [Cached]
+ private OverlayColourProvider colourProvider;
+
+ public OverlayColourContainer(OverlayColourScheme scheme)
+ {
+ colourProvider = new OverlayColourProvider(scheme);
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs
new file mode 100644
index 0000000000..9e77fcf675
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuDropdown.cs
@@ -0,0 +1,20 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics.UserInterface;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneOsuDropdown : ThemeComparisonTestScene
+ {
+ protected override Drawable CreateContent() =>
+ new OsuEnumDropdown
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Width = 150
+ };
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs
index 756928d3ec..fc1866cdf3 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuTextBox.cs
@@ -1,80 +1,67 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
+using System.Linq;
using NUnit.Framework;
+using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
+using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays;
using osuTK;
-using osuTK.Graphics;
namespace osu.Game.Tests.Visual.UserInterface
{
- public class TestSceneOsuTextBox : OsuTestScene
+ public class TestSceneOsuTextBox : ThemeComparisonTestScene
{
- private readonly OsuNumberBox numberBox;
+ private IEnumerable numberBoxes => this.ChildrenOfType();
- public TestSceneOsuTextBox()
+ protected override Drawable CreateContent() => new FillFlowContainer
{
- Child = new Container
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Padding = new MarginPadding(50f),
+ Spacing = new Vector2(0f, 50f),
+ Children = new[]
{
- Masking = true,
- CornerRadius = 10f,
- AutoSizeAxes = Axes.Both,
- Padding = new MarginPadding(15f),
- Children = new Drawable[]
+ new OsuTextBox
{
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = Color4.DarkSlateGray,
- Alpha = 0.75f,
- },
- new FillFlowContainer
- {
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Vertical,
- Padding = new MarginPadding(50f),
- Spacing = new Vector2(0f, 50f),
- Children = new[]
- {
- new OsuTextBox
- {
- Width = 500f,
- PlaceholderText = "Normal textbox",
- },
- new OsuPasswordTextBox
- {
- Width = 500f,
- PlaceholderText = "Password textbox",
- },
- numberBox = new OsuNumberBox
- {
- Width = 500f,
- PlaceholderText = "Number textbox"
- }
- }
- }
+ RelativeSizeAxes = Axes.X,
+ PlaceholderText = "Normal textbox",
+ },
+ new OsuPasswordTextBox
+ {
+ RelativeSizeAxes = Axes.X,
+ PlaceholderText = "Password textbox",
+ },
+ new OsuNumberBox
+ {
+ RelativeSizeAxes = Axes.X,
+ PlaceholderText = "Number textbox"
}
- };
- }
+ }
+ };
[Test]
public void TestNumberBox()
{
- clearTextbox(numberBox);
- AddStep("enter numbers", () => numberBox.Text = "987654321");
- expectedValue(numberBox, "987654321");
+ AddStep("create themed content", () => CreateThemedContent(OverlayColourScheme.Red));
- clearTextbox(numberBox);
- AddStep("enter text + single number", () => numberBox.Text = "1 hello 2 world 3");
- expectedValue(numberBox, "123");
+ clearTextboxes(numberBoxes);
+ AddStep("enter numbers", () => numberBoxes.ForEach(numberBox => numberBox.Text = "987654321"));
+ expectedValue(numberBoxes, "987654321");
- clearTextbox(numberBox);
+ clearTextboxes(numberBoxes);
+ AddStep("enter text + single number", () => numberBoxes.ForEach(numberBox => numberBox.Text = "1 hello 2 world 3"));
+ expectedValue(numberBoxes, "123");
+
+ clearTextboxes(numberBoxes);
}
- private void clearTextbox(OsuTextBox textBox) => AddStep("clear textbox", () => textBox.Text = null);
- private void expectedValue(OsuTextBox textBox, string value) => AddAssert("expected textbox value", () => textBox.Text == value);
+ private void clearTextboxes(IEnumerable textBoxes) => AddStep("clear textbox", () => textBoxes.ForEach(textBox => textBox.Text = null));
+ private void expectedValue(IEnumerable textBoxes, string value) => AddAssert("expected textbox value", () => textBoxes.All(textbox => textbox.Text == value));
}
}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs
new file mode 100644
index 0000000000..9ccfba7c74
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs
@@ -0,0 +1,44 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics.UserInterfaceV2;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneRoundedButton : OsuTestScene
+ {
+ [Test]
+ public void TestBasic()
+ {
+ RoundedButton button = null;
+
+ AddStep("create button", () => Child = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Colour4.DarkGray
+ },
+ button = new RoundedButton
+ {
+ Width = 400,
+ Text = "Test button",
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Action = () => { }
+ }
+ }
+ });
+
+ AddToggleStep("toggle disabled", disabled => button.Action = disabled ? (Action)null : () => { });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsCheckbox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsCheckbox.cs
new file mode 100644
index 0000000000..fb04c5bad0
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsCheckbox.cs
@@ -0,0 +1,68 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Settings;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneSettingsCheckbox : OsuTestScene
+ {
+ [TestCase]
+ public void TestCheckbox()
+ {
+ AddStep("create component", () =>
+ {
+ FillFlowContainer flow;
+
+ Child = flow = new FillFlowContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Width = 500,
+ AutoSizeAxes = Axes.Y,
+ Spacing = new Vector2(5),
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ new SettingsCheckbox
+ {
+ LabelText = "a sample component",
+ },
+ },
+ };
+
+ foreach (var colour1 in Enum.GetValues(typeof(OverlayColourScheme)).OfType())
+ {
+ flow.Add(new OverlayColourContainer(colour1)
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Child = new SettingsCheckbox
+ {
+ LabelText = "a sample component",
+ }
+ });
+ }
+ });
+ }
+
+ private class OverlayColourContainer : Container
+ {
+ [Cached]
+ private OverlayColourProvider colourProvider;
+
+ public OverlayColourContainer(OverlayColourScheme scheme)
+ {
+ colourProvider = new OverlayColourProvider(scheme);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapSetCover.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapSetCover.cs
index 4fef93e291..3ac3002713 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapSetCover.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapSetCover.cs
@@ -13,6 +13,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics.Containers;
+using osu.Game.Online.API.Requests.Responses;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
@@ -22,21 +23,21 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestLocal([Values] BeatmapSetCoverType coverType)
{
- AddStep("setup cover", () => Child = new UpdateableBeatmapSetCover(coverType)
+ AddStep("setup cover", () => Child = new UpdateableOnlineBeatmapSetCover(coverType)
{
BeatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet,
RelativeSizeAxes = Axes.Both,
Masking = true,
});
- AddUntilStep("wait for load", () => this.ChildrenOfType().SingleOrDefault()?.IsLoaded ?? false);
+ AddUntilStep("wait for load", () => this.ChildrenOfType().SingleOrDefault()?.IsLoaded ?? false);
}
[Test]
public void TestUnloadAndReload()
{
OsuScrollContainer scroll = null;
- List covers = new List();
+ List covers = new List();
AddStep("setup covers", () =>
{
@@ -65,7 +66,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
var coverType = coverTypes[i % coverTypes.Count];
- var cover = new UpdateableBeatmapSetCover(coverType)
+ var cover = new UpdateableOnlineBeatmapSetCover(coverType)
{
BeatmapSet = setInfo,
Height = 100,
@@ -84,7 +85,7 @@ namespace osu.Game.Tests.Visual.UserInterface
}
});
- var loadedCovers = covers.Where(c => c.ChildrenOfType().SingleOrDefault()?.IsLoaded ?? false);
+ var loadedCovers = covers.Where(c => c.ChildrenOfType().SingleOrDefault()?.IsLoaded ?? false);
AddUntilStep("some loaded", () => loadedCovers.Any());
AddStep("scroll to end", () => scroll.ScrollToEnd());
@@ -94,9 +95,9 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestSetNullBeatmapWhileLoading()
{
- TestUpdateableBeatmapSetCover updateableCover = null;
+ TestUpdateableOnlineBeatmapSetCover updateableCover = null;
- AddStep("setup cover", () => Child = updateableCover = new TestUpdateableBeatmapSetCover
+ AddStep("setup cover", () => Child = updateableCover = new TestUpdateableOnlineBeatmapSetCover
{
BeatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet,
RelativeSizeAxes = Axes.Both,
@@ -111,10 +112,10 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestCoverChangeOnNewBeatmap()
{
- TestUpdateableBeatmapSetCover updateableCover = null;
- BeatmapSetCover initialCover = null;
+ TestUpdateableOnlineBeatmapSetCover updateableCover = null;
+ OnlineBeatmapSetCover initialCover = null;
- AddStep("setup cover", () => Child = updateableCover = new TestUpdateableBeatmapSetCover(0)
+ AddStep("setup cover", () => Child = updateableCover = new TestUpdateableOnlineBeatmapSetCover(0)
{
BeatmapSet = createBeatmapWithCover("https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg"),
RelativeSizeAxes = Axes.Both,
@@ -122,38 +123,38 @@ namespace osu.Game.Tests.Visual.UserInterface
Alpha = 0.4f
});
- AddUntilStep("cover loaded", () => updateableCover.ChildrenOfType().Any());
- AddStep("store initial cover", () => initialCover = updateableCover.ChildrenOfType().Single());
+ AddUntilStep("cover loaded", () => updateableCover.ChildrenOfType().Any());
+ AddStep("store initial cover", () => initialCover = updateableCover.ChildrenOfType().Single());
AddUntilStep("wait for fade complete", () => initialCover.Alpha == 1);
AddStep("switch beatmap",
() => updateableCover.BeatmapSet = createBeatmapWithCover("https://assets.ppy.sh/beatmaps/1079428/covers/cover.jpg"));
- AddUntilStep("new cover loaded", () => updateableCover.ChildrenOfType().Except(new[] { initialCover }).Any());
+ AddUntilStep("new cover loaded", () => updateableCover.ChildrenOfType().Except(new[] { initialCover }).Any());
}
private static BeatmapSetInfo createBeatmapWithCover(string coverUrl) => new BeatmapSetInfo
{
- OnlineInfo = new BeatmapSetOnlineInfo
+ OnlineInfo = new APIBeatmapSet
{
Covers = new BeatmapSetOnlineCovers { Cover = coverUrl }
}
};
- private class TestUpdateableBeatmapSetCover : UpdateableBeatmapSetCover
+ private class TestUpdateableOnlineBeatmapSetCover : UpdateableOnlineBeatmapSetCover
{
private readonly int loadDelay;
- public TestUpdateableBeatmapSetCover(int loadDelay = 10000)
+ public TestUpdateableOnlineBeatmapSetCover(int loadDelay = 10000)
{
this.loadDelay = loadDelay;
}
- protected override Drawable CreateDrawable(BeatmapSetInfo model)
+ protected override Drawable CreateDrawable(IBeatmapSetOnlineInfo model)
{
if (model == null)
return null;
- return new TestBeatmapSetCover(model, loadDelay)
+ return new TestOnlineBeatmapSetCover(model, loadDelay)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -163,11 +164,11 @@ namespace osu.Game.Tests.Visual.UserInterface
}
}
- private class TestBeatmapSetCover : BeatmapSetCover
+ private class TestOnlineBeatmapSetCover : OnlineBeatmapSetCover
{
private readonly int loadDelay;
- public TestBeatmapSetCover(BeatmapSetInfo set, int loadDelay)
+ public TestOnlineBeatmapSetCover(IBeatmapSetOnlineInfo set, int loadDelay)
: base(set)
{
this.loadDelay = loadDelay;
diff --git a/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs b/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs
new file mode 100644
index 0000000000..db1c90f287
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/ThemeComparisonTestScene.cs
@@ -0,0 +1,69 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Overlays;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public abstract class ThemeComparisonTestScene : OsuGridTestScene
+ {
+ protected ThemeComparisonTestScene()
+ : base(1, 2)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ Cell(0, 0).AddRange(new[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colours.GreySeafoam
+ },
+ CreateContent()
+ });
+ }
+
+ protected void CreateThemedContent(OverlayColourScheme colourScheme)
+ {
+ var colourProvider = new OverlayColourProvider(colourScheme);
+
+ Cell(0, 1).Clear();
+ Cell(0, 1).Add(new DependencyProvidingContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ CachedDependencies = new (Type, object)[]
+ {
+ (typeof(OverlayColourProvider), colourProvider)
+ },
+ Children = new[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colourProvider.Background4
+ },
+ CreateContent()
+ }
+ });
+ }
+
+ protected abstract Drawable CreateContent();
+
+ [Test]
+ public void TestAllColourSchemes()
+ {
+ foreach (var scheme in Enum.GetValues(typeof(OverlayColourScheme)).Cast())
+ AddStep($"set {scheme} scheme", () => CreateThemedContent(scheme));
+ }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/NonVisual/LadderInfoSerialisationTest.cs b/osu.Game.Tournament.Tests/NonVisual/LadderInfoSerialisationTest.cs
new file mode 100644
index 0000000000..13cbcd3caf
--- /dev/null
+++ b/osu.Game.Tournament.Tests/NonVisual/LadderInfoSerialisationTest.cs
@@ -0,0 +1,65 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Newtonsoft.Json;
+using NUnit.Framework;
+using osu.Game.Tournament.Models;
+
+namespace osu.Game.Tournament.Tests.NonVisual
+{
+ [TestFixture]
+ public class LadderInfoSerialisationTest
+ {
+ [Test]
+ public void TestDeserialise()
+ {
+ var ladder = createSampleLadder();
+ string serialised = JsonConvert.SerializeObject(ladder);
+
+ JsonConvert.DeserializeObject(serialised, new JsonPointConverter());
+ }
+
+ [Test]
+ public void TestSerialise()
+ {
+ var ladder = createSampleLadder();
+ JsonConvert.SerializeObject(ladder);
+ }
+
+ private static LadderInfo createSampleLadder()
+ {
+ var match = TournamentTestScene.CreateSampleMatch();
+
+ return new LadderInfo
+ {
+ PlayersPerTeam = { Value = 4 },
+ Teams =
+ {
+ match.Team1.Value,
+ match.Team2.Value,
+ },
+ Rounds =
+ {
+ new TournamentRound
+ {
+ Beatmaps =
+ {
+ new RoundBeatmap { BeatmapInfo = TournamentTestScene.CreateSampleBeatmapInfo() },
+ new RoundBeatmap { BeatmapInfo = TournamentTestScene.CreateSampleBeatmapInfo() },
+ }
+ }
+ },
+
+ Matches =
+ {
+ match,
+ },
+ Progressions =
+ {
+ new TournamentProgression(1, 2),
+ new TournamentProgression(1, 3, true),
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs b/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs
index b14684200f..319a768e65 100644
--- a/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs
+++ b/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
public static TournamentGameBase LoadTournament(GameHost host, TournamentGameBase tournament = null)
{
tournament ??= new TournamentGameBase();
- Task.Run(() => host.Run(tournament))
+ Task.Factory.StartNew(() => host.Run(tournament), TaskCreationOptions.LongRunning)
.ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted);
WaitForOrAssert(() => tournament.IsLoaded, @"osu! failed to start in a reasonable amount of time");
return tournament;
diff --git a/osu.Game.Tournament.Tests/TournamentTestScene.cs b/osu.Game.Tournament.Tests/TournamentTestScene.cs
index 93e1e018a5..ce9fd91ff1 100644
--- a/osu.Game.Tournament.Tests/TournamentTestScene.cs
+++ b/osu.Game.Tournament.Tests/TournamentTestScene.cs
@@ -8,6 +8,7 @@ using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
+using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Tests.Visual;
using osu.Game.Tournament.IO;
@@ -152,7 +153,16 @@ namespace osu.Game.Tournament.Tests
};
public static BeatmapInfo CreateSampleBeatmapInfo() =>
- new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist", ID = RNG.Next(0, 1000000) } };
+ new BeatmapInfo
+ {
+ Metadata = new BeatmapMetadata
+ {
+ Title = "Test Title",
+ Artist = "Test Artist",
+ ID = RNG.Next(0, 1000000)
+ },
+ OnlineInfo = new APIBeatmap(),
+ };
protected override ITestSceneTestRunner CreateRunner() => new TournamentTestSceneTestRunner();
diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
index 0e5a66e7fe..be29566e07 100644
--- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
+++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs
@@ -10,7 +10,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures;
-using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
@@ -21,7 +20,8 @@ namespace osu.Game.Tournament.Components
{
public class TournamentBeatmapPanel : CompositeDrawable
{
- public readonly BeatmapInfo BeatmapInfo;
+ public readonly IBeatmapInfo BeatmapInfo;
+
private readonly string mod;
private const float horizontal_padding = 10;
@@ -32,12 +32,13 @@ namespace osu.Game.Tournament.Components
private readonly Bindable currentMatch = new Bindable();
private Box flash;
- public TournamentBeatmapPanel(BeatmapInfo beatmapInfo, string mod = null)
+ public TournamentBeatmapPanel(IBeatmapInfo beatmapInfo, string mod = null)
{
if (beatmapInfo == null) throw new ArgumentNullException(nameof(beatmapInfo));
BeatmapInfo = beatmapInfo;
this.mod = mod;
+
Width = 400;
Height = HEIGHT;
}
@@ -57,11 +58,11 @@ namespace osu.Game.Tournament.Components
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
},
- new UpdateableBeatmapSetCover
+ new UpdateableOnlineBeatmapSetCover
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(0.5f),
- BeatmapSet = BeatmapInfo.BeatmapSet,
+ BeatmapSet = BeatmapInfo.BeatmapSet as IBeatmapSetOnlineInfo,
},
new FillFlowContainer
{
@@ -74,9 +75,7 @@ namespace osu.Game.Tournament.Components
{
new TournamentSpriteText
{
- Text = new RomanisableString(
- $"{BeatmapInfo.Metadata.ArtistUnicode ?? BeatmapInfo.Metadata.Artist} - {BeatmapInfo.Metadata.TitleUnicode ?? BeatmapInfo.Metadata.Title}",
- $"{BeatmapInfo.Metadata.Artist} - {BeatmapInfo.Metadata.Title}"),
+ Text = BeatmapInfo.GetDisplayTitleRomanisable(false),
Font = OsuFont.Torus.With(weight: FontWeight.Bold),
},
new FillFlowContainer
@@ -93,7 +92,7 @@ namespace osu.Game.Tournament.Components
},
new TournamentSpriteText
{
- Text = BeatmapInfo.Metadata.AuthorString,
+ Text = BeatmapInfo.Metadata?.Author,
Padding = new MarginPadding { Right = 20 },
Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14)
},
@@ -105,7 +104,7 @@ namespace osu.Game.Tournament.Components
},
new TournamentSpriteText
{
- Text = BeatmapInfo.Version,
+ Text = BeatmapInfo.DifficultyName,
Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14)
},
}
@@ -149,7 +148,7 @@ namespace osu.Game.Tournament.Components
private void updateState()
{
- var found = currentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == BeatmapInfo.OnlineBeatmapID);
+ var found = currentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == BeatmapInfo.OnlineID);
bool doFlash = found != choice;
choice = found;
diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs
index 994dee4da0..77101e4023 100644
--- a/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs
+++ b/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs
@@ -127,7 +127,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
score2Text.X = Math.Max(5 + score2Text.DrawWidth / 2, score2Bar.DrawWidth);
}
- private class MatchScoreCounter : ScoreCounter
+ private class MatchScoreCounter : CommaSeparatedScoreCounter
{
private OsuSpriteText displayedSpriteText;
diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs
index 1e3c550323..5f6546c303 100644
--- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs
+++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs
@@ -147,11 +147,11 @@ namespace osu.Game.Tournament.Screens.MapPool
if (map != null)
{
- if (e.Button == MouseButton.Left && map.BeatmapInfo.OnlineBeatmapID != null)
- addForBeatmap(map.BeatmapInfo.OnlineBeatmapID.Value);
+ if (e.Button == MouseButton.Left && map.BeatmapInfo.OnlineID > 0)
+ addForBeatmap(map.BeatmapInfo.OnlineID);
else
{
- var existing = CurrentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == map.BeatmapInfo.OnlineBeatmapID);
+ var existing = CurrentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == map.BeatmapInfo.OnlineID);
if (existing != null)
{
diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs
index 7a43fee013..f3927bb852 100644
--- a/osu.Game.Tournament/TournamentGame.cs
+++ b/osu.Game.Tournament/TournamentGame.cs
@@ -12,6 +12,7 @@ using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Handlers.Mouse;
+using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Graphics;
using osu.Game.Graphics.Cursor;
@@ -60,72 +61,89 @@ namespace osu.Game.Tournament
loadingSpinner.Show();
- BracketLoadTask.ContinueWith(_ => LoadComponentsAsync(new[]
+ BracketLoadTask.ContinueWith(t =>
{
- new Container
+ if (t.IsFaulted)
{
- CornerRadius = 10,
- Depth = float.MinValue,
- Position = new Vector2(5),
- Masking = true,
- AutoSizeAxes = Axes.Both,
- Anchor = Anchor.BottomRight,
- Origin = Anchor.BottomRight,
- Children = new Drawable[]
+ Schedule(() =>
{
- new Box
- {
- Colour = OsuColour.Gray(0.2f),
- RelativeSizeAxes = Axes.Both,
- },
- new TourneyButton
- {
- Text = "Save Changes",
- Width = 140,
- Height = 50,
- Padding = new MarginPadding
- {
- Top = 10,
- Left = 10,
- },
- Margin = new MarginPadding
- {
- Right = 10,
- Bottom = 10,
- },
- Action = SaveChanges,
- },
- }
- },
- heightWarning = new WarningBox("Please make the window wider")
- {
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.BottomCentre,
- Margin = new MarginPadding(20),
- },
- new OsuContextMenuContainer
- {
- RelativeSizeAxes = Axes.Both,
- Child = new TournamentSceneManager()
+ loadingSpinner.Hide();
+ loadingSpinner.Expire();
+
+ Logger.Error(t.Exception, "Couldn't load bracket with error");
+ Add(new WarningBox("Your bracket.json file could not be parsed. Please check runtime.log for more details."));
+ });
+
+ return;
}
- }, drawables =>
- {
- loadingSpinner.Hide();
- loadingSpinner.Expire();
- AddRange(drawables);
-
- windowSize.BindValueChanged(size => ScheduleAfterChildren(() =>
+ LoadComponentsAsync(new[]
{
- var minWidth = (int)(size.NewValue.Height / 768f * TournamentSceneManager.REQUIRED_WIDTH) - 1;
- heightWarning.Alpha = size.NewValue.Width < minWidth ? 1 : 0;
- }), true);
-
- windowMode.BindValueChanged(mode => ScheduleAfterChildren(() =>
+ new Container
+ {
+ CornerRadius = 10,
+ Depth = float.MinValue,
+ Position = new Vector2(5),
+ Masking = true,
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = OsuColour.Gray(0.2f),
+ RelativeSizeAxes = Axes.Both,
+ },
+ new TourneyButton
+ {
+ Text = "Save Changes",
+ Width = 140,
+ Height = 50,
+ Padding = new MarginPadding
+ {
+ Top = 10,
+ Left = 10,
+ },
+ Margin = new MarginPadding
+ {
+ Right = 10,
+ Bottom = 10,
+ },
+ Action = SaveChanges,
+ },
+ }
+ },
+ heightWarning = new WarningBox("Please make the window wider")
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ Margin = new MarginPadding(20),
+ },
+ new OsuContextMenuContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = new TournamentSceneManager()
+ }
+ }, drawables =>
{
- windowMode.Value = WindowMode.Windowed;
- }), true);
- }));
+ loadingSpinner.Hide();
+ loadingSpinner.Expire();
+
+ AddRange(drawables);
+
+ windowSize.BindValueChanged(size => ScheduleAfterChildren(() =>
+ {
+ var minWidth = (int)(size.NewValue.Height / 768f * TournamentSceneManager.REQUIRED_WIDTH) - 1;
+ heightWarning.Alpha = size.NewValue.Width < minWidth ? 1 : 0;
+ }), true);
+
+ windowMode.BindValueChanged(mode => ScheduleAfterChildren(() =>
+ {
+ windowMode.Value = WindowMode.Windowed;
+ }), true);
+ });
+ });
}
}
}
diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs
index bdf7269c83..a6b0fa5cfc 100644
--- a/osu.Game.Tournament/TournamentGameBase.cs
+++ b/osu.Game.Tournament/TournamentGameBase.cs
@@ -58,75 +58,83 @@ namespace osu.Game.Tournament
private void readBracket()
{
- if (storage.Exists(bracket_filename))
+ try
{
- using (Stream stream = storage.GetStream(bracket_filename, FileAccess.Read, FileMode.Open))
- using (var sr = new StreamReader(stream))
- ladder = JsonConvert.DeserializeObject(sr.ReadToEnd(), new JsonPointConverter());
- }
-
- ladder ??= new LadderInfo();
-
- ladder.Ruleset.Value = RulesetStore.GetRuleset(ladder.Ruleset.Value?.ShortName)
- ?? RulesetStore.AvailableRulesets.First();
-
- bool addedInfo = false;
-
- // assign teams
- foreach (var match in ladder.Matches)
- {
- match.Team1.Value = ladder.Teams.FirstOrDefault(t => t.Acronym.Value == match.Team1Acronym);
- match.Team2.Value = ladder.Teams.FirstOrDefault(t => t.Acronym.Value == match.Team2Acronym);
-
- foreach (var conditional in match.ConditionalMatches)
+ if (storage.Exists(bracket_filename))
{
- conditional.Team1.Value = ladder.Teams.FirstOrDefault(t => t.Acronym.Value == conditional.Team1Acronym);
- conditional.Team2.Value = ladder.Teams.FirstOrDefault(t => t.Acronym.Value == conditional.Team2Acronym);
- conditional.Round.Value = match.Round.Value;
+ using (Stream stream = storage.GetStream(bracket_filename, FileAccess.Read, FileMode.Open))
+ using (var sr = new StreamReader(stream))
+ ladder = JsonConvert.DeserializeObject(sr.ReadToEnd(), new JsonPointConverter());
}
- }
- // assign progressions
- foreach (var pair in ladder.Progressions)
- {
- var src = ladder.Matches.FirstOrDefault(p => p.ID == pair.SourceID);
- var dest = ladder.Matches.FirstOrDefault(p => p.ID == pair.TargetID);
+ ladder ??= new LadderInfo();
- if (src == null)
- continue;
+ ladder.Ruleset.Value = RulesetStore.GetRuleset(ladder.Ruleset.Value?.ShortName)
+ ?? RulesetStore.AvailableRulesets.First();
- if (dest != null)
+ bool addedInfo = false;
+
+ // assign teams
+ foreach (var match in ladder.Matches)
{
- if (pair.Losers)
- src.LosersProgression.Value = dest;
- else
- src.Progression.Value = dest;
- }
- }
+ match.Team1.Value = ladder.Teams.FirstOrDefault(t => t.Acronym.Value == match.Team1Acronym);
+ match.Team2.Value = ladder.Teams.FirstOrDefault(t => t.Acronym.Value == match.Team2Acronym);
- // link matches to rounds
- foreach (var round in ladder.Rounds)
- {
- foreach (var id in round.Matches)
- {
- var found = ladder.Matches.FirstOrDefault(p => p.ID == id);
-
- if (found != null)
+ foreach (var conditional in match.ConditionalMatches)
{
- found.Round.Value = round;
- if (round.StartDate.Value > found.Date.Value)
- found.Date.Value = round.StartDate.Value;
+ conditional.Team1.Value = ladder.Teams.FirstOrDefault(t => t.Acronym.Value == conditional.Team1Acronym);
+ conditional.Team2.Value = ladder.Teams.FirstOrDefault(t => t.Acronym.Value == conditional.Team2Acronym);
+ conditional.Round.Value = match.Round.Value;
}
}
+
+ // assign progressions
+ foreach (var pair in ladder.Progressions)
+ {
+ var src = ladder.Matches.FirstOrDefault(p => p.ID == pair.SourceID);
+ var dest = ladder.Matches.FirstOrDefault(p => p.ID == pair.TargetID);
+
+ if (src == null)
+ continue;
+
+ if (dest != null)
+ {
+ if (pair.Losers)
+ src.LosersProgression.Value = dest;
+ else
+ src.Progression.Value = dest;
+ }
+ }
+
+ // link matches to rounds
+ foreach (var round in ladder.Rounds)
+ {
+ foreach (var id in round.Matches)
+ {
+ var found = ladder.Matches.FirstOrDefault(p => p.ID == id);
+
+ if (found != null)
+ {
+ found.Round.Value = round;
+ if (round.StartDate.Value > found.Date.Value)
+ found.Date.Value = round.StartDate.Value;
+ }
+ }
+ }
+
+ addedInfo |= addPlayers();
+ addedInfo |= addBeatmaps();
+
+ if (addedInfo)
+ SaveChanges();
+
+ ladder.CurrentMatch.Value = ladder.Matches.FirstOrDefault(p => p.Current.Value);
+ }
+ catch (Exception e)
+ {
+ taskCompletionSource.SetException(e);
+ return;
}
-
- addedInfo |= addPlayers();
- addedInfo |= addBeatmaps();
-
- if (addedInfo)
- SaveChanges();
-
- ladder.CurrentMatch.Value = ladder.Matches.FirstOrDefault(p => p.Current.Value);
Schedule(() =>
{
diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs
index ced1a8ec72..914d1163ad 100644
--- a/osu.Game.Tournament/TournamentSceneManager.cs
+++ b/osu.Game.Tournament/TournamentSceneManager.cs
@@ -237,7 +237,7 @@ namespace osu.Game.Tournament
{
Type = type;
BackgroundColour = OsuColour.Gray(0.2f);
- Action = () => RequestSelection(type);
+ Action = () => RequestSelection?.Invoke(type);
RelativeSizeAxes = Axes.X;
}
diff --git a/osu.Game/Audio/Effects/AudioFilter.cs b/osu.Game/Audio/Effects/AudioFilter.cs
index ee48bdd7d9..d2a39e9db7 100644
--- a/osu.Game/Audio/Effects/AudioFilter.cs
+++ b/osu.Game/Audio/Effects/AudioFilter.cs
@@ -4,7 +4,6 @@
using System.Diagnostics;
using ManagedBass.Fx;
using osu.Framework.Audio.Mixing;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
namespace osu.Game.Audio.Effects
@@ -21,10 +20,25 @@ namespace osu.Game.Audio.Effects
private readonly BQFParameters filter;
private readonly BQFType type;
+ private bool isAttached;
+
+ private int cutoff;
+
///
- /// The current cutoff of this filter.
+ /// The cutoff frequency of this filter.
///
- public BindableNumber Cutoff { get; }
+ public int Cutoff
+ {
+ get => cutoff;
+ set
+ {
+ if (value == cutoff)
+ return;
+
+ cutoff = value;
+ updateFilter(cutoff);
+ }
+ }
///
/// A Component that implements a BASS FX BiQuad Filter Effect.
@@ -36,102 +50,96 @@ namespace osu.Game.Audio.Effects
this.mixer = mixer;
this.type = type;
- int initialCutoff;
-
- switch (type)
- {
- case BQFType.HighPass:
- initialCutoff = 1;
- break;
-
- case BQFType.LowPass:
- initialCutoff = MAX_LOWPASS_CUTOFF;
- break;
-
- default:
- initialCutoff = 500; // A default that should ensure audio remains audible for other filters.
- break;
- }
-
- Cutoff = new BindableNumber(initialCutoff)
- {
- MinValue = 1,
- MaxValue = MAX_LOWPASS_CUTOFF
- };
-
filter = new BQFParameters
{
lFilter = type,
- fCenter = initialCutoff,
fBandwidth = 0,
- fQ = 0.7f // This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0)
+ // This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0)
+ fQ = 0.7f
};
- // Don't start attached if this is low-pass or high-pass filter (as they have special auto-attach/detach logic)
- if (type != BQFType.LowPass && type != BQFType.HighPass)
- attachFilter();
-
- Cutoff.ValueChanged += updateFilter;
+ Cutoff = getInitialCutoff(type);
}
- private void attachFilter()
+ private int getInitialCutoff(BQFType type)
{
- Debug.Assert(!mixer.Effects.Contains(filter));
- mixer.Effects.Add(filter);
- }
-
- private void detachFilter()
- {
- Debug.Assert(mixer.Effects.Contains(filter));
- mixer.Effects.Remove(filter);
- }
-
- private void updateFilter(ValueChangedEvent cutoff)
- {
- // Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz.
- if (type == BQFType.LowPass)
+ switch (type)
{
- if (cutoff.NewValue >= MAX_LOWPASS_CUTOFF)
- {
- detachFilter();
- return;
- }
+ case BQFType.HighPass:
+ return 1;
- if (cutoff.OldValue >= MAX_LOWPASS_CUTOFF && cutoff.NewValue < MAX_LOWPASS_CUTOFF)
- attachFilter();
+ case BQFType.LowPass:
+ return MAX_LOWPASS_CUTOFF;
+
+ default:
+ return 500; // A default that should ensure audio remains audible for other filters.
+ }
+ }
+
+ private void updateFilter(int newValue)
+ {
+ switch (type)
+ {
+ case BQFType.LowPass:
+ // Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz.
+ if (newValue >= MAX_LOWPASS_CUTOFF)
+ {
+ ensureDetached();
+ return;
+ }
+
+ break;
+
+ // Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz.
+ case BQFType.HighPass:
+ if (newValue <= 1)
+ {
+ ensureDetached();
+ return;
+ }
+
+ break;
}
- // Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz.
- if (type == BQFType.HighPass)
- {
- if (cutoff.NewValue <= 1)
- {
- detachFilter();
- return;
- }
-
- if (cutoff.OldValue <= 1 && cutoff.NewValue > 1)
- attachFilter();
- }
+ ensureAttached();
var filterIndex = mixer.Effects.IndexOf(filter);
+
if (filterIndex < 0) return;
if (mixer.Effects[filterIndex] is BQFParameters existingFilter)
{
- existingFilter.fCenter = cutoff.NewValue;
+ existingFilter.fCenter = newValue;
// required to update effect with new parameters.
mixer.Effects[filterIndex] = existingFilter;
}
}
+ private void ensureAttached()
+ {
+ if (isAttached)
+ return;
+
+ Debug.Assert(!mixer.Effects.Contains(filter));
+ mixer.Effects.Add(filter);
+ isAttached = true;
+ }
+
+ private void ensureDetached()
+ {
+ if (!isAttached)
+ return;
+
+ Debug.Assert(mixer.Effects.Contains(filter));
+ mixer.Effects.Remove(filter);
+ isAttached = false;
+ }
+
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
-
- if (mixer.Effects.Contains(filter))
- detachFilter();
+ ensureDetached();
}
}
}
diff --git a/osu.Game/Audio/Effects/ITransformableFilter.cs b/osu.Game/Audio/Effects/ITransformableFilter.cs
index e4de4cf8ff..fb6a924f68 100644
--- a/osu.Game/Audio/Effects/ITransformableFilter.cs
+++ b/osu.Game/Audio/Effects/ITransformableFilter.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
@@ -12,7 +11,7 @@ namespace osu.Game.Audio.Effects
///
/// The filter cutoff.
///
- BindableNumber Cutoff { get; }
+ int Cutoff { get; set; }
}
public static class FilterableAudioComponentExtensions
@@ -40,7 +39,7 @@ namespace osu.Game.Audio.Effects
public static TransformSequence CutoffTo(this T component, int newCutoff, double duration, TEasing easing)
where T : class, ITransformableFilter, IDrawable
where TEasing : IEasingFunction
- => component.TransformBindableTo(component.Cutoff, newCutoff, duration, easing);
+ => component.TransformTo(nameof(component.Cutoff), newCutoff, duration, easing);
///
/// Smoothly adjusts filter cutoff over time.
@@ -49,6 +48,6 @@ namespace osu.Game.Audio.Effects
public static TransformSequence CutoffTo(this TransformSequence sequence, int newCutoff, double duration, TEasing easing)
where T : class, ITransformableFilter, IDrawable
where TEasing : IEasingFunction
- => sequence.Append(o => o.TransformBindableTo(o.Cutoff, newCutoff, duration, easing));
+ => sequence.Append(o => o.TransformTo(nameof(o.Cutoff), newCutoff, duration, easing));
}
}
diff --git a/osu.Game/Audio/PreviewTrackManager.cs b/osu.Game/Audio/PreviewTrackManager.cs
index 1de9e1561f..ca63add31d 100644
--- a/osu.Game/Audio/PreviewTrackManager.cs
+++ b/osu.Game/Audio/PreviewTrackManager.cs
@@ -43,11 +43,11 @@ namespace osu.Game.Audio
}
///
- /// Retrieves a for a .
+ /// Retrieves a for a .
///
- /// The to retrieve the preview track for.
+ /// The to retrieve the preview track for.
/// The playable .
- public PreviewTrack Get(BeatmapSetInfo beatmapSetInfo)
+ public PreviewTrack Get(IBeatmapSetInfo beatmapSetInfo)
{
var track = CreatePreviewTrack(beatmapSetInfo, trackStore);
@@ -91,7 +91,7 @@ namespace osu.Game.Audio
///
/// Creates the .
///
- protected virtual TrackManagerPreviewTrack CreatePreviewTrack(BeatmapSetInfo beatmapSetInfo, ITrackStore trackStore) =>
+ protected virtual TrackManagerPreviewTrack CreatePreviewTrack(IBeatmapSetInfo beatmapSetInfo, ITrackStore trackStore) =>
new TrackManagerPreviewTrack(beatmapSetInfo, trackStore);
public class TrackManagerPreviewTrack : PreviewTrack
@@ -99,10 +99,10 @@ namespace osu.Game.Audio
[Resolved(canBeNull: true)]
public IPreviewTrackOwner Owner { get; private set; }
- private readonly BeatmapSetInfo beatmapSetInfo;
+ private readonly IBeatmapSetInfo beatmapSetInfo;
private readonly ITrackStore trackManager;
- public TrackManagerPreviewTrack(BeatmapSetInfo beatmapSetInfo, ITrackStore trackManager)
+ public TrackManagerPreviewTrack(IBeatmapSetInfo beatmapSetInfo, ITrackStore trackManager)
{
this.beatmapSetInfo = beatmapSetInfo;
this.trackManager = trackManager;
@@ -114,7 +114,7 @@ namespace osu.Game.Audio
Logger.Log($"A {nameof(PreviewTrack)} was created without a containing {nameof(IPreviewTrackOwner)}. An owner should be added for correct behaviour.");
}
- protected override Track GetTrack() => trackManager.Get($"https://b.ppy.sh/preview/{beatmapSetInfo?.OnlineBeatmapSetID}.mp3");
+ protected override Track GetTrack() => trackManager.Get($"https://b.ppy.sh/preview/{beatmapSetInfo.OnlineID}.mp3");
}
private class PreviewTrackStore : AudioCollectionManager, ITrackStore
diff --git a/osu.Game/Beatmaps/BeatmapMetrics.cs b/osu.Game/Beatmaps/APIFailTimes.cs
similarity index 96%
rename from osu.Game/Beatmaps/BeatmapMetrics.cs
rename to osu.Game/Beatmaps/APIFailTimes.cs
index b164aa6b30..7218906b38 100644
--- a/osu.Game/Beatmaps/BeatmapMetrics.cs
+++ b/osu.Game/Beatmaps/APIFailTimes.cs
@@ -9,7 +9,7 @@ namespace osu.Game.Beatmaps
///
/// Beatmap metrics based on accumulated online data from community plays.
///
- public class BeatmapMetrics
+ public class APIFailTimes
{
///
/// Points of failure on a relative time scale (usually 0..100).
diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs
index f3434c5153..627e54c803 100644
--- a/osu.Game/Beatmaps/BeatmapConverter.cs
+++ b/osu.Game/Beatmaps/BeatmapConverter.cs
@@ -40,7 +40,13 @@ namespace osu.Game.Beatmaps
public IBeatmap Convert(CancellationToken cancellationToken = default)
{
// We always operate on a clone of the original beatmap, to not modify it game-wide
- return ConvertBeatmap(Beatmap.Clone(), cancellationToken);
+ var original = Beatmap.Clone();
+
+ // Shallow clone isn't enough to ensure we don't mutate beatmap info unexpectedly.
+ // Can potentially be removed after `Beatmap.Difficulty` doesn't save back to `Beatmap.BeatmapInfo`.
+ original.BeatmapInfo = original.BeatmapInfo.Clone();
+
+ return ConvertBeatmap(original, cancellationToken);
}
///
diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs
index ac5b5d7a8a..9069ea4404 100644
--- a/osu.Game/Beatmaps/BeatmapInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapInfo.cs
@@ -9,6 +9,7 @@ using System.Linq;
using Newtonsoft.Json;
using osu.Framework.Testing;
using osu.Game.Database;
+using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Scoring;
@@ -16,7 +17,7 @@ namespace osu.Game.Beatmaps
{
[ExcludeFromDynamicCompile]
[Serializable]
- public class BeatmapInfo : IEquatable, IHasPrimaryKey, IBeatmapInfo
+ public class BeatmapInfo : IEquatable, IHasPrimaryKey, IBeatmapInfo, IBeatmapOnlineInfo
{
public int ID { get; set; }
@@ -47,10 +48,7 @@ namespace osu.Game.Beatmaps
public BeatmapDifficulty BaseDifficulty { get; set; }
[NotMapped]
- public BeatmapMetrics Metrics { get; set; }
-
- [NotMapped]
- public BeatmapOnlineInfo OnlineInfo { get; set; }
+ public APIBeatmap OnlineInfo { get; set; }
[NotMapped]
public int? MaxCombo { get; set; }
@@ -178,19 +176,49 @@ namespace osu.Game.Beatmaps
#region Implementation of IHasOnlineID
- public int? OnlineID => OnlineBeatmapID;
+ public int OnlineID => OnlineBeatmapID ?? -1;
#endregion
#region Implementation of IBeatmapInfo
+ [JsonIgnore]
string IBeatmapInfo.DifficultyName => Version;
+
+ [JsonIgnore]
IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
+
+ [JsonIgnore]
IBeatmapDifficultyInfo IBeatmapInfo.Difficulty => BaseDifficulty;
+
+ [JsonIgnore]
IBeatmapSetInfo IBeatmapInfo.BeatmapSet => BeatmapSet;
+
+ [JsonIgnore]
IRulesetInfo IBeatmapInfo.Ruleset => Ruleset;
+
+ [JsonIgnore]
double IBeatmapInfo.StarRating => StarDifficulty;
#endregion
+
+ #region Implementation of IBeatmapOnlineInfo
+
+ [JsonIgnore]
+ public int CircleCount => OnlineInfo.CircleCount;
+
+ [JsonIgnore]
+ public int SliderCount => OnlineInfo.SliderCount;
+
+ [JsonIgnore]
+ public int PlayCount => OnlineInfo.PlayCount;
+
+ [JsonIgnore]
+ public int PassCount => OnlineInfo.PassCount;
+
+ [JsonIgnore]
+ public APIFailTimes FailTimes => OnlineInfo.FailTimes;
+
+ #endregion
}
}
diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs
index eba19ac1a1..836302c424 100644
--- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs
+++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs
@@ -16,12 +16,17 @@ namespace osu.Game.Beatmaps
///
/// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields.
///
- public static RomanisableString GetDisplayTitleRomanisable(this IBeatmapInfo beatmapInfo)
+ public static RomanisableString GetDisplayTitleRomanisable(this IBeatmapInfo beatmapInfo, bool includeDifficultyName = true)
{
var metadata = getClosestMetadata(beatmapInfo).GetDisplayTitleRomanisable();
- var versionString = getVersionString(beatmapInfo);
- return new RomanisableString($"{metadata.GetPreferred(true)} {versionString}".Trim(), $"{metadata.GetPreferred(false)} {versionString}".Trim());
+ if (includeDifficultyName)
+ {
+ var versionString = getVersionString(beatmapInfo);
+ return new RomanisableString($"{metadata.GetPreferred(true)} {versionString}".Trim(), $"{metadata.GetPreferred(false)} {versionString}".Trim());
+ }
+
+ return new RomanisableString($"{metadata.GetPreferred(true)}".Trim(), $"{metadata.GetPreferred(false)}".Trim());
}
public static string[] GetSearchableTerms(this IBeatmapInfo beatmapInfo) => new[]
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 240db22c00..0509a9db47 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps
/// Handles general operations related to global beatmap management.
///
[ExcludeFromDynamicCompile]
- public class BeatmapManager : IModelDownloader, IModelManager, IModelFileManager, ICanAcceptFiles, IWorkingBeatmapCache, IDisposable
+ public class BeatmapManager : IModelDownloader, IModelManager, IModelFileManager, IWorkingBeatmapCache, IDisposable
{
private readonly BeatmapModelManager beatmapModelManager;
private readonly BeatmapModelDownloader beatmapModelDownloader;
@@ -54,7 +54,7 @@ namespace osu.Game.Beatmaps
}
}
- protected virtual BeatmapModelDownloader CreateBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider api, GameHost host)
+ protected virtual BeatmapModelDownloader CreateBeatmapModelDownloader(IBeatmapModelManager modelManager, IAPIProvider api, GameHost host)
{
return new BeatmapModelDownloader(modelManager, api, host);
}
@@ -176,11 +176,6 @@ namespace osu.Game.Beatmaps
}
}
- ///
- /// Fired when the user requests to view the resulting import.
- ///
- public Action>> PresentImport { set => beatmapModelManager.PostImport = value; }
-
///