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

Merge branch 'master' into ui-scaling-keybind

This commit is contained in:
Dean Herbert 2021-10-27 01:06:14 +09:00 committed by GitHub
commit 70f432805e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
341 changed files with 9214 additions and 2427 deletions

1
.github/FUNDING.yml vendored
View File

@ -1 +1,2 @@
github: ppy
custom: https://osu.ppy.sh/home/support

View File

@ -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)

View File

@ -30,3 +30,5 @@ jobs:
name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}})
path: "*.trx"
reporter: dotnet-trx
list-suites: 'failed'
list-tests: 'failed'

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1004.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1004.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1026.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1026.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -0,0 +1,91 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.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;
}
}
}

View File

@ -0,0 +1,141 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using 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
{
/// <summary>
/// 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.
/// </summary>
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<Path> verticalPaths = new List<Path>();
private readonly List<Vector2[]> verticalLineVertices = new List<Vector2[]>();
[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<SnapResult> 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;
}
}

View File

@ -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<CatchHitObject>
{
private const float distance_snap_radius = 50;
private CatchDistanceSnapGrid distanceSnapGrid;
private readonly Bindable<TernaryState> distanceSnapToggle = new Bindable<TernaryState>();
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<CatchHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) =>
@ -42,14 +80,95 @@ namespace osu.Game.Rulesets.Catch.Edit
new BananaShowerCompositionTool()
};
protected override IEnumerable<TernaryButton> 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<CatchHitObject>().LastOrDefault(h => h.GetEndTime() < time && !(h is BananaShower));
switch (hitObject)
{
case Fruit fruit:
return fruit;
case JuiceStream juiceStream:
return juiceStream.NestedHitObjects.OfType<PalpableCatchHitObject>().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;
}
}
}

View File

@ -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;

View File

@ -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();
}

View File

@ -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", () =>

View File

@ -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 } });

View File

@ -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);

View File

@ -28,7 +28,12 @@ namespace osu.Game.Rulesets.Mania.Configuration
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
{
new TrackedSetting<double>(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)"
)
)
};
}

View File

@ -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:

View File

@ -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);

View File

@ -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;
}
}
}

View File

@ -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,
}
}
},

View File

@ -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<HitObject>
{
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)

View File

@ -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<SkinProvidingContainer> skinProvider = null)
private void loadContent() => loadContent(false);
private void loadContent(bool automated, Func<SkinProvidingContainer> skinProvider = null)
{
SetContents(_ =>
{

View File

@ -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();

View File

@ -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);

View File

@ -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 += _ =>

View File

@ -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<OsuReplayFrame>()
.Select(replayFrame =>
{
var adjustedTime = replayFrame.Time * rate;
return new OsuReplayFrame(adjustedTime, replayFrame.Position, replayFrame.Actions.ToArray());
})
.Cast<ReplayFrame>()
.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
},
}
};

View File

@ -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();

View File

@ -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 <v8 maps for the same time duration.
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / beatmap.ControlPointInfo.DifficultyPointAt(original.StartTime).SpeedMultiplier : 1
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1
}.Yield();
case IHasDuration endTimeData:

View File

@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
public double OverallDifficulty { get; set; }
public double DrainRate { get; set; }
public int HitCircleCount { get; set; }
public int SliderCount { get; set; }
public int SpinnerCount { get; set; }
}
}

View File

@ -64,6 +64,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
maxCombo += beatmap.HitObjects.OfType<Slider>().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
};

View File

@ -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;
}

View File

@ -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);

View File

@ -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);

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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<bool> 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);

View File

@ -0,0 +1,76 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using 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
{
/// <summary>
/// Slightly higher than the cutoff for <see cref="Drawable.IsPresent"/>.
/// </summary>
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<int> currentCombo;
private float targetAlpha;
[SettingSource(
"Hidden at combo",
"The combo count at which the cursor becomes completely hidden",
SettingControlType = typeof(SettingsSlider<int, HiddenComboSlider>)
)]
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<int>
{
public override LocalisableString TooltipText => Current.Value == 0 ? "always hidden" : base.TooltipText;
}
}

View File

@ -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;

View File

@ -192,6 +192,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModBarrelRoll(),
new OsuModApproachDifferent(),
new OsuModMuted(),
new OsuModNoScope(),
};
case ModType.System:

View File

@ -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;

View File

@ -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;

View File

@ -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));
}
}
}
}

View File

@ -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();

View File

@ -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());
}

View File

@ -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);
}
}
}

View File

@ -0,0 +1,820 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.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<RealmBeatmapSet>? imported;
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
imported = await importer.Import(reader);
Assert.AreEqual(1, realmFactory.Context.All<RealmBeatmapSet>().Count());
Assert.NotNull(imported);
Debug.Assert(imported != null);
imported.PerformWrite(s => s.DeletePending = true);
Assert.AreEqual(1, realmFactory.Context.All<RealmBeatmapSet>().Count(s => s.DeletePending));
}
});
Logger.Log("Running with no work to purge pending deletions");
RunTestWithRealm((realmFactory, _) => { Assert.AreEqual(0, realmFactory.Context.All<RealmBeatmapSet>().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<RealmBeatmapSet>? 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<RealmBeatmapSet>().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<RealmRuleset>().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<RealmBeatmapSet>().First();
var beatmapToUpdate = setToUpdate.Beatmaps.First();
realmFactory.Context.Write(() => beatmapToUpdate.DifficultyName = "updated");
RealmBeatmap updatedInfo = realmFactory.Context.All<RealmBeatmap>().First(b => b.ID == beatmapToUpdate.ID);
Assert.That(updatedInfo.DifficultyName, Is.EqualTo("updated"));
});
}
public static async Task<RealmBeatmapSet?> 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<RealmBeatmapSet>().FirstOrDefault(beatmapSet => beatmapSet.ID == importedSet!.ID);
}
public static async Task<RealmBeatmapSet> 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<RealmBeatmapSet>().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<RealmBeatmapSet>().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<RealmBeatmapSet>().Count()
: realm.All<RealmBeatmapSet>().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<RealmBeatmap>().Where(_ => true).ToList().Count);
}
private static void checkSingleReferencedFileCount(Realm realm, int expected)
{
int singleReferencedCount = 0;
foreach (var f in realm.All<RealmFile>())
{
if (f.BacklinksCount == 1)
singleReferencedCount++;
}
Assert.AreEqual(expected, singleReferencedCount);
}
private static void ensureLoaded(Realm realm, int timeout = 60000)
{
IQueryable<RealmBeatmapSet>? resultSets = null;
waitForOrAssert(() => (resultSets = realm.All<RealmBeatmapSet>().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<RealmBeatmapSet> queryBeatmapSets() => realm.All<RealmBeatmapSet>().Where(s => !s.DeletePending && s.OnlineID == 241526);
var set = queryBeatmapSets().First();
// ReSharper disable once PossibleUnintendedReferenceComparison
IEnumerable<RealmBeatmap> queryBeatmaps() => realm.All<RealmBeatmap>().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<bool> result, string failureMessage, int timeout = 60000)
{
const int sleep = 200;
while (timeout > 0)
{
Thread.Sleep(sleep);
timeout -= sleep;
if (result())
return;
}
Assert.Fail(failureMessage);
}
}
}

View File

@ -0,0 +1,114 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.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<RealmFile>().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<RealmFile>().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<RealmFile>().Any());
Assert.True(files.Storage.Exists(path));
files.Cleanup();
Logger.Log($"Cleanup complete at {timer.ElapsedMilliseconds}");
Assert.True(realm.All<RealmFile>().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<RealmFile>().Any());
Assert.True(files.Storage.Exists(path));
files.Cleanup();
Assert.False(realm.All<RealmFile>().Any());
Assert.False(file.IsValid);
Assert.False(files.Storage.Exists(path));
});
}
}
}

View File

@ -1,3 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Threading;
using System.Threading.Tasks;

View File

@ -0,0 +1,213 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.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<RealmBeatmap> beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive();
ILive<IBeatmapInfo> iBeatmap = beatmap;
Assert.AreEqual(0, iBeatmap.Value.Length);
});
}
[Test]
public void TestValueAccessWithOpenContext()
{
RunTestWithRealm((realmFactory, _) =>
{
RealmLive<RealmBeatmap>? 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<RealmBeatmap>? 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<RealmBeatmap>? 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<RealmBeatmap>? 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<InvalidOperationException>(() =>
{
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<RealmBeatmap>().SubscribeForNotifications(gotChange);
RealmLive<RealmBeatmap>? 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<RealmBeatmap>().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<RealmBeatmap>().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<RealmBeatmap> sender, ChangeSet changes, Exception error)
{
changesTriggered++;
}
});
}
}
}

View File

@ -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<RealmContextFactory, Storage> 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<RealmContextFactory, Storage, Task> 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<Task> 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)

View File

@ -0,0 +1,54 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.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<RealmRuleset>().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<RealmRuleset>().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);
});
}
}
}

View File

@ -0,0 +1,104 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.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<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
BeatmapSet = new BeatmapSetInfo
{
Files = new List<BeatmapSetFileInfo>(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<TestWorkingBeatmap>(beatmap, null, null);
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream);
mockWorkingBeatmap.As<IWorkingBeatmap>().SetupGet(w => w.Storyboard).Returns(storyboard);
return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
}
}
}

View File

@ -0,0 +1,128 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.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<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
BeatmapSet = new BeatmapSetInfo
{
Files = new List<BeatmapSetFileInfo>(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<TestWorkingBeatmap>(beatmap, null, null);
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream);
return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
}
}
}

View File

@ -0,0 +1,86 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.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<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
BeatmapSet = new BeatmapSetInfo
{
Files = new List<BeatmapSetFileInfo>(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<Stream>();
mockStream.Setup(s => s.Length).Returns(byteLength);
var mockWorkingBeatmap = new Mock<IWorkingBeatmap>();
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(mockStream.Object);
return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
}
private BeatmapVerifierContext getContextMissing()
{
var mockWorkingBeatmap = new Mock<IWorkingBeatmap>();
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns((Stream)null);
return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
}
}
}

View File

@ -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
{

View File

@ -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");

View File

@ -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 });

View File

@ -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)
{
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Binary file not shown.

View File

@ -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:

View File

@ -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);
}
}

View File

@ -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<int> lowPassSlider;
private OsuSliderBar<int> 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<int>
lowPassSlider = new OsuSliderBar<int>
{
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<int>
highPassSlider = new OsuSliderBar<int>
{
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();
}
}
}

View File

@ -0,0 +1,53 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using 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<BeatmapSetOnlineStatus>().Select(status => new BeatmapSetOnlineStatusPill
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Status = status
})
};
private IEnumerable<BeatmapSetOnlineStatusPill> statusPills => this.ChildrenOfType<BeatmapSetOnlineStatusPill>();
[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));
}
}
}

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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<MetadataSection>().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<HitObjectComposer>().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");
}
}
}

View File

@ -20,14 +20,15 @@ namespace osu.Game.Tests.Visual.Gameplay
/// </summary>
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<double>(OsuSetting.DimLevel).Value = 1.0;
Dependencies.Cache(Config = new OsuConfigManager(LocalStorage));
Config.GetBindable<double>(OsuSetting.DimLevel).Value = 1.0;
}
[Test]

View File

@ -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);

View File

@ -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;

View File

@ -291,7 +291,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddAssert($"epilepsy warning {(warning ? "present" : "absent")}", () => this.ChildrenOfType<EpilepsyWarning>().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<EpilepsyWarning>().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<EpilepsyWarning>().SingleOrDefault();
private class TestPlayerLoader : PlayerLoader
{
public new VisualSettings VisualSettings => base.VisualSettings;

View File

@ -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;
}
}
}
}

View File

@ -93,9 +93,9 @@ namespace osu.Game.Tests.Visual.Gameplay
private IList<MultiplierControlPoint> testControlPoints => new List<MultiplierControlPoint>
{
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]

View File

@ -0,0 +1,51 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using 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));
}
}
}

View File

@ -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);

View File

@ -0,0 +1,167 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.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<LoungeSubScreen>();
mockLounge
.Setup(l => l.Join(It.IsAny<Room>(), It.IsAny<string>(), It.IsAny<Action<Room>>(), It.IsAny<Action<string>>()))
.Callback<Room, string, Action<Room>, Action<string>>((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<DrawableLoungeRoom.PasswordEntryPopover>().SingleOrDefault()) != null);
AddAssert("textbox has focus", () => checkFocus(popover.ChildrenOfType<OsuPasswordTextBox>().Single()));
AddStep("enter password", () => popover.ChildrenOfType<OsuPasswordTextBox>().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<OsuPasswordTextBox>().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<DrawableLoungeRoom.PasswordEntryPopover>().SingleOrDefault()) != null);
AddAssert("textbox has focus", () => checkFocus(popover.ChildrenOfType<OsuPasswordTextBox>().Single()));
AddStep("enter password", () => popover.ChildrenOfType<OsuPasswordTextBox>().Single().Text = "password");
AddStep("commit via click button", () =>
{
var button = popover.ChildrenOfType<OsuButton>().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<OsuPasswordTextBox>().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;
}
}

View File

@ -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()
{

View File

@ -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 }
}
};
});

View File

@ -564,11 +564,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
AddRepeatStep("click spectate button", () =>
AddUntilStep("wait for ready button to be enabled", () => readyButton.ChildrenOfType<OsuButton>().Single().Enabled.Value);
AddStep("click ready button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerReadyButton>().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<OsuButton>().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<MultiplayerReadyButton>().Single();
private void createRoom(Func<Room> room)
{
AddUntilStep("wait for lounge", () => multiplayerScreen.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true);

View File

@ -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<string, UserStatistics>
{
{
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()

View File

@ -0,0 +1,31 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.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<ProgressCompletionNotification>().Count() == 1);
}
}
}

View File

@ -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
{

View File

@ -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<BeatmapListingOverlay.NotFoundDrawable>().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<BeatmapListingOverlay.NotFoundDrawable>().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<BeatmapListingOverlay.NotFoundDrawable>().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<BeatmapListingOverlay.SupporterRequiredDrawable>().Any(d => d.IsPresent)
&& !overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().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;
}
}
}

View File

@ -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<BeatmapInfo>
{
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<BeatmapInfo>
{
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,
};
}

View File

@ -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<BeatmapInfo>
{
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
}
};

View File

@ -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<BarGraph>().All(graph => graph.MaxValue == 100));
AddAssert("graph max values correct", () => successRate.ChildrenOfType<BarGraph>().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<BarGraph>().All(graph => graph.MaxValue == 0));
AddAssert("graph max values correct", () => successRate.ChildrenOfType<BarGraph>().All(graph => graph.MaxValue == 0));
}
private class GraphExposingSuccessRate : SuccessRate

View File

@ -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;

View File

@ -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
{

View File

@ -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,

View File

@ -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> user = new Bindable<User>();
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<string>() },
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<string>() },
null
};
}
}

View File

@ -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<ScorePanel>().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<ScorePanel>().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<ScorePanel>().Count() > 1);
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().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<ScorePanel>().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<ScorePanelList>().FirstOrDefault()?.AllPanelsVisible == true);
waitForDisplay();
}
private void waitForDisplay()
{
AddUntilStep("wait for request to complete", () => requestComplete);
AddUntilStep("wait for panels to be visible", () => resultsScreen.ChildrenOfType<ScorePanelList>().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);

View File

@ -0,0 +1,63 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using 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<float> current = new Bindable<float>
{
Default = default,
Value = 1,
};
[Test]
public void TestBasic()
{
RestoreDefaultValueButton<float> 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<float>
{
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);
}
}
}

View File

@ -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<RestoreDefaultValueButton<string>>().Single();
});
AddUntilStep("wait for loaded", () => textBox.IsLoaded);
AddStep("retrieve restore default button", () => restoreDefaultValueButton = textBox.ChildrenOfType<RestoreDefaultValueButton<string>>().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<string> restoreDefaultValueButton = null;
OsuTextBox control = null;
AddStep("create settings item", () =>
{
Child = textBox = new SettingsTextBox
{
Current = new Bindable<string>
{
Default = "test",
Value = "test"
}
};
});
AddUntilStep("wait for loaded", () => textBox.IsLoaded);
AddStep("retrieve components", () =>
{
restoreDefaultValueButton = textBox.ChildrenOfType<RestoreDefaultValueButton<string>>().Single();
control = textBox.ChildrenOfType<OsuTextBox>().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<OsuSpriteText>().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));
}
/// <summary>
/// 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<RestoreDefaultValueButton<float>>().Single();
});
AddUntilStep("wait for loaded", () => sliderBar.IsLoaded);
AddStep("retrieve restore default button", () => restoreDefaultValueButton = sliderBar.ChildrenOfType<RestoreDefaultValueButton<float>>().Single());
AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);

View File

@ -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()
{

View File

@ -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(),
},
}
});
}

View File

@ -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<FillFlowContainer>().OfType<IHasText>().First().Text == collectionName)));
() => shouldContain == (getCollectionDropdownItems().Any(i => i.ChildrenOfType<CompositeDrawable>().OfType<IHasText>().First().Text == collectionName)));
private IconButton getAddOrRemoveButton(int index)
=> getCollectionDropdownItems().ElementAt(index).ChildrenOfType<IconButton>().Single();

View File

@ -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<DrawableCarouselBeatmap>().Any());
AddStep("select next and enter", () =>
{
InputManager.MoveMouseTo(songSelect.Carousel.ChildrenOfType<DrawableCarouselBeatmap>()
@ -599,10 +601,10 @@ namespace osu.Game.Tests.Visual.SongSelect
});
FilterableDifficultyIcon difficultyIcon = null;
AddStep("Find an icon", () =>
AddUntilStep("Find an icon", () =>
{
difficultyIcon = set.ChildrenOfType<FilterableDifficultyIcon>()
.First(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex());
return (difficultyIcon = set.ChildrenOfType<FilterableDifficultyIcon>()
.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<FilterableGroupedDifficultyIcon>()
.First(icon => icon.Items.First().BeatmapInfo.Ruleset.ID == 3);
return (groupIcon = set.ChildrenOfType<FilterableGroupedDifficultyIcon>()
.FirstOrDefault(icon => icon.Items.First().BeatmapInfo.Ruleset.ID == 3)) != null;
});
AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0);

View File

@ -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
{

View File

@ -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
{

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