1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-13 08:32:57 +08:00

Merge branch 'master' into beatmap-cancellation-token

This commit is contained in:
Dean Herbert 2021-11-16 14:43:13 +09:00
commit 53c0682a08
206 changed files with 4647 additions and 1128 deletions

View File

@ -5,7 +5,7 @@ updates:
schedule:
interval: monthly
time: "17:00"
open-pull-requests-limit: 99
open-pull-requests-limit: 0 # disabled until https://github.com/dependabot/dependabot-core/issues/369 is resolved.
ignore:
- dependency-name: Microsoft.EntityFrameworkCore.Design
versions:

View File

@ -52,22 +52,29 @@ jobs:
build-only-android:
name: Build only (Android)
runs-on: windows-latest
runs-on: macos-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v2
# Pin Xamarin.Android version to 11.2 for now to avoid build failures caused by a Xamarin-side regression.
# See: https://github.com/xamarin/xamarin-android/issues/6284
# This can be removed/reverted when the fix makes it to upstream and is deployed on github runners.
- name: Set default Xamarin SDK version
run: |
$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=11.2
- 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
# 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.Android.slnf /restore /p:Configuration=Debug
run: msbuild osu.Android/osu.Android.csproj /restore /p:Configuration=Debug
build-only-ios:
# While this workflow technically *can* run, it fails as iOS builds are blocked by multiple issues.

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1026.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1106.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1112.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1108.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,118 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Edit.Checks;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Catch.Tests.Editor.Checks
{
[TestFixture]
public class TestCheckBananaShowerGap
{
private CheckBananaShowerGap check;
[SetUp]
public void Setup()
{
check = new CheckBananaShowerGap();
}
[Test]
public void TestAllowedSpinnerGaps()
{
assertOk(mockBeatmap(250, 1000, 1250), DifficultyRating.Easy);
assertOk(mockBeatmap(250, 1000, 1250), DifficultyRating.Normal);
assertOk(mockBeatmap(125, 1000, 1250), DifficultyRating.Hard);
assertOk(mockBeatmap(125, 1000, 1125), DifficultyRating.Insane);
assertOk(mockBeatmap(62, 1000, 1125), DifficultyRating.Expert);
assertOk(mockBeatmap(62, 1000, 1125), DifficultyRating.ExpertPlus);
}
[Test]
public void TestDisallowedSpinnerGapStart()
{
assertTooShortSpinnerStart(mockBeatmap(249, 1000, 1250), DifficultyRating.Easy);
assertTooShortSpinnerStart(mockBeatmap(249, 1000, 1250), DifficultyRating.Normal);
assertTooShortSpinnerStart(mockBeatmap(124, 1000, 1250), DifficultyRating.Hard);
assertTooShortSpinnerStart(mockBeatmap(124, 1000, 1250), DifficultyRating.Insane);
assertTooShortSpinnerStart(mockBeatmap(61, 1000, 1250), DifficultyRating.Expert);
assertTooShortSpinnerStart(mockBeatmap(61, 1000, 1250), DifficultyRating.ExpertPlus);
}
[Test]
public void TestDisallowedSpinnerGapEnd()
{
assertTooShortSpinnerEnd(mockBeatmap(250, 1000, 1249), DifficultyRating.Easy);
assertTooShortSpinnerEnd(mockBeatmap(250, 1000, 1249), DifficultyRating.Normal);
assertTooShortSpinnerEnd(mockBeatmap(125, 1000, 1249), DifficultyRating.Hard);
assertTooShortSpinnerEnd(mockBeatmap(125, 1000, 1124), DifficultyRating.Insane);
assertTooShortSpinnerEnd(mockBeatmap(62, 1000, 1124), DifficultyRating.Expert);
assertTooShortSpinnerEnd(mockBeatmap(62, 1000, 1124), DifficultyRating.ExpertPlus);
}
[Test]
public void TestConsecutiveSpinners()
{
var spinnerConsecutiveBeatmap = new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new BananaShower { StartTime = 0, EndTime = 100, X = 0 },
new BananaShower { StartTime = 101, EndTime = 200, X = 0 },
new BananaShower { StartTime = 201, EndTime = 300, X = 0 }
}
};
assertOk(spinnerConsecutiveBeatmap, DifficultyRating.Easy);
assertOk(spinnerConsecutiveBeatmap, DifficultyRating.Normal);
assertOk(spinnerConsecutiveBeatmap, DifficultyRating.Hard);
assertOk(spinnerConsecutiveBeatmap, DifficultyRating.Insane);
assertOk(spinnerConsecutiveBeatmap, DifficultyRating.Expert);
assertOk(spinnerConsecutiveBeatmap, DifficultyRating.ExpertPlus);
}
private Beatmap<HitObject> mockBeatmap(double bananaShowerStart, double bananaShowerEnd, double nextFruitStart)
{
return new Beatmap<HitObject>
{
HitObjects = new List<HitObject>
{
new Fruit { StartTime = 0, X = 0 },
new BananaShower { StartTime = bananaShowerStart, EndTime = bananaShowerEnd, X = 0 },
new Fruit { StartTime = nextFruitStart, X = 0 }
}
};
}
private void assertOk(IBeatmap beatmap, DifficultyRating difficultyRating)
{
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating);
Assert.That(check.Run(context), Is.Empty);
}
private void assertTooShortSpinnerStart(IBeatmap beatmap, DifficultyRating difficultyRating)
{
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating);
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.All(issue => issue.Template is CheckBananaShowerGap.IssueTemplateBananaShowerStartGap));
}
private void assertTooShortSpinnerEnd(IBeatmap beatmap, DifficultyRating difficultyRating)
{
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating);
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.All(issue => issue.Template is CheckBananaShowerGap.IssueTemplateBananaShowerEndGap));
}
}
}

View File

@ -0,0 +1,110 @@
// 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 NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests.Mods
{
public class TestSceneCatchModNoScope : ModTestScene
{
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
[Test]
public void TestVisibleDuringBreak()
{
CreateModTest(new ModTestData
{
Mod = new CatchModNoScope
{
HiddenComboCount = { Value = 0 },
},
Autoplay = true,
PassCondition = () => true,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Fruit
{
X = CatchPlayfield.CENTER_X,
StartTime = 1000,
},
new Fruit
{
X = CatchPlayfield.CENTER_X,
StartTime = 5000,
}
},
Breaks = new List<BreakPeriod>
{
new BreakPeriod(2000, 4000),
}
}
});
AddUntilStep("wait for catcher to hide", () => catcherAlphaAlmostEquals(0));
AddUntilStep("wait for start of break", isBreak);
AddUntilStep("wait for catcher to show", () => catcherAlphaAlmostEquals(1));
AddUntilStep("wait for end of break", () => !isBreak());
AddUntilStep("wait for catcher to hide", () => catcherAlphaAlmostEquals(0));
}
[Test]
public void TestVisibleAfterComboBreak()
{
CreateModTest(new ModTestData
{
Mod = new CatchModNoScope
{
HiddenComboCount = { Value = 2 },
},
Autoplay = true,
PassCondition = () => true,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Fruit
{
X = 0,
StartTime = 1000,
},
new Fruit
{
X = CatchPlayfield.CENTER_X,
StartTime = 3000,
},
new Fruit
{
X = CatchPlayfield.WIDTH,
StartTime = 5000,
},
}
}
});
AddAssert("catcher must start visible", () => catcherAlphaAlmostEquals(1));
AddUntilStep("wait for combo", () => Player.ScoreProcessor.Combo.Value >= 2);
AddAssert("catcher must dim after combo", () => !catcherAlphaAlmostEquals(1));
AddStep("break combo", () => Player.ScoreProcessor.Combo.Value = 0);
AddUntilStep("wait for catcher to show", () => catcherAlphaAlmostEquals(1));
}
private bool isBreak() => Player.IsBreakTime.Value;
private bool catcherAlphaAlmostEquals(float alpha)
{
var playfield = (CatchPlayfield)Player.DrawableRuleset.Playfield;
return Precision.AlmostEquals(playfield.CatcherArea.Alpha, alpha);
}
}
}

View File

@ -133,6 +133,7 @@ namespace osu.Game.Rulesets.Catch
new MultiMod(new ModWindUp(), new ModWindDown()),
new CatchModFloatingFruits(),
new CatchModMuted(),
new CatchModNoScope(),
};
default:
@ -188,5 +189,7 @@ namespace osu.Game.Rulesets.Catch
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame();
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier();
}
}

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Catch.Edit.Checks;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Catch.Edit
{
public class CatchBeatmapVerifier : IBeatmapVerifier
{
private readonly List<ICheck> checks = new List<ICheck>
{
new CheckBananaShowerGap()
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
return checks.SelectMany(check => check.Run(context));
}
}
}

View File

@ -0,0 +1,102 @@
// 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 osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Catch.Edit.Checks
{
/// <summary>
/// Check the spinner/banana shower gaps specified in the osu!catch difficulty specific ranking criteria.
/// </summary>
public class CheckBananaShowerGap : ICheck
{
private static readonly Dictionary<DifficultyRating, (int startGap, int endGap)> spinner_delta_threshold = new Dictionary<DifficultyRating, (int, int)>
{
[DifficultyRating.Easy] = (250, 250),
[DifficultyRating.Normal] = (250, 250),
[DifficultyRating.Hard] = (125, 250),
[DifficultyRating.Insane] = (125, 125),
[DifficultyRating.Expert] = (62, 125),
[DifficultyRating.ExpertPlus] = (62, 125)
};
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Compose, "Too short spinner gap");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateBananaShowerStartGap(this),
new IssueTemplateBananaShowerEndGap(this)
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
var hitObjects = context.Beatmap.HitObjects;
(int expectedStartDelta, int expectedEndDelta) = spinner_delta_threshold[context.InterpretedDifficulty];
for (int i = 0; i < hitObjects.Count - 1; ++i)
{
if (!(hitObjects[i] is BananaShower bananaShower))
continue;
// Skip if the previous hitobject is a banana shower, consecutive spinners are allowed
if (i != 0 && hitObjects[i - 1] is CatchHitObject previousHitObject && !(previousHitObject is BananaShower))
{
double spinnerStartDelta = bananaShower.StartTime - previousHitObject.GetEndTime();
if (spinnerStartDelta < expectedStartDelta)
{
yield return new IssueTemplateBananaShowerStartGap(this)
.Create(spinnerStartDelta, expectedStartDelta, bananaShower, previousHitObject);
}
}
// Skip if the next hitobject is a banana shower, consecutive spinners are allowed
if (hitObjects[i + 1] is CatchHitObject nextHitObject && !(nextHitObject is BananaShower))
{
double spinnerEndDelta = nextHitObject.StartTime - bananaShower.EndTime;
if (spinnerEndDelta < expectedEndDelta)
{
yield return new IssueTemplateBananaShowerEndGap(this)
.Create(spinnerEndDelta, expectedEndDelta, bananaShower, nextHitObject);
}
}
}
}
public abstract class IssueTemplateBananaShowerGap : IssueTemplate
{
protected IssueTemplateBananaShowerGap(ICheck check, IssueType issueType, string unformattedMessage)
: base(check, issueType, unformattedMessage)
{
}
public Issue Create(double deltaTime, int expectedDeltaTime, params HitObject[] hitObjects)
{
return new Issue(hitObjects, this, Math.Floor(deltaTime), expectedDeltaTime);
}
}
public class IssueTemplateBananaShowerStartGap : IssueTemplateBananaShowerGap
{
public IssueTemplateBananaShowerStartGap(ICheck check)
: base(check, IssueType.Problem, "There is only {0} ms between the start of the spinner and the last object, it should not be less than {1} ms.")
{
}
}
public class IssueTemplateBananaShowerEndGap : IssueTemplateBananaShowerGap
{
public IssueTemplateBananaShowerEndGap(ICheck check)
: base(check, IssueType.Problem, "There is only {0} ms between the end of the spinner and the next object, it should not be less than {1} ms.")
{
}
}
}
}

View File

@ -0,0 +1,40 @@
// 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.Framework.Bindables;
using osu.Game.Rulesets.Mods;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModNoScope : ModNoScope, IUpdatableByPlayfield
{
public override string Description => "Where's the catcher?";
[SettingSource(
"Hidden at combo",
"The combo count at which the catcher becomes completely hidden",
SettingControlType = typeof(SettingsSlider<int, HiddenComboSlider>)
)]
public override BindableInt HiddenComboCount { get; } = new BindableInt
{
Default = 10,
Value = 10,
MinValue = 0,
MaxValue = 50,
};
public void Update(Playfield playfield)
{
var catchPlayfield = (CatchPlayfield)playfield;
bool shouldAlwaysShowCatcher = IsBreakTime.Value;
float targetAlpha = shouldAlwaysShowCatcher ? 1 : ComboBasedAlpha;
catchPlayfield.CatcherArea.Alpha = (float)Interpolation.Lerp(catchPlayfield.CatcherArea.Alpha, targetAlpha, Math.Clamp(catchPlayfield.Time.Elapsed / TRANSITION_DURATION, 0, 1));
}
}
}

View File

@ -22,6 +22,9 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
[Resolved]
private SkinManager skins { get; set; }
[Cached]
private EditorClipboard clipboard = new EditorClipboard();
[SetUpSteps]
public void SetUpSteps()
{

View File

@ -0,0 +1,185 @@
// 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.Utils;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public class TestSceneSliderStreamConversion : TestSceneOsuEditor
{
private BindableBeatDivisor beatDivisor => (BindableBeatDivisor)Editor.Dependencies.Get(typeof(BindableBeatDivisor));
[Test]
public void TestSimpleConversion()
{
Slider slider = null;
AddStep("select first slider", () =>
{
slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider);
EditorClock.Seek(slider.StartTime);
EditorBeatmap.SelectedHitObjects.Add(slider);
});
convertToStream();
AddAssert("stream created", () => streamCreatedFor(slider,
(time: 0, pathPosition: 0),
(time: 0.25, pathPosition: 0.25),
(time: 0.5, pathPosition: 0.5),
(time: 0.75, pathPosition: 0.75),
(time: 1, pathPosition: 1)));
AddStep("undo", () => Editor.Undo());
AddAssert("slider restored", () => sliderRestored(slider));
AddStep("select first slider", () =>
{
slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider);
EditorClock.Seek(slider.StartTime);
EditorBeatmap.SelectedHitObjects.Add(slider);
});
AddStep("change beat divisor", () => beatDivisor.Value = 8);
convertToStream();
AddAssert("stream created", () => streamCreatedFor(slider,
(time: 0, pathPosition: 0),
(time: 0.125, pathPosition: 0.125),
(time: 0.25, pathPosition: 0.25),
(time: 0.375, pathPosition: 0.375),
(time: 0.5, pathPosition: 0.5),
(time: 0.625, pathPosition: 0.625),
(time: 0.75, pathPosition: 0.75),
(time: 0.875, pathPosition: 0.875),
(time: 1, pathPosition: 1)));
}
[Test]
public void TestConversionWithNonMatchingDivisor()
{
Slider slider = null;
AddStep("select second slider", () =>
{
slider = (Slider)EditorBeatmap.HitObjects.Where(h => h is Slider).ElementAt(1);
EditorClock.Seek(slider.StartTime);
EditorBeatmap.SelectedHitObjects.Add(slider);
});
AddStep("change beat divisor", () => beatDivisor.Value = 3);
convertToStream();
AddAssert("stream created", () => streamCreatedFor(slider,
(time: 0, pathPosition: 0),
(time: 2 / 3d, pathPosition: 2 / 3d)));
}
[Test]
public void TestConversionWithRepeats()
{
Slider slider = null;
AddStep("select first slider with repeats", () =>
{
slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider s && s.RepeatCount > 0);
EditorClock.Seek(slider.StartTime);
EditorBeatmap.SelectedHitObjects.Add(slider);
});
AddStep("change beat divisor", () => beatDivisor.Value = 2);
convertToStream();
AddAssert("stream created", () => streamCreatedFor(slider,
(time: 0, pathPosition: 0),
(time: 0.25, pathPosition: 0.5),
(time: 0.5, pathPosition: 1),
(time: 0.75, pathPosition: 0.5),
(time: 1, pathPosition: 0)));
}
[Test]
public void TestConversionPreservesSliderProperties()
{
Slider slider = null;
AddStep("select second new-combo-starting slider", () =>
{
slider = (Slider)EditorBeatmap.HitObjects.Where(h => h is Slider s && s.NewCombo).ElementAt(1);
EditorClock.Seek(slider.StartTime);
EditorBeatmap.SelectedHitObjects.Add(slider);
});
convertToStream();
AddAssert("stream created", () => streamCreatedFor(slider,
(time: 0, pathPosition: 0),
(time: 0.25, pathPosition: 0.25),
(time: 0.5, pathPosition: 0.5),
(time: 0.75, pathPosition: 0.75),
(time: 1, pathPosition: 1)));
AddStep("undo", () => Editor.Undo());
AddAssert("slider restored", () => sliderRestored(slider));
}
private void convertToStream()
{
AddStep("convert to stream", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.PressKey(Key.LShift);
InputManager.Key(Key.F);
InputManager.ReleaseKey(Key.LShift);
InputManager.ReleaseKey(Key.LControl);
});
}
private bool streamCreatedFor(Slider slider, params (double time, double pathPosition)[] expectedCircles)
{
if (EditorBeatmap.HitObjects.Contains(slider))
return false;
foreach ((double expectedTime, double expectedPathPosition) in expectedCircles)
{
double time = slider.StartTime + slider.Duration * expectedTime;
Vector2 position = slider.Position + slider.Path.PositionAt(expectedPathPosition);
if (!EditorBeatmap.HitObjects.OfType<HitCircle>().Any(h => matches(h, time, position, slider.NewCombo && expectedTime == 0)))
return false;
}
return true;
bool matches(HitCircle circle, double time, Vector2 position, bool startsNewCombo) =>
Precision.AlmostEquals(circle.StartTime, time, 1)
&& Precision.AlmostEquals(circle.Position, position, 0.01f)
&& circle.NewCombo == startsNewCombo
&& circle.Samples.SequenceEqual(slider.HeadCircle.Samples)
&& circle.SampleControlPoint.IsRedundant(slider.SampleControlPoint);
}
private bool sliderRestored(Slider slider)
{
var objects = EditorBeatmap.HitObjects.Where(h => h.StartTime >= slider.StartTime && h.GetEndTime() <= slider.EndTime).ToList();
if (objects.Count > 1)
return false;
var hitObject = objects.Single();
if (!(hitObject is Slider restoredSlider))
return false;
return Precision.AlmostEquals(slider.StartTime, restoredSlider.StartTime)
&& Precision.AlmostEquals(slider.GetEndTime(), restoredSlider.GetEndTime())
&& Precision.AlmostEquals(slider.Position, restoredSlider.Position, 0.01f)
&& Precision.AlmostEquals(slider.EndPosition, restoredSlider.EndPosition, 0.01f);
}
}
}

View File

@ -137,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
AddAssert("cursor must start visible", () => cursorAlphaAlmostEquals(1));
AddUntilStep("wait for combo", () => Player.ScoreProcessor.Combo.Value >= 2);
AddAssert("cursor must dim after combo", () => !cursorAlphaAlmostEquals(1));
AddStep("break combo", () => Player.ScoreProcessor.Combo.Set(0));
AddStep("break combo", () => Player.ScoreProcessor.Combo.Value = 0);
AddUntilStep("wait for cursor to show", () => cursorAlphaAlmostEquals(1));
}

View File

@ -15,13 +15,13 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
[TestCase(6.6975550434910005d, "diffcalc-test")]
[TestCase(1.4670676815251105d, "zero-length-sliders")]
[TestCase(6.6972307565739273d, "diffcalc-test")]
[TestCase(1.4484754139145539d, "zero-length-sliders")]
public void Test(double expected, string name)
=> base.Test(expected, name);
[TestCase(8.9389769779826267d, "diffcalc-test")]
[TestCase(1.7786917985891204d, "zero-length-sliders")]
[TestCase(8.9382559208689809d, "diffcalc-test")]
[TestCase(1.7548875851757628d, "zero-length-sliders")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new OsuModDoubleTime());

View File

@ -10,6 +10,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
public double AimStrain { get; set; }
public double SpeedStrain { get; set; }
public double FlashlightRating { get; set; }
public double SliderFactor { get; set; }
public double ApproachRate { get; set; }
public double OverallDifficulty { get; set; }
public double DrainRate { get; set; }

View File

@ -34,8 +34,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return new OsuDifficultyAttributes { Mods = mods, Skills = skills };
double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
double flashlightRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
double flashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * difficulty_multiplier;
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
if (mods.Any(h => h is OsuModRelax))
speedRating = 0.0;
@ -74,6 +77,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
AimStrain = aimRating,
SpeedStrain = speedRating,
FlashlightRating = flashlightRating,
SliderFactor = sliderFactor,
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
OverallDifficulty = (80 - hitWindowGreat) / 6,
DrainRate = drainRate,
@ -108,7 +112,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return new Skill[]
{
new Aim(mods),
new Aim(mods, true),
new Aim(mods, false),
new Speed(mods, hitWindowGreat),
new Flashlight(mods)
};

View File

@ -125,6 +125,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty
aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
}
// We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator.
double estimateDifficultSliders = Attributes.SliderCount * 0.15;
if (Attributes.SliderCount > 0)
{
double estimateSliderEndsDropped = Math.Clamp(Math.Min(countOk + countMeh + countMiss, Attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
double sliderNerfFactor = (1 - Attributes.SliderFactor) * Math.Pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + Attributes.SliderFactor;
aimValue *= sliderNerfFactor;
}
aimValue *= accuracy;
// It is important to also consider accuracy difficulty when doing that.
aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
private const int normalized_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
private const int min_delta_time = 25;
private const float maximum_slider_radius = normalized_radius * 2.4f;
private const float assumed_slider_radius = normalized_radius * 1.65f;
private const float assumed_slider_radius = normalized_radius * 1.8f;
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;

View File

@ -14,11 +14,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
/// </summary>
public class Aim : OsuStrainSkill
{
public Aim(Mod[] mods)
public Aim(Mod[] mods, bool withSliders)
: base(mods)
{
this.withSliders = withSliders;
}
private readonly bool withSliders;
protected override int HistoryLength => 2;
private const double wide_angle_multiplier = 1.5;
@ -44,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
double currVelocity = osuCurrObj.JumpDistance / osuCurrObj.StrainTime;
// But if the last object is a slider, then we extend the travel velocity through the slider into the current object.
if (osuLastObj.BaseObject is Slider)
if (osuLastObj.BaseObject is Slider && withSliders)
{
double movementVelocity = osuCurrObj.MovementDistance / osuCurrObj.MovementTime; // calculate the movement velocity from slider end to current object
double travelVelocity = osuCurrObj.TravelDistance / osuCurrObj.TravelTime; // calculate the slider velocity from slider head to slider end.
@ -55,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
// As above, do the same for the previous hitobject.
double prevVelocity = osuLastObj.JumpDistance / osuLastObj.StrainTime;
if (osuLastLastObj.BaseObject is Slider)
if (osuLastLastObj.BaseObject is Slider && withSliders)
{
double movementVelocity = osuLastObj.MovementDistance / osuLastObj.MovementTime;
double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime;
@ -135,7 +138,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier + velocityChangeBonus * velocity_change_multiplier);
// Add in additional slider velocity bonus.
aimStrain += sliderBonus * slider_multiplier;
if (withSliders)
aimStrain += sliderBonus * slider_multiplier;
return aimStrain;
}

View File

@ -11,9 +11,12 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
@ -47,6 +50,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
[Resolved(CanBeNull = true)]
private BindableBeatDivisor beatDivisor { get; set; }
public override Quad SelectionQuad => BodyPiece.ScreenSpaceDrawQuad;
private readonly BindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>();
@ -173,6 +179,20 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
}
protected override bool OnKeyDown(KeyDownEvent e)
{
if (!IsSelected)
return false;
if (e.Key == Key.F && e.ControlPressed && e.ShiftPressed)
{
convertToStream();
return true;
}
return false;
}
private int addControlPoint(Vector2 position)
{
position -= HitObject.Position;
@ -234,9 +254,56 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
editorBeatmap?.Update(HitObject);
}
private void convertToStream()
{
if (editorBeatmap == null || changeHandler == null || beatDivisor == null)
return;
var timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(HitObject.StartTime);
double streamSpacing = timingPoint.BeatLength / beatDivisor.Value;
changeHandler.BeginChange();
int i = 0;
double time = HitObject.StartTime;
while (!Precision.DefinitelyBigger(time, HitObject.GetEndTime(), 1))
{
// positionWithRepeats is a fractional number in the range of [0, HitObject.SpanCount()]
// and indicates how many fractional spans of a slider have passed up to time.
double positionWithRepeats = (time - HitObject.StartTime) / HitObject.Duration * HitObject.SpanCount();
double pathPosition = positionWithRepeats - (int)positionWithRepeats;
// every second span is in the reverse direction - need to reverse the path position.
if (Precision.AlmostBigger(positionWithRepeats % 2, 1))
pathPosition = 1 - pathPosition;
Vector2 position = HitObject.Position + HitObject.Path.PositionAt(pathPosition);
var samplePoint = (SampleControlPoint)HitObject.SampleControlPoint.DeepClone();
samplePoint.Time = time;
editorBeatmap.Add(new HitCircle
{
StartTime = time,
Position = position,
NewCombo = i == 0 && HitObject.NewCombo,
SampleControlPoint = samplePoint,
Samples = HitObject.HeadCircle.Samples.Select(s => s.With()).ToList()
});
i += 1;
time = HitObject.StartTime + i * streamSpacing;
}
editorBeatmap.Remove(HitObject);
changeHandler.EndChange();
}
public override MenuItem[] ContextMenuItems => new MenuItem[]
{
new OsuMenuItem("Add control point", MenuItemType.Standard, () => addControlPoint(rightClickPosition)),
new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream),
};
// Always refer to the drawable object's slider body so subsequent movement deltas are calculated with updated positions.

View File

@ -4,52 +4,29 @@
using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModNoScope : Mod, IUpdatableByPlayfield, IApplicableToScoreProcessor, IApplicableToPlayer, IApplicableToBeatmap
public class OsuModNoScope : ModNoScope, IUpdatableByPlayfield, IApplicableToBeatmap
{
/// <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 IBindable<bool> isBreakTime;
private PeriodTracker spinnerPeriods;
private float comboBasedAlpha;
[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
public override BindableInt HiddenComboCount { get; } = new BindableInt
{
Default = 10,
Value = 10,
@ -57,39 +34,16 @@ namespace osu.Game.Rulesets.Osu.Mods
MaxValue = 50,
};
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
public void ApplyToPlayer(Player player)
{
isBreakTime = player.IsBreakTime.GetBoundCopy();
}
public void ApplyToBeatmap(IBeatmap beatmap)
{
spinnerPeriods = new PeriodTracker(beatmap.HitObjects.OfType<Spinner>().Select(b => new Period(b.StartTime - transition_duration, b.EndTime)));
spinnerPeriods = new PeriodTracker(beatmap.HitObjects.OfType<Spinner>().Select(b => new Period(b.StartTime - TRANSITION_DURATION, b.EndTime)));
}
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
public void Update(Playfield playfield)
{
if (HiddenComboCount.Value == 0) return;
currentCombo = scoreProcessor.Combo.GetBoundCopy();
currentCombo.BindValueChanged(combo =>
{
comboBasedAlpha = Math.Max(min_alpha, 1 - (float)combo.NewValue / HiddenComboCount.Value);
}, true);
bool shouldAlwaysShowCursor = IsBreakTime.Value || spinnerPeriods.IsInAny(playfield.Clock.CurrentTime);
float targetAlpha = shouldAlwaysShowCursor ? 1 : ComboBasedAlpha;
playfield.Cursor.Alpha = (float)Interpolation.Lerp(playfield.Cursor.Alpha, targetAlpha, Math.Clamp(playfield.Time.Elapsed / TRANSITION_DURATION, 0, 1));
}
public virtual void Update(Playfield playfield)
{
bool shouldAlwaysShowCursor = isBreakTime.Value || spinnerPeriods.IsInAny(playfield.Clock.CurrentTime);
float targetAlpha = shouldAlwaysShowCursor ? 1 : comboBasedAlpha;
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

@ -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 System.Linq;
using NUnit.Framework;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Select;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Taiko.Tests.Editor
{
public class TestSceneEditorSaving : OsuGameTestScene
{
private Screens.Edit.Editor editor => Game.ChildrenOfType<Screens.Edit.Editor>().FirstOrDefault();
private EditorBeatmap editorBeatmap => (EditorBeatmap)editor.Dependencies.Get(typeof(EditorBeatmap));
/// <summary>
/// Tests the general expected flow of creating a new beatmap, saving it, then loading it back from song select.
/// Emphasis is placed on <see cref="BeatmapDifficulty.SliderMultiplier"/>, since taiko has special handling for it to keep compatibility with stable.
/// </summary>
[Test]
public void TestNewBeatmapSaveThenLoad()
{
AddStep("set default beatmap", () => Game.Beatmap.SetDefault());
AddStep("set taiko ruleset", () => Ruleset.Value = new TaikoRuleset().RulesetInfo);
PushAndConfirm(() => new EditorLoader());
AddUntilStep("wait for editor load", () => editor?.IsLoaded == true);
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 slider multiplier", () => editorBeatmap.Difficulty.SliderMultiplier = 2);
AddStep("Set artist and title", () =>
{
editorBeatmap.BeatmapInfo.Metadata.Artist = "artist";
editorBeatmap.BeatmapInfo.Metadata.Title = "title";
});
AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName = "difficulty");
checkMutations();
AddStep("Save", () => InputManager.Keys(PlatformAction.Save));
checkMutations();
AddStep("Exit", () => InputManager.Key(Key.Escape));
AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
PushAndConfirm(() => new PlaySongSelect());
AddUntilStep("Wait for beatmap selected", () => !Game.Beatmap.IsDefault);
AddStep("Open options", () => InputManager.Key(Key.F3));
AddStep("Enter editor", () => InputManager.Key(Key.Number5));
AddUntilStep("Wait for editor load", () => editor != null);
checkMutations();
}
private void checkMutations()
{
AddAssert("Beatmap has correct slider multiplier", () =>
{
// we can only assert value correctness on TaikoMultiplierAppliedDifficulty, because that is the final difficulty converted taiko beatmaps use.
// therefore, ensure that we have that difficulty type by calling .CopyFrom(), which is a no-op if the type is already correct.
var taikoDifficulty = new TaikoBeatmapConverter.TaikoMultiplierAppliedDifficulty();
taikoDifficulty.CopyFrom(editorBeatmap.Difficulty);
return Precision.AlmostEquals(taikoDifficulty.SliderMultiplier, 2);
});
AddAssert("Beatmap has correct metadata", () => editorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && editorBeatmap.BeatmapInfo.Metadata.Title == "title");
AddAssert("Beatmap has correct difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName == "difficulty");
}
}
}

View File

@ -191,7 +191,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
protected override Beatmap<TaikoHitObject> CreateBeatmap() => new TaikoBeatmap();
private class TaikoMultiplierAppliedDifficulty : BeatmapDifficulty
internal class TaikoMultiplierAppliedDifficulty : BeatmapDifficulty
{
public TaikoMultiplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty)
{
@ -209,7 +209,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{
base.CopyTo(other);
if (!(other is TaikoMultiplierAppliedDifficulty))
SliderMultiplier /= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
other.SliderMultiplier /= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
}
public override void CopyFrom(IBeatmapDifficultyInfo other)

View File

@ -113,11 +113,11 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual("Soleily", metadata.Artist);
Assert.AreEqual("Soleily", metadata.ArtistUnicode);
Assert.AreEqual("Gamu", metadata.Author.Username);
Assert.AreEqual("Insane", beatmapInfo.Version);
Assert.AreEqual("Insane", beatmapInfo.DifficultyName);
Assert.AreEqual(string.Empty, metadata.Source);
Assert.AreEqual("MBC7 Unisphere 地球ヤバイEP Chikyu Yabai", metadata.Tags);
Assert.AreEqual(557821, beatmapInfo.OnlineBeatmapID);
Assert.AreEqual(241526, beatmapInfo.BeatmapSet.OnlineBeatmapSetID);
Assert.AreEqual(557821, beatmapInfo.OnlineID);
Assert.AreEqual(241526, beatmapInfo.BeatmapSet.OnlineID);
}
}

View File

@ -31,7 +31,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
{
var beatmap = decodeAsJson(normal);
var meta = beatmap.BeatmapInfo.Metadata;
Assert.AreEqual(241526, beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID);
Assert.AreEqual(241526, beatmap.BeatmapInfo.BeatmapSet.OnlineID);
Assert.AreEqual("Soleily", meta.Artist);
Assert.AreEqual("Soleily", meta.ArtistUnicode);
Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", meta.AudioFile);

View File

@ -18,6 +18,7 @@ using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Scoring;
@ -387,6 +388,41 @@ namespace osu.Game.Tests.Beatmaps.IO
}
}
[Test]
public async Task TestModelCreationFailureDoesntReturn()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
{
try
{
var osu = LoadOsuIntoHost(host);
var importer = osu.Dependencies.Get<BeatmapManager>();
var progressNotification = new ImportProgressNotification();
var zipStream = new MemoryStream();
using (var zip = ZipArchive.Create())
zip.SaveTo(zipStream, new ZipWriterOptions(CompressionType.Deflate));
var imported = await importer.Import(
progressNotification,
new ImportTask(zipStream, string.Empty)
);
checkBeatmapSetCount(osu, 0);
checkBeatmapCount(osu, 0);
Assert.IsEmpty(imported);
Assert.AreEqual(ProgressNotificationState.Cancelled, progressNotification.State);
}
finally
{
host.Exit();
}
}
}
[Test]
public async Task TestRollbackOnFailure()
{
@ -408,8 +444,8 @@ namespace osu.Game.Tests.Beatmaps.IO
var manager = osu.Dependencies.Get<BeatmapManager>();
// ReSharper disable once AccessToModifiedClosure
manager.ItemUpdated.BindValueChanged(_ => Interlocked.Increment(ref itemAddRemoveFireCount));
manager.ItemRemoved.BindValueChanged(_ => Interlocked.Increment(ref itemAddRemoveFireCount));
manager.ItemUpdated += _ => Interlocked.Increment(ref itemAddRemoveFireCount);
manager.ItemRemoved += _ => Interlocked.Increment(ref itemAddRemoveFireCount);
var imported = await LoadOszIntoOsu(osu);
@ -506,7 +542,7 @@ namespace osu.Game.Tests.Beatmaps.IO
var imported = await LoadOszIntoOsu(osu);
foreach (var b in imported.Beatmaps)
b.OnlineBeatmapID = null;
b.OnlineID = null;
osu.Dependencies.Get<BeatmapManager>().Update(imported);
@ -545,19 +581,19 @@ namespace osu.Game.Tests.Beatmaps.IO
var toImport = new BeatmapSetInfo
{
OnlineBeatmapSetID = 1,
OnlineID = 1,
Metadata = metadata,
Beatmaps = new List<BeatmapInfo>
{
new BeatmapInfo
{
OnlineBeatmapID = 2,
OnlineID = 2,
Metadata = metadata,
BaseDifficulty = difficulty
},
new BeatmapInfo
{
OnlineBeatmapID = 2,
OnlineID = 2,
Metadata = metadata,
Status = BeatmapSetOnlineStatus.Loved,
BaseDifficulty = difficulty
@ -570,8 +606,8 @@ namespace osu.Game.Tests.Beatmaps.IO
var imported = await manager.Import(toImport);
Assert.NotNull(imported);
Assert.AreEqual(null, imported.Value.Beatmaps[0].OnlineBeatmapID);
Assert.AreEqual(null, imported.Value.Beatmaps[1].OnlineBeatmapID);
Assert.AreEqual(null, imported.Value.Beatmaps[0].OnlineID);
Assert.AreEqual(null, imported.Value.Beatmaps[1].OnlineID);
}
finally
{
@ -790,12 +826,12 @@ namespace osu.Game.Tests.Beatmaps.IO
// Update via the beatmap, not the beatmap info, to ensure correct linking
BeatmapSetInfo setToUpdate = manager.GetAllUsableBeatmapSets()[0];
Beatmap beatmapToUpdate = (Beatmap)manager.GetWorkingBeatmap(setToUpdate.Beatmaps.First(b => b.RulesetID == 0)).Beatmap;
beatmapToUpdate.BeatmapInfo.Version = "updated";
beatmapToUpdate.BeatmapInfo.DifficultyName = "updated";
manager.Update(setToUpdate);
BeatmapInfo updatedInfo = manager.QueryBeatmap(b => b.ID == beatmapToUpdate.BeatmapInfo.ID);
Assert.That(updatedInfo.Version, Is.EqualTo("updated"));
Assert.That(updatedInfo.DifficultyName, Is.EqualTo("updated"));
}
finally
{
@ -863,7 +899,7 @@ namespace osu.Game.Tests.Beatmaps.IO
var beatmap = working.Beatmap;
beatmap.BeatmapInfo.Version = "difficulty";
beatmap.BeatmapInfo.DifficultyName = "difficulty";
beatmap.BeatmapInfo.Metadata = new BeatmapMetadata
{
Artist = "Artist/With\\Slashes",
@ -1020,13 +1056,13 @@ namespace osu.Game.Tests.Beatmaps.IO
{
IEnumerable<BeatmapSetInfo> resultSets = null;
var store = osu.Dependencies.Get<BeatmapManager>();
waitForOrAssert(() => (resultSets = store.QueryBeatmapSets(s => s.OnlineBeatmapSetID == 241526)).Any(),
waitForOrAssert(() => (resultSets = store.QueryBeatmapSets(s => 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<BeatmapInfo> queryBeatmaps() => store.QueryBeatmaps(s => s.BeatmapSet.OnlineBeatmapSetID == 241526 && s.BaseDifficultyID > 0);
IEnumerable<BeatmapSetInfo> queryBeatmapSets() => store.QueryBeatmapSets(s => s.OnlineBeatmapSetID == 241526);
IEnumerable<BeatmapInfo> queryBeatmaps() => store.QueryBeatmaps(s => s.BeatmapSet.OnlineID == 241526 && s.BaseDifficultyID > 0);
IEnumerable<BeatmapSetInfo> queryBeatmapSets() => store.QueryBeatmapSets(s => s.OnlineID == 241526);
// if we don't re-check here, the set will be inserted but the beatmaps won't be present yet.
waitForOrAssert(() => queryBeatmaps().Count() == 12,
@ -1042,7 +1078,7 @@ namespace osu.Game.Tests.Beatmaps.IO
var set = queryBeatmapSets().First();
foreach (BeatmapInfo b in set.Beatmaps)
Assert.IsTrue(set.Beatmaps.Any(c => c.OnlineBeatmapID == b.OnlineBeatmapID));
Assert.IsTrue(set.Beatmaps.Any(c => c.OnlineID == b.OnlineID));
Assert.IsTrue(set.Beatmaps.Count > 0);
var beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 0))?.Beatmap;
Assert.IsTrue(beatmap?.HitObjects.Any() == true);

View File

@ -56,7 +56,7 @@ namespace osu.Game.Tests.Beatmaps.IO
var meta = beatmap.Metadata;
Assert.AreEqual(241526, beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID);
Assert.AreEqual(241526, beatmap.BeatmapInfo.BeatmapSet.OnlineID);
Assert.AreEqual("Soleily", meta.Artist);
Assert.AreEqual("Soleily", meta.ArtistUnicode);
Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", meta.AudioFile);

View File

@ -52,7 +52,7 @@ namespace osu.Game.Tests.Beatmaps
Title = "title",
Author = new APIUser { Username = "creator" }
},
Version = "difficulty"
DifficultyName = "difficulty"
};
Assert.That(beatmap.ToString(), Is.EqualTo("artist - title (creator) [difficulty]"));

View File

@ -16,6 +16,7 @@ using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO.Archives;
using osu.Game.Models;
using osu.Game.Overlays.Notifications;
using osu.Game.Stores;
using osu.Game.Tests.Resources;
using Realms;
@ -367,6 +368,34 @@ namespace osu.Game.Tests.Database
});
}
[Test]
public void TestModelCreationFailureDoesntReturn()
{
RunTestWithRealmAsync(async (realmFactory, storage) =>
{
using var importer = new BeatmapImporter(realmFactory, storage);
using var store = new RealmRulesetStore(realmFactory, storage);
var progressNotification = new ImportProgressNotification();
var zipStream = new MemoryStream();
using (var zip = ZipArchive.Create())
zip.SaveTo(zipStream, new ZipWriterOptions(CompressionType.Deflate));
var imported = await importer.Import(
progressNotification,
new ImportTask(zipStream, string.Empty)
);
checkBeatmapSetCount(realmFactory.Context, 0);
checkBeatmapCount(realmFactory.Context, 0);
Assert.IsEmpty(imported);
Assert.AreEqual(ProgressNotificationState.Cancelled, progressNotification.State);
});
}
[Test]
public void TestRollbackOnFailure()
{

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Platform;
using osu.Game.Database;
@ -33,7 +34,7 @@ namespace osu.Game.Tests.Database
storage = new NativeStorage(directory.FullName);
realmContextFactory = new RealmContextFactory(storage, "test");
keyBindingStore = new RealmKeyBindingStore(realmContextFactory);
keyBindingStore = new RealmKeyBindingStore(realmContextFactory, new ReadableKeyCombinationProvider());
}
[Test]

View File

@ -54,7 +54,7 @@ namespace osu.Game.Tests.Editing.Checks
{
// While this is a problem, it is out of scope for this check and is caught by a different one.
beatmap.Metadata.BackgroundFile = string.Empty;
var context = getContext(null, System.Array.Empty<byte>());
var context = getContext(null, new MemoryStream(System.Array.Empty<byte>()));
Assert.That(check.Run(context), Is.Empty);
}
@ -103,7 +103,7 @@ namespace osu.Game.Tests.Editing.Checks
[Test]
public void TestTooUncompressed()
{
var context = getContext(new Texture(1920, 1080), new byte[1024 * 1024 * 3]);
var context = getContext(new Texture(1920, 1080), new MemoryStream(new byte[1024 * 1024 * 3]));
var issues = check.Run(context).ToList();
@ -111,19 +111,32 @@ namespace osu.Game.Tests.Editing.Checks
Assert.That(issues.Single().Template is CheckBackgroundQuality.IssueTemplateTooUncompressed);
}
private BeatmapVerifierContext getContext(Texture background, [CanBeNull] byte[] fileBytes = null)
[Test]
public void TestStreamClosed()
{
return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(background, fileBytes).Object);
var background = new Texture(1920, 1080);
var stream = new Mock<MemoryStream>(new byte[1024 * 1024]);
var context = getContext(background, stream.Object);
Assert.That(check.Run(context), Is.Empty);
stream.Verify(x => x.Close(), Times.Once());
}
private BeatmapVerifierContext getContext(Texture background, [CanBeNull] Stream stream = null)
{
return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(background, stream).Object);
}
/// <summary>
/// Returns the mock of the working beatmap with the given background and filesize.
/// Returns the mock of the working beatmap with the given background and its file stream.
/// </summary>
/// <param name="background">The texture of the background.</param>
/// <param name="fileBytes">The bytes that represent the background file.</param>
private Mock<IWorkingBeatmap> getMockWorkingBeatmap(Texture background, [CanBeNull] byte[] fileBytes = null)
/// <param name="stream">The stream representing the background file.</param>
private Mock<IWorkingBeatmap> getMockWorkingBeatmap(Texture background, [CanBeNull] Stream stream = null)
{
var stream = new MemoryStream(fileBytes ?? new byte[1024 * 1024]);
stream ??= new MemoryStream(new byte[1024 * 1024]);
var mock = new Mock<IWorkingBeatmap>();
mock.SetupGet(w => w.Beatmap).Returns(beatmap);

View File

@ -0,0 +1,133 @@
// 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 Moq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Users;
#nullable enable
namespace osu.Game.Tests.Models
{
[TestFixture]
public class DisplayStringTest
{
[Test]
public void TestNull()
{
IBeatmapSetInfo? beatmap = null;
Assert.That(beatmap.GetDisplayString(), Is.EqualTo("null"));
}
[Test]
public void TestBeatmapSet()
{
var mock = new Mock<IBeatmapSetInfo>();
mock.Setup(m => m.Metadata!.Artist).Returns("artist");
mock.Setup(m => m.Metadata!.Title).Returns("title");
mock.Setup(m => m.Metadata!.Author.Username).Returns("author");
Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("artist - title (author)"));
}
[Test]
public void TestBeatmapSetWithNoAuthor()
{
var mock = new Mock<IBeatmapSetInfo>();
mock.Setup(m => m.Metadata!.Artist).Returns("artist");
mock.Setup(m => m.Metadata!.Title).Returns("title");
mock.Setup(m => m.Metadata!.Author.Username).Returns(string.Empty);
Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("artist - title"));
}
[Test]
public void TestBeatmapSetWithNoMetadata()
{
var mock = new Mock<IBeatmapSetInfo>();
mock.Setup(m => m.Metadata).Returns(new BeatmapMetadata());
Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("unknown artist - unknown title"));
}
[Test]
public void TestBeatmap()
{
var mock = new Mock<IBeatmapInfo>();
mock.Setup(m => m.Metadata.Artist).Returns("artist");
mock.Setup(m => m.Metadata.Title).Returns("title");
mock.Setup(m => m.Metadata.Author.Username).Returns("author");
mock.Setup(m => m.DifficultyName).Returns("difficulty");
Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("artist - title (author) [difficulty]"));
}
[Test]
public void TestMetadata()
{
var mock = new Mock<IBeatmapMetadataInfo>();
mock.Setup(m => m.Artist).Returns("artist");
mock.Setup(m => m.Title).Returns("title");
mock.Setup(m => m.Author.Username).Returns("author");
Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("artist - title (author)"));
}
[Test]
public void TestScore()
{
var mock = new Mock<IScoreInfo>();
mock.Setup(m => m.User).Returns(new APIUser { Username = "user" }); // TODO: temporary.
mock.Setup(m => m.Beatmap.Metadata.Artist).Returns("artist");
mock.Setup(m => m.Beatmap.Metadata.Title).Returns("title");
mock.Setup(m => m.Beatmap.Metadata.Author.Username).Returns("author");
mock.Setup(m => m.Beatmap.DifficultyName).Returns("difficulty");
Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("user playing artist - title (author) [difficulty]"));
}
[Test]
public void TestRuleset()
{
var mock = new Mock<IRulesetInfo>();
mock.Setup(m => m.Name).Returns("ruleset");
Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("ruleset"));
}
[Test]
public void TestUser()
{
var mock = new Mock<IUser>();
mock.Setup(m => m.Username).Returns("user");
Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("user"));
}
[Test]
public void TestFallback()
{
var fallback = new Fallback();
Assert.That(fallback.GetDisplayString(), Is.EqualTo("fallback"));
}
private class Fallback
{
public override string ToString() => "fallback";
}
}
}

View File

@ -12,8 +12,8 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestOnlineWithOnline()
{
var ourInfo = new BeatmapSetInfo { OnlineBeatmapSetID = 123 };
var otherInfo = new BeatmapSetInfo { OnlineBeatmapSetID = 123 };
var ourInfo = new BeatmapSetInfo { OnlineID = 123 };
var otherInfo = new BeatmapSetInfo { OnlineID = 123 };
Assert.AreEqual(ourInfo, otherInfo);
}
@ -30,8 +30,8 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestDatabasedWithOnline()
{
var ourInfo = new BeatmapSetInfo { ID = 123, OnlineBeatmapSetID = 12 };
var otherInfo = new BeatmapSetInfo { OnlineBeatmapSetID = 12 };
var ourInfo = new BeatmapSetInfo { ID = 123, OnlineID = 12 };
var otherInfo = new BeatmapSetInfo { OnlineID = 12 };
Assert.AreEqual(ourInfo, otherInfo);
}

View File

@ -17,7 +17,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
private BeatmapInfo getExampleBeatmap() => new BeatmapInfo
{
Ruleset = new RulesetInfo { ID = 5 },
StarDifficulty = 4.0d,
StarRating = 4.0d,
BaseDifficulty = new BeatmapDifficulty
{
ApproachRate = 5.0f,
@ -34,7 +34,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
Source = "unit tests",
Tags = "look for tags too",
},
Version = "version as well",
DifficultyName = "version as well",
Length = 2500,
BPM = 160,
BeatDivisor = 12,
@ -207,8 +207,8 @@ namespace osu.Game.Tests.NonVisual.Filtering
public void TestCriteriaMatchingBeatmapIDs(string query, bool filtered)
{
var beatmap = getExampleBeatmap();
beatmap.OnlineBeatmapID = 20201010;
beatmap.BeatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = 1535 };
beatmap.OnlineID = 20201010;
beatmap.BeatmapSet = new BeatmapSetInfo { OnlineID = 1535 };
var criteria = new FilterCriteria { SearchText = query };
var carouselItem = new CarouselBeatmap(beatmap);

View File

@ -22,7 +22,8 @@ namespace osu.Game.Tests.NonVisual.Ranking
var unstableRate = new UnstableRate(events);
Assert.IsTrue(Precision.AlmostEquals(unstableRate.Value, 10 * Math.Sqrt(10)));
Assert.IsNotNull(unstableRate.Value);
Assert.IsTrue(Precision.AlmostEquals(unstableRate.Value.Value, 10 * Math.Sqrt(10)));
}
[Test]

View File

@ -5,6 +5,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Notifications;
using osu.Game.Tests.Visual;
@ -16,7 +17,30 @@ namespace osu.Game.Tests.Online
private BeatmapManager beatmaps;
private ProgressNotification recentNotification;
private static readonly BeatmapSetInfo test_model = new BeatmapSetInfo { OnlineBeatmapSetID = 1 };
private static readonly BeatmapSetInfo test_db_model = new BeatmapSetInfo
{
OnlineID = 1,
Metadata = new BeatmapMetadata
{
Artist = "test author",
Title = "test title",
Author = new APIUser
{
Username = "mapper"
}
}
};
private static readonly APIBeatmapSet test_online_model = new APIBeatmapSet
{
OnlineID = 2,
Artist = "test author",
Title = "test title",
Author = new APIUser
{
Username = "mapper"
}
};
[BackgroundDependencyLoader]
private void load(BeatmapManager beatmaps)
@ -26,25 +50,41 @@ namespace osu.Game.Tests.Online
beatmaps.PostNotification = n => recentNotification = n as ProgressNotification;
}
private static readonly object[][] notification_test_cases =
{
new object[] { test_db_model },
new object[] { test_online_model }
};
[TestCaseSource(nameof(notification_test_cases))]
public void TestNotificationMessage(IBeatmapSetInfo model)
{
AddStep("clear recent notification", () => recentNotification = null);
AddStep("download beatmap", () => beatmaps.Download(model));
AddUntilStep("wait for notification", () => recentNotification != null);
AddUntilStep("notification text correct", () => recentNotification.Text.ToString() == "Downloading test author - test title (mapper)");
}
[Test]
public void TestCancelDownloadFromRequest()
{
AddStep("download beatmap", () => beatmaps.Download(test_model));
AddStep("download beatmap", () => beatmaps.Download(test_db_model));
AddStep("cancel download from request", () => beatmaps.GetExistingDownload(test_model).Cancel());
AddStep("cancel download from request", () => beatmaps.GetExistingDownload(test_db_model).Cancel());
AddUntilStep("is removed from download list", () => beatmaps.GetExistingDownload(test_model) == null);
AddUntilStep("is removed from download list", () => beatmaps.GetExistingDownload(test_db_model) == null);
AddAssert("is notification cancelled", () => recentNotification.State == ProgressNotificationState.Cancelled);
}
[Test]
public void TestCancelDownloadFromNotification()
{
AddStep("download beatmap", () => beatmaps.Download(test_model));
AddStep("download beatmap", () => beatmaps.Download(test_db_model));
AddStep("cancel download from notification", () => recentNotification.Close());
AddUntilStep("is removed from download list", () => beatmaps.GetExistingDownload(test_model) == null);
AddUntilStep("is removed from download list", () => beatmaps.GetExistingDownload(test_db_model) == null);
AddAssert("is notification cancelled", () => recentNotification.State == ProgressNotificationState.Cancelled);
}
}

View File

@ -58,7 +58,7 @@ namespace osu.Game.Tests.Online
testBeatmapInfo = getTestBeatmapInfo(testBeatmapFile);
testBeatmapSet = testBeatmapInfo.BeatmapSet;
var existing = beatmaps.QueryBeatmapSet(s => s.OnlineBeatmapSetID == testBeatmapSet.OnlineBeatmapSetID);
var existing = beatmaps.QueryBeatmapSet(s => s.OnlineID == testBeatmapSet.OnlineID);
if (existing != null)
beatmaps.Delete(existing);
@ -101,10 +101,10 @@ namespace osu.Game.Tests.Online
AddStep("import beatmap", () => beatmaps.Import(testBeatmapFile).Wait());
addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable);
AddStep("delete beatmap", () => beatmaps.Delete(beatmaps.QueryBeatmapSet(b => b.OnlineBeatmapSetID == testBeatmapSet.OnlineBeatmapSetID)));
AddStep("delete beatmap", () => beatmaps.Delete(beatmaps.QueryBeatmapSet(b => b.OnlineID == testBeatmapSet.OnlineID)));
addAvailabilityCheckStep("state not downloaded", BeatmapAvailability.NotDownloaded);
AddStep("undelete beatmap", () => beatmaps.Undelete(beatmaps.QueryBeatmapSet(b => b.OnlineBeatmapSetID == testBeatmapSet.OnlineBeatmapSetID)));
AddStep("undelete beatmap", () => beatmaps.Undelete(beatmaps.QueryBeatmapSet(b => b.OnlineID == testBeatmapSet.OnlineID)));
addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable);
}

View File

@ -9,8 +9,11 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osuTK;
@ -20,6 +23,8 @@ namespace osu.Game.Tests.Visual.Beatmaps
{
public class TestSceneBeatmapCard : OsuTestScene
{
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
private APIBeatmapSet[] testCases;
#region Test case generation
@ -31,22 +36,51 @@ namespace osu.Game.Tests.Visual.Beatmaps
normal.HasVideo = true;
normal.HasStoryboard = true;
var withStatistics = CreateAPIBeatmapSet(Ruleset.Value);
withStatistics.Title = withStatistics.TitleUnicode = "play favourite stats";
withStatistics.Status = BeatmapSetOnlineStatus.Approved;
withStatistics.FavouriteCount = 284_239;
withStatistics.PlayCount = 999_001;
withStatistics.Ranked = DateTimeOffset.Now.AddDays(-45);
withStatistics.HypeStatus = new BeatmapSetHypeStatus
{
Current = 34,
Required = 5
};
withStatistics.NominationStatus = new BeatmapSetNominationStatus
{
Current = 1,
Required = 2
};
var undownloadable = getUndownloadableBeatmapSet();
undownloadable.LastUpdated = DateTimeOffset.Now.AddYears(-1);
var someDifficulties = getManyDifficultiesBeatmapSet(11);
someDifficulties.Title = someDifficulties.TitleUnicode = "favourited";
someDifficulties.Title = someDifficulties.TitleUnicode = "some difficulties";
someDifficulties.Status = BeatmapSetOnlineStatus.Qualified;
someDifficulties.HasFavourited = true;
someDifficulties.FavouriteCount = 1;
someDifficulties.NominationStatus = new BeatmapSetNominationStatus
{
Current = 2,
Required = 2
};
var manyDifficulties = getManyDifficultiesBeatmapSet(100);
manyDifficulties.Status = BeatmapSetOnlineStatus.Pending;
var explicitMap = CreateAPIBeatmapSet(Ruleset.Value);
explicitMap.Title = someDifficulties.TitleUnicode = "explicit beatmap";
explicitMap.HasExplicitContent = true;
var featuredMap = CreateAPIBeatmapSet(Ruleset.Value);
featuredMap.Title = someDifficulties.TitleUnicode = "featured artist beatmap";
featuredMap.TrackId = 1;
var explicitFeaturedMap = CreateAPIBeatmapSet(Ruleset.Value);
explicitFeaturedMap.Title = someDifficulties.TitleUnicode = "explicit featured artist";
explicitFeaturedMap.HasExplicitContent = true;
explicitFeaturedMap.TrackId = 2;
@ -59,6 +93,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
testCases = new[]
{
normal,
withStatistics,
undownloadable,
someDifficulties,
manyDifficulties,
@ -134,6 +169,19 @@ namespace osu.Game.Tests.Visual.Beatmaps
#endregion
[SetUpSteps]
public void SetUpSteps()
{
AddStep("register request handling", () => dummyAPI.HandleRequest = request =>
{
if (!(request is PostBeatmapFavouriteRequest))
return false;
request.TriggerSuccess();
return true;
});
}
private Drawable createContent(OverlayColourScheme colourScheme, Func<APIBeatmapSet, Drawable> creationFunc)
{
var colourProvider = new OverlayColourProvider(colourScheme);

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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Drawables.Cards.Buttons;
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.Resources.Localisation.Web;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Beatmaps
{
public class TestSceneBeatmapCardFavouriteButton : OsuManualInputManagerTestScene
{
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
[Test]
public void TestInitialState([Values] bool favourited)
{
APIBeatmapSet beatmapSetInfo = null;
FavouriteButton button = null;
AddStep("create beatmap set", () =>
{
beatmapSetInfo = CreateAPIBeatmapSet(Ruleset.Value);
beatmapSetInfo.HasFavourited = favourited;
});
AddStep("create button", () => Child = button = new FavouriteButton(beatmapSetInfo) { Scale = new Vector2(2) });
assertCorrectIcon(favourited);
AddAssert("correct tooltip text", () => button.TooltipText == (favourited ? BeatmapsetsStrings.ShowDetailsUnfavourite : BeatmapsetsStrings.ShowDetailsFavourite));
}
[Test]
public void TestRequestHandling()
{
APIBeatmapSet beatmapSetInfo = null;
FavouriteButton button = null;
BeatmapFavouriteAction? lastRequestAction = null;
AddStep("create beatmap set", () => beatmapSetInfo = CreateAPIBeatmapSet(Ruleset.Value));
AddStep("create button", () => Child = button = new FavouriteButton(beatmapSetInfo) { Scale = new Vector2(2) });
assertCorrectIcon(false);
AddStep("register request handling", () => dummyAPI.HandleRequest = request =>
{
if (!(request is PostBeatmapFavouriteRequest favouriteRequest))
return false;
lastRequestAction = favouriteRequest.Action;
request.TriggerSuccess();
return true;
});
AddStep("click icon", () =>
{
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("favourite request sent", () => lastRequestAction == BeatmapFavouriteAction.Favourite);
assertCorrectIcon(true);
AddStep("click icon", () =>
{
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("unfavourite request sent", () => lastRequestAction == BeatmapFavouriteAction.UnFavourite);
assertCorrectIcon(false);
}
private void assertCorrectIcon(bool favourited) => AddAssert("icon correct",
() => this.ChildrenOfType<SpriteIcon>().Single().Icon.Equals(favourited ? FontAwesome.Solid.Heart : FontAwesome.Regular.Heart));
}
}

View File

@ -13,10 +13,17 @@ namespace osu.Game.Tests.Visual.Components
{
public class TestScenePreviewTrackManager : OsuTestScene, IPreviewTrackOwner
{
private readonly TestPreviewTrackManager trackManager = new TestPreviewTrackManager();
private readonly IAdjustableAudioComponent gameTrackAudio = new AudioAdjustments();
private readonly TestPreviewTrackManager trackManager;
private AudioManager audio;
public TestScenePreviewTrackManager()
{
trackManager = new TestPreviewTrackManager(gameTrackAudio);
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
@ -151,19 +158,19 @@ namespace osu.Game.Tests.Visual.Components
audio.VolumeTrack.Value = 1;
});
AddAssert("game not muted", () => audio.Tracks.AggregateVolume.Value != 0);
AddAssert("game not muted", () => gameTrackAudio.AggregateVolume.Value != 0);
AddStep("get track", () => Add(owner = new TestTrackOwner(track = getTrack())));
AddUntilStep("wait loaded", () => track.IsLoaded);
AddStep("start track", () => track.Start());
AddAssert("game is muted", () => audio.Tracks.AggregateVolume.Value == 0);
AddAssert("game is muted", () => gameTrackAudio.AggregateVolume.Value == 0);
if (stopAnyPlaying)
AddStep("stop any playing", () => trackManager.StopAnyPlaying(owner));
else
AddStep("stop track", () => track.Stop());
AddAssert("game not muted", () => audio.Tracks.AggregateVolume.Value != 0);
AddAssert("game not muted", () => gameTrackAudio.AggregateVolume.Value != 0);
}
[Test]
@ -224,6 +231,11 @@ namespace osu.Game.Tests.Visual.Components
public new PreviewTrack CurrentTrack => base.CurrentTrack;
public TestPreviewTrackManager(IAdjustableAudioComponent mainTrackAdjustments)
: base(mainTrackAdjustments)
{
}
protected override TrackManagerPreviewTrack CreatePreviewTrack(IBeatmapSetInfo beatmapSetInfo, ITrackStore trackStore) => new TestPreviewTrack(beatmapSetInfo, trackStore);
public override bool UpdateSubTree()

View File

@ -26,6 +26,9 @@ namespace osu.Game.Tests.Visual.Editing
}
});
[Cached]
private EditorClipboard clipboard = new EditorClipboard();
[BackgroundDependencyLoader]
private void load()
{

View File

@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Editing
editorBeatmap.BeatmapInfo.Metadata.Artist = "artist";
editorBeatmap.BeatmapInfo.Metadata.Title = "title";
});
AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.Version = "difficulty");
AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName = "difficulty");
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
@ -85,7 +85,7 @@ namespace osu.Game.Tests.Visual.Editing
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");
AddAssert("Beatmap has correct difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName == "difficulty");
}
}
}

View File

@ -0,0 +1,192 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps.IO;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneEditorTestGameplay : EditorTestScene
{
protected override bool IsolateSavingFromDatabase => false;
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
[Resolved]
private OsuGameBase game { get; set; }
[Resolved]
private BeatmapManager beatmaps { get; set; }
private BeatmapSetInfo importedBeatmapSet;
public override void SetUpSteps()
{
AddStep("import test beatmap", () => importedBeatmapSet = ImportBeatmapTest.LoadOszIntoOsu(game).Result);
base.SetUpSteps();
}
protected override void LoadEditor()
{
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First(b => b.RulesetID == 0));
base.LoadEditor();
}
[Test]
public void TestBasicGameplayTest()
{
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
AddStep("exit player", () => editorPlayer.Exit());
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
AddUntilStep("background has correct params", () =>
{
var background = this.ChildrenOfType<BackgroundScreenBeatmap>().Single();
return background.Colour == Color4.DarkGray && background.BlurAmount.Value == 0;
});
}
[Test]
public void TestGameplayTestWhenTrackRunning()
{
AddStep("start track", () => EditorClock.Start());
AddAssert("sample playback enabled", () => !Editor.SamplePlaybackDisabled.Value);
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
AddAssert("editor track stopped", () => !EditorClock.IsRunning);
AddAssert("sample playback disabled", () => Editor.SamplePlaybackDisabled.Value);
AddStep("exit player", () => editorPlayer.Exit());
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
AddUntilStep("background has correct params", () =>
{
var background = this.ChildrenOfType<BackgroundScreenBeatmap>().Single();
return background.Colour == Color4.DarkGray && background.BlurAmount.Value == 0;
});
AddStep("start track", () => EditorClock.Start());
AddAssert("sample playback re-enabled", () => !Editor.SamplePlaybackDisabled.Value);
}
[Test]
public void TestCancelGameplayTestWithUnsavedChanges()
{
AddStep("delete all but first object", () => EditorBeatmap.RemoveRange(EditorBeatmap.HitObjects.Skip(1).ToList()));
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveBeforeGameplayTestDialog);
AddStep("dismiss prompt", () =>
{
var button = DialogOverlay.CurrentDialog.Buttons.Last();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
AddWaitStep("wait some", 3);
AddAssert("stayed in editor", () => Stack.CurrentScreen is Editor);
}
[Test]
public void TestSaveChangesBeforeGameplayTest()
{
AddStep("delete all but first object", () => EditorBeatmap.RemoveRange(EditorBeatmap.HitObjects.Skip(1).ToList()));
// bit of a hack to ensure this test can be ran multiple times without running into UNIQUE constraint failures
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = Guid.NewGuid().ToString());
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveBeforeGameplayTestDialog);
AddStep("save changes", () => DialogOverlay.CurrentDialog.PerformOkAction());
EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
AddAssert("beatmap has 1 object", () => editorPlayer.Beatmap.Value.Beatmap.HitObjects.Count == 1);
AddUntilStep("wait for return to editor", () => Stack.CurrentScreen is Editor);
AddAssert("track stopped", () => !Beatmap.Value.Track.IsRunning);
}
[Test]
public void TestSharedClockState()
{
AddStep("seek to 00:01:00", () => EditorClock.Seek(60_000));
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
GameplayClockContainer gameplayClockContainer = null;
AddStep("fetch gameplay clock", () => gameplayClockContainer = editorPlayer.ChildrenOfType<GameplayClockContainer>().First());
AddUntilStep("gameplay clock running", () => gameplayClockContainer.IsRunning);
AddAssert("gameplay time past 00:01:00", () => gameplayClockContainer.CurrentTime >= 60_000);
double timeAtPlayerExit = 0;
AddWaitStep("wait some", 5);
AddStep("store time before exit", () => timeAtPlayerExit = gameplayClockContainer.CurrentTime);
AddStep("exit player", () => editorPlayer.Exit());
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
AddAssert("time is past player exit", () => EditorClock.CurrentTime >= timeAtPlayerExit);
}
public override void TearDownSteps()
{
base.TearDownSteps();
AddStep("delete imported", () =>
{
beatmaps.Delete(importedBeatmapSet);
});
}
}
}

View File

@ -0,0 +1,171 @@
// 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 Humanizer;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Screens.Edit.Timing;
using osu.Game.Tests.Beatmaps;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneHitObjectDifficultyPointAdjustments : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("add test objects", () =>
{
EditorBeatmap.Add(new Slider
{
StartTime = 0,
Position = (OsuPlayfield.BASE_SIZE - new Vector2(0, 100)) / 2,
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(new Vector2(0, 0)),
new PathControlPoint(new Vector2(0, 100))
}
}
});
EditorBeatmap.Add(new Slider
{
StartTime = 500,
Position = (OsuPlayfield.BASE_SIZE - new Vector2(100, 0)) / 2,
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(new Vector2(0, 0)),
new PathControlPoint(new Vector2(100, 0))
}
},
DifficultyControlPoint = new DifficultyControlPoint
{
SliderVelocity = 2
}
});
});
}
[Test]
public void TestSingleSelection()
{
clickDifficultyPiece(0);
velocityPopoverHasSingleValue(1);
dismissPopover();
// select first object to ensure that difficulty pieces for unselected objects
// work independently from selection state.
AddStep("select first object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.First()));
clickDifficultyPiece(1);
velocityPopoverHasSingleValue(2);
setVelocityViaPopover(5);
hitObjectHasVelocity(1, 5);
}
[Test]
public void TestMultipleSelectionWithSameSliderVelocity()
{
AddStep("unify slider velocity", () =>
{
foreach (var h in EditorBeatmap.HitObjects)
h.DifficultyControlPoint.SliderVelocity = 1.5;
});
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
clickDifficultyPiece(0);
velocityPopoverHasSingleValue(1.5);
dismissPopover();
clickDifficultyPiece(1);
velocityPopoverHasSingleValue(1.5);
setVelocityViaPopover(5);
hitObjectHasVelocity(0, 5);
hitObjectHasVelocity(1, 5);
}
[Test]
public void TestMultipleSelectionWithDifferentSliderVelocity()
{
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
clickDifficultyPiece(0);
velocityPopoverHasIndeterminateValue();
dismissPopover();
clickDifficultyPiece(1);
velocityPopoverHasIndeterminateValue();
setVelocityViaPopover(3);
hitObjectHasVelocity(0, 3);
hitObjectHasVelocity(1, 3);
}
private void clickDifficultyPiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} difficulty piece", () =>
{
var difficultyPiece = this.ChildrenOfType<DifficultyPointPiece>().Single(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex));
InputManager.MoveMouseTo(difficultyPiece);
InputManager.Click(MouseButton.Left);
});
private void velocityPopoverHasSingleValue(double velocity) => AddUntilStep($"velocity popover has {velocity}", () =>
{
var popover = this.ChildrenOfType<DifficultyPointPiece.DifficultyEditPopover>().SingleOrDefault();
var slider = popover?.ChildrenOfType<IndeterminateSliderWithTextBoxInput<double>>().Single();
return slider?.Current.Value == velocity;
});
private void velocityPopoverHasIndeterminateValue() => AddUntilStep("velocity popover has indeterminate value", () =>
{
var popover = this.ChildrenOfType<DifficultyPointPiece.DifficultyEditPopover>().SingleOrDefault();
var slider = popover?.ChildrenOfType<IndeterminateSliderWithTextBoxInput<double>>().Single();
return slider != null && slider.Current.Value == null;
});
private void dismissPopover()
{
AddStep("dismiss popover", () => InputManager.Key(Key.Escape));
AddUntilStep("wait for dismiss", () => !this.ChildrenOfType<DifficultyPointPiece.DifficultyEditPopover>().Any(popover => popover.IsPresent));
}
private void setVelocityViaPopover(double velocity) => AddStep($"set {velocity} via popover", () =>
{
var popover = this.ChildrenOfType<DifficultyPointPiece.DifficultyEditPopover>().Single();
var slider = popover.ChildrenOfType<IndeterminateSliderWithTextBoxInput<double>>().Single();
slider.Current.Value = velocity;
});
private void hitObjectHasVelocity(int objectIndex, double velocity) => AddAssert($"{objectIndex.ToOrdinalWords()} has velocity {velocity}", () =>
{
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
return h.DifficultyControlPoint.SliderVelocity == velocity;
});
}
}

View File

@ -0,0 +1,252 @@
// 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 Humanizer;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Screens.Edit.Timing;
using osu.Game.Tests.Beatmaps;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneHitObjectSamplePointAdjustments : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("add test objects", () =>
{
EditorBeatmap.Add(new HitCircle
{
StartTime = 0,
Position = (OsuPlayfield.BASE_SIZE - new Vector2(100, 0)) / 2,
SampleControlPoint = new SampleControlPoint
{
SampleBank = "normal",
SampleVolume = 80
}
});
EditorBeatmap.Add(new HitCircle
{
StartTime = 500,
Position = (OsuPlayfield.BASE_SIZE + new Vector2(100, 0)) / 2,
SampleControlPoint = new SampleControlPoint
{
SampleBank = "soft",
SampleVolume = 60
}
});
});
}
[Test]
public void TestSingleSelection()
{
clickSamplePiece(0);
samplePopoverHasSingleBank("normal");
samplePopoverHasSingleVolume(80);
dismissPopover();
// select first object to ensure that sample pieces for unselected objects
// work independently from selection state.
AddStep("select first object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.First()));
clickSamplePiece(1);
samplePopoverHasSingleBank("soft");
samplePopoverHasSingleVolume(60);
setVolumeViaPopover(90);
hitObjectHasSampleVolume(1, 90);
setBankViaPopover("drum");
hitObjectHasSampleBank(1, "drum");
}
[Test]
public void TestMultipleSelectionWithSameSampleVolume()
{
AddStep("unify sample volume", () =>
{
foreach (var h in EditorBeatmap.HitObjects)
h.SampleControlPoint.SampleVolume = 50;
});
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
clickSamplePiece(0);
samplePopoverHasSingleVolume(50);
dismissPopover();
clickSamplePiece(1);
samplePopoverHasSingleVolume(50);
setVolumeViaPopover(75);
hitObjectHasSampleVolume(0, 75);
hitObjectHasSampleVolume(1, 75);
}
[Test]
public void TestMultipleSelectionWithDifferentSampleVolume()
{
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
clickSamplePiece(0);
samplePopoverHasIndeterminateVolume();
dismissPopover();
clickSamplePiece(1);
samplePopoverHasIndeterminateVolume();
setVolumeViaPopover(30);
hitObjectHasSampleVolume(0, 30);
hitObjectHasSampleVolume(1, 30);
}
[Test]
public void TestMultipleSelectionWithSameSampleBank()
{
AddStep("unify sample bank", () =>
{
foreach (var h in EditorBeatmap.HitObjects)
h.SampleControlPoint.SampleBank = "soft";
});
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
clickSamplePiece(0);
samplePopoverHasSingleBank("soft");
dismissPopover();
clickSamplePiece(1);
samplePopoverHasSingleBank("soft");
setBankViaPopover(string.Empty);
hitObjectHasSampleBank(0, "soft");
hitObjectHasSampleBank(1, "soft");
samplePopoverHasSingleBank("soft");
setBankViaPopover("drum");
hitObjectHasSampleBank(0, "drum");
hitObjectHasSampleBank(1, "drum");
samplePopoverHasSingleBank("drum");
}
[Test]
public void TestMultipleSelectionWithDifferentSampleBank()
{
AddStep("select both objects", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
clickSamplePiece(0);
samplePopoverHasIndeterminateBank();
dismissPopover();
clickSamplePiece(1);
samplePopoverHasIndeterminateBank();
setBankViaPopover(string.Empty);
hitObjectHasSampleBank(0, "normal");
hitObjectHasSampleBank(1, "soft");
samplePopoverHasIndeterminateBank();
setBankViaPopover("normal");
hitObjectHasSampleBank(0, "normal");
hitObjectHasSampleBank(1, "normal");
samplePopoverHasSingleBank("normal");
}
private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} difficulty piece", () =>
{
var difficultyPiece = this.ChildrenOfType<SamplePointPiece>().Single(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex));
InputManager.MoveMouseTo(difficultyPiece);
InputManager.Click(MouseButton.Left);
});
private void samplePopoverHasSingleVolume(int volume) => AddUntilStep($"sample popover has volume {volume}", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().SingleOrDefault();
var slider = popover?.ChildrenOfType<IndeterminateSliderWithTextBoxInput<int>>().Single();
return slider?.Current.Value == volume;
});
private void samplePopoverHasIndeterminateVolume() => AddUntilStep("sample popover has indeterminate volume", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().SingleOrDefault();
var slider = popover?.ChildrenOfType<IndeterminateSliderWithTextBoxInput<int>>().Single();
return slider != null && slider.Current.Value == null;
});
private void samplePopoverHasSingleBank(string bank) => AddUntilStep($"sample popover has bank {bank}", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().SingleOrDefault();
var textBox = popover?.ChildrenOfType<OsuTextBox>().First();
return textBox?.Current.Value == bank && string.IsNullOrEmpty(textBox?.PlaceholderText.ToString());
});
private void samplePopoverHasIndeterminateBank() => AddUntilStep("sample popover has indeterminate bank", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().SingleOrDefault();
var textBox = popover?.ChildrenOfType<OsuTextBox>().First();
return textBox != null && string.IsNullOrEmpty(textBox.Current.Value) && !string.IsNullOrEmpty(textBox.PlaceholderText.ToString());
});
private void dismissPopover()
{
AddStep("dismiss popover", () => InputManager.Key(Key.Escape));
AddUntilStep("wait for dismiss", () => !this.ChildrenOfType<DifficultyPointPiece.DifficultyEditPopover>().Any(popover => popover.IsPresent));
}
private void setVolumeViaPopover(int volume) => AddStep($"set volume {volume} via popover", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().Single();
var slider = popover.ChildrenOfType<IndeterminateSliderWithTextBoxInput<int>>().Single();
slider.Current.Value = volume;
});
private void hitObjectHasSampleVolume(int objectIndex, int volume) => AddAssert($"{objectIndex.ToOrdinalWords()} has volume {volume}", () =>
{
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
return h.SampleControlPoint.SampleVolume == volume;
});
private void setBankViaPopover(string bank) => AddStep($"set bank {bank} via popover", () =>
{
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().Single();
var textBox = popover.ChildrenOfType<LabelledTextBox>().First();
textBox.Current.Value = bank;
// force a commit via keyboard.
// this is needed when testing attempting to set empty bank - which should revert to the previous value, but only on commit.
InputManager.ChangeFocus(textBox);
InputManager.Key(Key.Enter);
});
private void hitObjectHasSampleBank(int objectIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} has bank {bank}", () =>
{
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex);
return h.SampleControlPoint.SampleBank == bank;
});
}
}

View File

@ -4,6 +4,7 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Edit;
@ -23,6 +24,9 @@ namespace osu.Game.Tests.Visual.Editing
[Cached(typeof(IBeatSnapProvider))]
private readonly EditorBeatmap editorBeatmap;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
public TestSceneSetupScreen()
{
editorBeatmap = new EditorBeatmap(new OsuBeatmap());

View File

@ -4,6 +4,7 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
@ -18,6 +19,9 @@ namespace osu.Game.Tests.Visual.Editing
[Cached(typeof(IBeatSnapProvider))]
private readonly EditorBeatmap editorBeatmap;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
protected override bool ScrollUsingMouseWheel => false;
public TestSceneTimingScreen()

View File

@ -31,12 +31,18 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddUntilStep("wait for fail", () => Player.HasFailed);
AddUntilStep("wait for fail overlay", () => ((FailPlayer)Player).FailOverlay.State.Value == Visibility.Visible);
// The pause screen and fail animation both ramp frequency.
// This tests to ensure that it doesn't reset during that handoff.
AddAssert("frequency only ever decreased", () => !((FailPlayer)Player).FrequencyIncreased);
}
private class FailPlayer : TestPlayer
{
public new FailOverlay FailOverlay => base.FailOverlay;
public bool FrequencyIncreased { get; private set; }
public FailPlayer()
: base(false, false)
{
@ -47,6 +53,19 @@ namespace osu.Game.Tests.Visual.Gameplay
base.LoadComplete();
HealthProcessor.FailConditions += (_, __) => true;
}
private double lastFrequency = double.MaxValue;
protected override void Update()
{
base.Update();
double freq = Beatmap.Value.Track.AggregateFrequency.Value;
FrequencyIncreased |= freq > lastFrequency;
lastFrequency = freq;
}
}
}
}

View File

@ -1,9 +1,10 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets;
@ -20,16 +21,17 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public class TestScenePerformancePointsCounter : OsuTestScene
{
[Cached]
private GameplayState gameplayState;
private DependencyProvidingContainer dependencyContainer;
[Cached]
private GameplayState gameplayState;
private ScoreProcessor scoreProcessor;
private int iteration;
private Bindable<JudgementResult> lastJudgementResult = new Bindable<JudgementResult>();
private PerformancePointsCounter counter;
public TestScenePerformancePointsCounter()
[SetUpSteps]
public void SetUpSteps() => AddStep("create components", () =>
{
var ruleset = CreateRuleset();
@ -38,32 +40,43 @@ namespace osu.Game.Tests.Visual.Gameplay
var beatmap = CreateWorkingBeatmap(ruleset.RulesetInfo)
.GetPlayableBeatmap(ruleset.RulesetInfo);
lastJudgementResult = new Bindable<JudgementResult>();
gameplayState = new GameplayState(beatmap, ruleset);
gameplayState.LastJudgementResult.BindTo(lastJudgementResult);
scoreProcessor = new ScoreProcessor();
}
Child = dependencyContainer = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[]
{
(typeof(GameplayState), gameplayState),
(typeof(ScoreProcessor), scoreProcessor)
}
};
iteration = 0;
});
protected override Ruleset CreateRuleset() => new OsuRuleset();
[SetUpSteps]
public void SetUpSteps()
private void createCounter() => AddStep("Create counter", () =>
{
AddStep("Create counter", () =>
dependencyContainer.Child = counter = new PerformancePointsCounter
{
iteration = 0;
Child = counter = new PerformancePointsCounter
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(5),
};
});
}
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(5),
};
});
[Test]
public void TestBasicCounting()
{
int previousValue = 0;
createCounter();
AddAssert("counter displaying zero", () => counter.Current.Value == 0);
@ -86,6 +99,17 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("counter non-zero", () => counter.Current.Value > 0);
}
[Test]
public void TestCounterUpdatesWithJudgementsBeforeCreation()
{
AddRepeatStep("Add judgement", applyOneJudgement, 10);
createCounter();
AddUntilStep("counter non-zero", () => counter.Current.Value > 0);
AddUntilStep("counter opaque", () => counter.Child.Alpha == 1);
}
private void applyOneJudgement()
{
var scoreInfo = gameplayState.Score.ScoreInfo;
@ -94,13 +118,14 @@ namespace osu.Game.Tests.Visual.Gameplay
scoreInfo.Accuracy = 1;
scoreInfo.Statistics[HitResult.Great] = iteration * 1000;
scoreProcessor.ApplyResult(new OsuJudgementResult(new HitObject
lastJudgementResult.Value = new OsuJudgementResult(new HitObject
{
StartTime = iteration * 10000,
}, new OsuJudgement())
{
Type = HitResult.Perfect,
});
};
scoreProcessor.ApplyResult(lastJudgementResult.Value);
iteration++;
}

View File

@ -215,7 +215,7 @@ namespace osu.Game.Tests.Visual.Gameplay
createPlayerTest(false, r =>
{
var beatmap = createTestBeatmap(r);
beatmap.BeatmapInfo.OnlineBeatmapID = null;
beatmap.BeatmapInfo.OnlineID = null;
return beatmap;
});

View File

@ -10,6 +10,7 @@ using osu.Game.Scoring;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
@ -113,6 +114,36 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType<DownloadButton>().First().Enabled.Value);
}
[Resolved]
private ScoreManager scoreManager { get; set; }
[Test]
public void TestScoreImportThenDelete()
{
ILive<ScoreInfo> imported = null;
AddStep("create button without replay", () =>
{
Child = downloadButton = new TestReplayDownloadButton(getScoreInfo(false))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
});
AddUntilStep("wait for load", () => downloadButton.IsLoaded);
AddUntilStep("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded);
AddStep("import score", () => imported = scoreManager.Import(getScoreInfo(true)).Result);
AddUntilStep("state is available", () => downloadButton.State.Value == DownloadState.LocallyAvailable);
AddStep("delete score", () => scoreManager.Delete(imported.Value));
AddUntilStep("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded);
}
[Test]
public void CreateButtonWithNoScore()
{
@ -144,7 +175,7 @@ namespace osu.Game.Tests.Visual.Gameplay
Id = 39828,
Username = @"WubWoofWolf",
}
}.CreateScoreInfo(rulesets);
}.CreateScoreInfo(rulesets, CreateBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo);
}
private class TestReplayDownloadButton : ReplayDownloadButton

View File

@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("import beatmap", () =>
{
importedBeatmap = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).Result;
importedBeatmapId = importedBeatmap.Beatmaps.First(b => b.RulesetID == 0).OnlineBeatmapID ?? -1;
importedBeatmapId = importedBeatmap.Beatmaps.First(b => b.RulesetID == 0).OnlineID ?? -1;
});
}

View File

@ -0,0 +1,108 @@
// 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.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osuTK;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneUnstableRateCounter : OsuTestScene
{
[Cached(typeof(ScoreProcessor))]
private TestScoreProcessor scoreProcessor = new TestScoreProcessor();
private readonly OsuHitWindows hitWindows = new OsuHitWindows();
private UnstableRateCounter counter;
private double prev;
[SetUpSteps]
public void SetUp()
{
AddStep("Reset Score Processor", () => scoreProcessor.Reset());
}
[Test]
public void TestBasic()
{
AddStep("Create Display", recreateDisplay);
// Needs multiples 2 by the nature of UR, and went for 4 to be safe.
// Creates a 250 UR by placing a +25ms then a -25ms judgement, which then results in a 250 UR
AddRepeatStep("Set UR to 250", () => applyJudgement(25, true), 4);
AddUntilStep("UR = 250", () => counter.Current.Value == 250.0);
AddRepeatStep("Revert UR", () =>
{
scoreProcessor.RevertResult(
new JudgementResult(new HitCircle { HitWindows = hitWindows }, new Judgement())
{
TimeOffset = 25,
Type = HitResult.Perfect,
});
}, 4);
AddUntilStep("UR is 0", () => counter.Current.Value == 0.0);
AddUntilStep("Counter is invalid", () => counter.Child.Alpha == 0.3f);
//Sets a UR of 0 by creating 10 10ms offset judgements. Since average = offset, UR = 0
AddRepeatStep("Set UR to 0", () => applyJudgement(10, false), 10);
//Applies a UR of 100 by creating 10 -10ms offset judgements. At the 10th judgement, offset should be 100.
AddRepeatStep("Bring UR to 100", () => applyJudgement(-10, false), 10);
}
[Test]
public void TestCounterReceivesJudgementsBeforeCreation()
{
AddRepeatStep("Set UR to 250", () => applyJudgement(25, true), 4);
AddStep("Create Display", recreateDisplay);
AddUntilStep("UR = 250", () => counter.Current.Value == 250.0);
}
private void recreateDisplay()
{
Clear();
Add(counter = new UnstableRateCounter
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(5),
});
}
private void applyJudgement(double offsetMs, bool alt)
{
double placement = offsetMs;
if (alt)
{
placement = prev > 0 ? -offsetMs : offsetMs;
prev = placement;
}
scoreProcessor.ApplyResult(new JudgementResult(new HitCircle { HitWindows = hitWindows }, new Judgement())
{
TimeOffset = placement,
Type = HitResult.Perfect,
});
}
private class TestScoreProcessor : ScoreProcessor
{
public void Reset() => base.Reset(false);
}
}
}

View File

@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
BeatmapInfo =
{
StarDifficulty = 2.5
StarRating = 2.5
}
}.BeatmapInfo,
}
@ -82,7 +82,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
BeatmapInfo =
{
StarDifficulty = 2.5,
StarRating = 2.5,
Metadata =
{
Artist = "very very very very very very very very very long artist",
@ -111,7 +111,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
BeatmapInfo =
{
StarDifficulty = 2.5
StarRating = 2.5
}
}.BeatmapInfo,
}
@ -124,7 +124,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
BeatmapInfo =
{
StarDifficulty = 4.5
StarRating = 4.5
}
}.BeatmapInfo,
}

View File

@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
importedSet = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).Result;
importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0);
importedBeatmapId = importedBeatmap.OnlineBeatmapID ?? -1;
importedBeatmapId = importedBeatmap.OnlineID ?? -1;
}
[SetUp]

View File

@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
foreach (int user in users)
{
SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineID ?? 0);
multiplayerUsers.Add(OnlinePlayDependencies.Client.AddUser(new APIUser { Id = user }, true));
}

View File

@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
foreach (int user in users)
{
SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineID ?? 0);
var roomUser = OnlinePlayDependencies.Client.AddUser(new APIUser { Id = user }, true);
roomUser.MatchState = new TeamVersusUserState

View File

@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
beatmaps.Add(new BeatmapInfo
{
Ruleset = rulesets.GetRuleset(i % 4),
OnlineBeatmapID = beatmapId,
OnlineID = beatmapId,
Length = length,
BPM = bpm,
BaseDifficulty = new BeatmapDifficulty()
@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
manager.Import(new BeatmapSetInfo
{
OnlineBeatmapSetID = 10,
OnlineID = 10,
Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
Metadata = new BeatmapMetadata
{

View File

@ -54,8 +54,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
beatmaps.Add(new BeatmapInfo
{
Ruleset = new OsuRuleset().RulesetInfo,
OnlineBeatmapID = beatmapId,
Version = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
OnlineID = beatmapId,
DifficultyName = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
Length = length,
BPM = bpm,
BaseDifficulty = new BeatmapDifficulty
@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
manager.Import(new BeatmapSetInfo
{
OnlineBeatmapSetID = 10,
OnlineID = 10,
Hash = new MemoryStream(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())).ComputeMD5Hash(),
Metadata = new BeatmapMetadata
{

View File

@ -31,8 +31,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
SelectedRoom.Value.Playlist.AddRange(new[]
{
new PlaylistItem { Beatmap = { Value = new BeatmapInfo { StarDifficulty = min } } },
new PlaylistItem { Beatmap = { Value = new BeatmapInfo { StarDifficulty = max } } },
new PlaylistItem { Beatmap = { Value = new BeatmapInfo { StarRating = min } } },
new PlaylistItem { Beatmap = { Value = new BeatmapInfo { StarRating = max } } },
});
});
}

View File

@ -10,6 +10,7 @@ using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
@ -97,16 +98,24 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
AddAssert("user on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
AddStep("add another user", () => client.AddUser(new APIUser { Username = "otheruser", Id = 44 }));
AddStep("press button", () =>
AddStep("press own button", () =>
{
InputManager.MoveMouseTo(multiplayerScreenStack.ChildrenOfType<TeamDisplay>().First());
InputManager.Click(MouseButton.Left);
});
AddAssert("user on team 1", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 1);
AddStep("press button", () => InputManager.Click(MouseButton.Left));
AddStep("press own button again", () => InputManager.Click(MouseButton.Left));
AddAssert("user on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
AddStep("press other user's button", () =>
{
InputManager.MoveMouseTo(multiplayerScreenStack.ChildrenOfType<TeamDisplay>().ElementAt(1));
InputManager.Click(MouseButton.Left);
});
AddAssert("user still on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
}
[Test]
@ -127,9 +136,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("room type is head to head", () => client.Room?.Settings.MatchType == MatchType.HeadToHead);
AddUntilStep("team displays are not displaying teams", () => multiplayerScreenStack.ChildrenOfType<TeamDisplay>().All(d => d.DisplayedTeam == null));
AddStep("change to team vs", () => client.ChangeSettings(matchType: MatchType.TeamVersus));
AddAssert("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus);
AddUntilStep("team displays are displaying teams", () => multiplayerScreenStack.ChildrenOfType<TeamDisplay>().All(d => d.DisplayedTeam != null));
}
private void createRoom(Func<Room> room)

View File

@ -174,7 +174,7 @@ namespace osu.Game.Tests.Visual.Navigation
{
AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait());
PushAndConfirm(() => new TestPlaySongSelect());
AddUntilStep("beatmap updated", () => Game.Beatmap.Value.BeatmapSetInfo.OnlineBeatmapSetID == 241526);
AddUntilStep("beatmap updated", () => Game.Beatmap.Value.BeatmapSetInfo.OnlineID == 241526);
}
public class DialogBlockingScreen : OsuScreen

View File

@ -107,20 +107,20 @@ namespace osu.Game.Tests.Visual.Navigation
imported = Game.BeatmapManager.Import(new BeatmapSetInfo
{
Hash = Guid.NewGuid().ToString(),
OnlineBeatmapSetID = i,
OnlineID = i,
Metadata = metadata,
Beatmaps = new List<BeatmapInfo>
{
new BeatmapInfo
{
OnlineBeatmapID = i * 1024,
OnlineID = i * 1024,
Metadata = metadata,
BaseDifficulty = difficulty,
Ruleset = ruleset ?? new OsuRuleset().RulesetInfo
},
new BeatmapInfo
{
OnlineBeatmapID = i * 2048,
OnlineID = i * 2048,
Metadata = metadata,
BaseDifficulty = difficulty,
Ruleset = ruleset ?? new OsuRuleset().RulesetInfo
@ -145,11 +145,11 @@ namespace osu.Game.Tests.Visual.Navigation
private void presentSecondDifficultyAndConfirm(Func<BeatmapSetInfo> getImport, int importedID)
{
Predicate<BeatmapInfo> pred = b => b.OnlineBeatmapID == importedID * 2048;
Predicate<BeatmapInfo> pred = b => b.OnlineID == importedID * 2048;
AddStep("present difficulty", () => Game.PresentBeatmap(getImport(), pred));
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect);
AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineBeatmapID == importedID * 2048);
AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineID == importedID * 2048);
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Beatmaps.First().Ruleset.ID);
}
}

View File

@ -39,20 +39,20 @@ namespace osu.Game.Tests.Visual.Navigation
beatmap = Game.BeatmapManager.Import(new BeatmapSetInfo
{
Hash = Guid.NewGuid().ToString(),
OnlineBeatmapSetID = 1,
OnlineID = 1,
Metadata = metadata,
Beatmaps = new List<BeatmapInfo>
{
new BeatmapInfo
{
OnlineBeatmapID = 1 * 1024,
OnlineID = 1 * 1024,
Metadata = metadata,
BaseDifficulty = difficulty,
Ruleset = new OsuRuleset().RulesetInfo
},
new BeatmapInfo
{
OnlineBeatmapID = 1 * 2048,
OnlineID = 1 * 2048,
Metadata = metadata,
BaseDifficulty = difficulty,
Ruleset = new OsuRuleset().RulesetInfo

View File

@ -44,11 +44,11 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("button state not downloaded", () => downloadButton.DownloadState == DownloadState.NotDownloaded);
AddStep("import soleily", () => beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()));
AddUntilStep("wait for beatmap import", () => beatmaps.GetAllUsableBeatmapSets().Any(b => b.OnlineBeatmapSetID == 241526));
AddAssert("button state downloaded", () => downloadButton.DownloadState == DownloadState.LocallyAvailable);
AddUntilStep("wait for beatmap import", () => beatmaps.GetAllUsableBeatmapSets().Any(b => b.OnlineID == 241526));
AddUntilStep("button state downloaded", () => downloadButton.DownloadState == DownloadState.LocallyAvailable);
createButtonWithBeatmap(createSoleily());
AddAssert("button state downloaded", () => downloadButton.DownloadState == DownloadState.LocallyAvailable);
AddUntilStep("button state downloaded", () => downloadButton.DownloadState == DownloadState.LocallyAvailable);
ensureSoleilyRemoved();
AddAssert("button state not downloaded", () => downloadButton.DownloadState == DownloadState.NotDownloaded);
}
@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Online
{
AddStep("remove soleily", () =>
{
var beatmap = beatmaps.QueryBeatmapSet(b => b.OnlineBeatmapSetID == 241526);
var beatmap = beatmaps.QueryBeatmapSet(b => b.OnlineID == 241526);
if (beatmap != null) beatmaps.Delete(beatmap);
});

View File

@ -64,7 +64,7 @@ namespace osu.Game.Tests.Visual.Online
AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null)
{
BeatmapInfo = { OnlineBeatmapID = hasOnlineId ? 1234 : (int?)null }
BeatmapInfo = { OnlineID = hasOnlineId ? 1234 : (int?)null }
});
AddStep("Run command", () => Add(new NowPlayingCommand()));

View File

@ -121,8 +121,8 @@ namespace osu.Game.Tests.Visual.Playlists
beatmap.BeatmapInfo.BaseDifficulty.CircleSize = 1;
// intentionally increment online IDs to clash with import below.
beatmap.BeatmapInfo.OnlineBeatmapID++;
beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID++;
beatmap.BeatmapInfo.OnlineID++;
beatmap.BeatmapInfo.BeatmapSet.OnlineID++;
importedSet = manager.Import(beatmap.BeatmapInfo.BeatmapSet).Result.Value;
});

View File

@ -337,7 +337,7 @@ namespace osu.Game.Tests.Visual.Ranking
public UnrankedSoloResultsScreen(ScoreInfo score)
: base(score, true)
{
Score.BeatmapInfo.OnlineBeatmapID = 0;
Score.BeatmapInfo.OnlineID = 0;
Score.BeatmapInfo.Status = BeatmapSetOnlineStatus.Pending;
}

View File

@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.SongSelect
OverallDifficulty = 5.7f,
ApproachRate = 3.5f
},
StarDifficulty = 4.5f
StarRating = 4.5f
};
[Test]
@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.SongSelect
OverallDifficulty = 4.5f,
ApproachRate = 3.1f
},
StarDifficulty = 8
StarRating = 8
});
AddAssert("first bar text is Key Count", () => advancedStats.ChildrenOfType<SpriteText>().First().Text == "Key Count");

View File

@ -435,8 +435,8 @@ namespace osu.Game.Tests.Visual.SongSelect
for (int i = 0; i < 3; i++)
{
var set = createTestBeatmapSet(i);
set.Beatmaps[0].StarDifficulty = 3 - i;
set.Beatmaps[2].StarDifficulty = 6 + i;
set.Beatmaps[0].StarRating = 3 - i;
set.Beatmaps[2].StarRating = 6 + i;
sets.Add(set);
}
@ -684,9 +684,9 @@ namespace osu.Game.Tests.Visual.SongSelect
{
set.Beatmaps.Add(new BeatmapInfo
{
Version = $"Stars: {i}",
DifficultyName = $"Stars: {i}",
Ruleset = new OsuRuleset().RulesetInfo,
StarDifficulty = i,
StarRating = i,
});
}
@ -838,7 +838,7 @@ namespace osu.Game.Tests.Visual.SongSelect
return new BeatmapSetInfo
{
ID = id,
OnlineBeatmapSetID = id,
OnlineID = id,
Hash = new MemoryStream(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())).ComputeMD5Hash(),
Metadata = new BeatmapMetadata
{
@ -867,9 +867,9 @@ namespace osu.Game.Tests.Visual.SongSelect
yield return new BeatmapInfo
{
OnlineBeatmapID = id++ * 10,
Version = version,
StarDifficulty = diff,
OnlineID = id++ * 10,
DifficultyName = version,
StarRating = diff,
Ruleset = new OsuRuleset().RulesetInfo,
BaseDifficulty = new BeatmapDifficulty
{
@ -884,7 +884,7 @@ namespace osu.Game.Tests.Visual.SongSelect
var toReturn = new BeatmapSetInfo
{
ID = id,
OnlineBeatmapSetID = id,
OnlineID = id,
Hash = new MemoryStream(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())).ComputeMD5Hash(),
Metadata = new BeatmapMetadata
{
@ -900,11 +900,11 @@ namespace osu.Game.Tests.Visual.SongSelect
{
toReturn.Beatmaps.Add(new BeatmapInfo
{
OnlineBeatmapID = b * 10,
OnlineID = b * 10,
Path = $"extra{b}.osu",
Version = $"Extra {b}",
DifficultyName = $"Extra {b}",
Ruleset = rulesets.GetRuleset((b - 1) % 4),
StarDifficulty = 2,
StarRating = 2,
BaseDifficulty = new BeatmapDifficulty
{
OverallDifficulty = 3.5f,

View File

@ -198,8 +198,8 @@ namespace osu.Game.Tests.Visual.SongSelect
Title = $"{ruleset.ShortName}Title"
},
Ruleset = ruleset,
StarDifficulty = 6,
Version = $"{ruleset.ShortName}Version",
StarRating = 6,
DifficultyName = $"{ruleset.ShortName}Version",
BaseDifficulty = new BeatmapDifficulty()
},
HitObjects = objects
@ -219,7 +219,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Source = "Verrrrry long Source",
Title = "Verrrrry long Title"
},
Version = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Version",
DifficultyName = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Version",
Status = BeatmapSetOnlineStatus.Graveyard,
},
};

View File

@ -388,7 +388,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
leaderboard.BeatmapInfo = new BeatmapInfo
{
OnlineBeatmapID = 1113057,
OnlineID = 1113057,
Status = status,
};
}

View File

@ -45,8 +45,8 @@ namespace osu.Game.Tests.Visual.SongSelect
{
Title = title,
},
Version = version,
StarDifficulty = RNG.NextDouble(0, 10),
DifficultyName = version,
StarRating = RNG.NextDouble(0, 10),
}
}));
}
@ -64,8 +64,8 @@ namespace osu.Game.Tests.Visual.SongSelect
{
Title = "Heavy beatmap",
},
Version = "10k objects",
StarDifficulty = 99.99f,
DifficultyName = "10k objects",
StarRating = 99.99f,
}
}));

View File

@ -180,16 +180,16 @@ namespace osu.Game.Tests.Visual.SongSelect
var beatmapSet = new BeatmapSetInfo
{
Hash = Guid.NewGuid().ToString(),
OnlineBeatmapSetID = importID,
OnlineID = importID,
Metadata = metadata,
Beatmaps = difficultyRulesets.Select((ruleset, difficultyIndex) => new BeatmapInfo
{
OnlineBeatmapID = importID * 1024 + difficultyIndex,
OnlineID = importID * 1024 + difficultyIndex,
Metadata = metadata,
BaseDifficulty = new BeatmapDifficulty(),
Ruleset = ruleset,
StarDifficulty = difficultyIndex + 1,
Version = $"SR{difficultyIndex + 1}"
StarRating = difficultyIndex + 1,
DifficultyName = $"SR{difficultyIndex + 1}"
}).ToList()
};
@ -205,8 +205,8 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect);
AddUntilStep("recommended beatmap displayed", () =>
{
int? expectedID = getImport().Beatmaps[expectedDiff - 1].OnlineBeatmapID;
return Game.Beatmap.Value.BeatmapInfo.OnlineBeatmapID == expectedID;
int? expectedID = getImport().Beatmaps[expectedDiff - 1].OnlineID;
return Game.Beatmap.Value.BeatmapInfo.OnlineID == expectedID;
});
}
}

View File

@ -507,13 +507,13 @@ namespace osu.Game.Tests.Visual.SongSelect
i.IsFiltered || i.Item.BeatmapInfo.Ruleset.ID == targetRuleset || i.Item.BeatmapInfo.Ruleset.ID == 0);
});
AddUntilStep("carousel has correct", () => songSelect.Carousel.SelectedBeatmapInfo?.OnlineBeatmapID == target.OnlineBeatmapID);
AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.OnlineBeatmapID == target.OnlineBeatmapID);
AddUntilStep("carousel has correct", () => songSelect.Carousel.SelectedBeatmapInfo?.OnlineID == target.OnlineID);
AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.OnlineID == target.OnlineID);
AddStep("reset filter text", () => songSelect.FilterControl.ChildrenOfType<SearchTextBox>().First().Text = string.Empty);
AddAssert("game still correct", () => Beatmap.Value?.BeatmapInfo.OnlineBeatmapID == target.OnlineBeatmapID);
AddAssert("carousel still correct", () => songSelect.Carousel.SelectedBeatmapInfo.OnlineBeatmapID == target.OnlineBeatmapID);
AddAssert("game still correct", () => Beatmap.Value?.BeatmapInfo.OnlineID == target.OnlineID);
AddAssert("carousel still correct", () => songSelect.Carousel.SelectedBeatmapInfo.OnlineID == target.OnlineID);
}
[Test]
@ -544,8 +544,8 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo != null);
AddUntilStep("carousel has correct", () => songSelect.Carousel.SelectedBeatmapInfo?.OnlineBeatmapID == target.OnlineBeatmapID);
AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.OnlineBeatmapID == target.OnlineBeatmapID);
AddUntilStep("carousel has correct", () => songSelect.Carousel.SelectedBeatmapInfo?.OnlineID == target.OnlineID);
AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.OnlineID == target.OnlineID);
AddStep("set filter text", () => songSelect.FilterControl.ChildrenOfType<SearchTextBox>().First().Text = "nononoo");
@ -918,8 +918,8 @@ namespace osu.Game.Tests.Visual.SongSelect
beatmaps.Add(new BeatmapInfo
{
Ruleset = getRuleset(),
OnlineBeatmapID = beatmapId,
Version = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
OnlineID = beatmapId,
DifficultyName = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
Length = length,
BPM = bpm,
BaseDifficulty = new BeatmapDifficulty
@ -931,7 +931,7 @@ namespace osu.Game.Tests.Visual.SongSelect
return new BeatmapSetInfo
{
OnlineBeatmapSetID = setId,
OnlineID = setId,
Hash = new MemoryStream(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())).ComputeMD5Hash(),
Metadata = new BeatmapMetadata
{

View File

@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Username = "TestAuthor"
},
},
Version = "Insane"
DifficultyName = "Insane"
},
}
},

View File

@ -1,13 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Mixing;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -19,27 +14,26 @@ namespace osu.Game.Audio
{
public class PreviewTrackManager : Component
{
private readonly IAdjustableAudioComponent mainTrackAdjustments;
private readonly BindableDouble muteBindable = new BindableDouble();
[Resolved]
private AudioManager audio { get; set; }
private PreviewTrackStore trackStore;
private ITrackStore trackStore;
protected TrackManagerPreviewTrack CurrentTrack;
private readonly BindableNumber<double> globalTrackVolumeAdjust = new BindableNumber<double>(OsuGameBase.GLOBAL_TRACK_VOLUME_ADJUST);
public PreviewTrackManager(IAdjustableAudioComponent mainTrackAdjustments)
{
this.mainTrackAdjustments = mainTrackAdjustments;
}
[BackgroundDependencyLoader]
private void load(AudioManager audioManager)
{
// this is a temporary solution to get around muting ourselves.
// todo: update this once we have a BackgroundTrackManager or similar.
trackStore = new PreviewTrackStore(audioManager.TrackMixer, new OnlineStore());
audio.AddItem(trackStore);
trackStore.AddAdjustment(AdjustableProperty.Volume, globalTrackVolumeAdjust);
trackStore.AddAdjustment(AdjustableProperty.Volume, audio.VolumeTrack);
trackStore = audioManager.GetTrackStore(new OnlineStore());
}
/// <summary>
@ -55,7 +49,7 @@ namespace osu.Game.Audio
{
CurrentTrack?.Stop();
CurrentTrack = track;
audio.Tracks.AddAdjustment(AdjustableProperty.Volume, muteBindable);
mainTrackAdjustments.AddAdjustment(AdjustableProperty.Volume, muteBindable);
});
track.Stopped += () => Schedule(() =>
@ -64,7 +58,7 @@ namespace osu.Game.Audio
return;
CurrentTrack = null;
audio.Tracks.RemoveAdjustment(AdjustableProperty.Volume, muteBindable);
mainTrackAdjustments.RemoveAdjustment(AdjustableProperty.Volume, muteBindable);
});
return track;
@ -116,52 +110,5 @@ namespace osu.Game.Audio
protected override Track GetTrack() => trackManager.Get($"https://b.ppy.sh/preview/{beatmapSetInfo.OnlineID}.mp3");
}
private class PreviewTrackStore : AudioCollectionManager<AdjustableAudioComponent>, ITrackStore
{
private readonly AudioMixer defaultMixer;
private readonly IResourceStore<byte[]> store;
internal PreviewTrackStore(AudioMixer defaultMixer, IResourceStore<byte[]> store)
{
this.defaultMixer = defaultMixer;
this.store = store;
}
public Track GetVirtual(double length = double.PositiveInfinity)
{
if (IsDisposed) throw new ObjectDisposedException($"Cannot retrieve items for an already disposed {nameof(PreviewTrackStore)}");
var track = new TrackVirtual(length);
AddItem(track);
return track;
}
public Track Get(string name)
{
if (IsDisposed) throw new ObjectDisposedException($"Cannot retrieve items for an already disposed {nameof(PreviewTrackStore)}");
if (string.IsNullOrEmpty(name)) return null;
var dataStream = store.GetStream(name);
if (dataStream == null)
return null;
// Todo: This is quite unsafe. TrackBass shouldn't be exposed as public.
Track track = new TrackBass(dataStream);
defaultMixer.Add(track);
AddItem(track);
return track;
}
public Task<Track> GetAsync(string name) => Task.Run(() => Get(name));
public Stream GetStream(string name) => store.GetStream(name);
public IEnumerable<string> GetAvailableResources() => store.GetAvailableResources();
}
}
}

View File

@ -56,7 +56,7 @@ namespace osu.Game.Beatmaps
Title = @"Unknown",
AuthorString = @"Unknown Creator",
},
Version = @"Normal",
DifficultyName = @"Normal",
BaseDifficulty = Difficulty,
};
}

View File

@ -291,7 +291,7 @@ namespace osu.Game.Beatmaps
catch (BeatmapInvalidForRulesetException e)
{
if (rulesetInfo.Equals(beatmapInfo.Ruleset))
Logger.Error(e, $"Failed to convert {beatmapInfo.OnlineBeatmapID} to the beatmap's default ruleset ({beatmapInfo.Ruleset}).");
Logger.Error(e, $"Failed to convert {beatmapInfo.OnlineID} to the beatmap's default ruleset ({beatmapInfo.Ruleset}).");
return new StarDifficulty();
}

View File

@ -23,13 +23,14 @@ namespace osu.Game.Beatmaps
public int BeatmapVersion;
private int? onlineBeatmapID;
private int? onlineID;
[JsonProperty("id")]
public int? OnlineBeatmapID
[Column("OnlineBeatmapID")]
public int? OnlineID
{
get => onlineBeatmapID;
set => onlineBeatmapID = value > 0 ? value : null;
get => onlineID;
set => onlineID = value > 0 ? value : null;
}
[JsonIgnore]
@ -134,12 +135,12 @@ namespace osu.Game.Beatmaps
public double TimelineZoom { get; set; }
// Metadata
public string Version { get; set; }
private string versionString => string.IsNullOrEmpty(Version) ? string.Empty : $"[{Version}]";
[Column("Version")]
public string DifficultyName { get; set; }
[JsonProperty("difficulty_rating")]
public double StarDifficulty { get; set; }
[Column("StarDifficulty")]
public double StarRating { get; set; }
/// <summary>
/// Currently only populated for beatmap deletion. Use <see cref="ScoreManager"/> to query scores.
@ -147,7 +148,7 @@ namespace osu.Game.Beatmaps
public List<ScoreInfo> Scores { get; set; }
[JsonIgnore]
public DifficultyRating DifficultyRating => BeatmapDifficultyCache.GetDifficultyRating(StarDifficulty);
public DifficultyRating DifficultyRating => BeatmapDifficultyCache.GetDifficultyRating(StarRating);
public override string ToString() => this.GetDisplayTitle();
@ -176,15 +177,12 @@ namespace osu.Game.Beatmaps
#region Implementation of IHasOnlineID
public int OnlineID => OnlineBeatmapID ?? -1;
int IHasOnlineID<int>.OnlineID => OnlineID ?? -1;
#endregion
#region Implementation of IBeatmapInfo
[JsonIgnore]
string IBeatmapInfo.DifficultyName => Version;
[JsonIgnore]
IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata ?? BeatmapSet?.Metadata ?? new BeatmapMetadata();
@ -197,9 +195,6 @@ namespace osu.Game.Beatmaps
[JsonIgnore]
IRulesetInfo IBeatmapInfo.Ruleset => Ruleset;
[JsonIgnore]
double IBeatmapInfo.StarRating => StarDifficulty;
#endregion
}
}

View File

@ -11,7 +11,7 @@ namespace osu.Game.Beatmaps
/// <summary>
/// A user-presentable display title representing this beatmap.
/// </summary>
public static string GetDisplayTitle(this IBeatmapInfo beatmapInfo) => $"{beatmapInfo.Metadata} {getVersionString(beatmapInfo)}".Trim();
public static string GetDisplayTitle(this IBeatmapInfo beatmapInfo) => $"{beatmapInfo.Metadata.GetDisplayTitle()} {getVersionString(beatmapInfo)}".Trim();
/// <summary>
/// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields.

View File

@ -10,7 +10,8 @@ using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Audio.Mixing;
using osu.Framework.Audio.Track;
using osu.Framework.IO.Stores;
using osu.Framework.Platform;
using osu.Framework.Testing;
@ -31,18 +32,23 @@ namespace osu.Game.Beatmaps
[ExcludeFromDynamicCompile]
public class BeatmapManager : IModelDownloader<IBeatmapSetInfo>, IModelManager<BeatmapSetInfo>, IModelFileManager<BeatmapSetInfo, BeatmapSetFileInfo>, IModelImporter<BeatmapSetInfo>, IWorkingBeatmapCache, IDisposable
{
public ITrackStore BeatmapTrackStore { get; }
private readonly BeatmapModelManager beatmapModelManager;
private readonly BeatmapModelDownloader beatmapModelDownloader;
private readonly WorkingBeatmapCache workingBeatmapCache;
private readonly BeatmapOnlineLookupQueue onlineBeatmapLookupQueue;
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> resources, GameHost host = null,
WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false)
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> gameResources, GameHost host = null, WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false, AudioMixer mainTrackMixer = null)
{
var userResources = new FileStore(contextFactory, storage).Store;
BeatmapTrackStore = audioManager.GetTrackStore(userResources);
beatmapModelManager = CreateBeatmapModelManager(storage, contextFactory, rulesets, api, host);
beatmapModelDownloader = CreateBeatmapModelDownloader(beatmapModelManager, api, host);
workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, resources, new FileStore(contextFactory, storage).Store, defaultBeatmap, host);
workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host);
workingBeatmapCache.BeatmapManager = beatmapModelManager;
beatmapModelManager.WorkingBeatmapCache = workingBeatmapCache;
@ -59,8 +65,10 @@ namespace osu.Game.Beatmaps
return new BeatmapModelDownloader(modelManager, api, host);
}
protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> storage, WorkingBeatmap defaultBeatmap, GameHost host) =>
new WorkingBeatmapCache(audioManager, resources, storage, defaultBeatmap, host);
protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore<byte[]> resources, IResourceStore<byte[]> storage, WorkingBeatmap defaultBeatmap, GameHost host)
{
return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host);
}
protected virtual BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host) =>
new BeatmapModelManager(storage, contextFactory, rulesets, host);
@ -101,12 +109,20 @@ namespace osu.Game.Beatmaps
/// <summary>
/// Fired when a single difficulty has been hidden.
/// </summary>
public IBindable<WeakReference<BeatmapInfo>> BeatmapHidden => beatmapModelManager.BeatmapHidden;
public event Action<BeatmapInfo> BeatmapHidden
{
add => beatmapModelManager.BeatmapHidden += value;
remove => beatmapModelManager.BeatmapHidden -= value;
}
/// <summary>
/// Fired when a single difficulty has been restored.
/// </summary>
public IBindable<WeakReference<BeatmapInfo>> BeatmapRestored => beatmapModelManager.BeatmapRestored;
public event Action<BeatmapInfo> BeatmapRestored
{
add => beatmapModelManager.BeatmapRestored += value;
remove => beatmapModelManager.BeatmapRestored -= value;
}
/// <summary>
/// Saves an <see cref="IBeatmap"/> file against a given <see cref="BeatmapInfo"/>.
@ -198,9 +214,17 @@ namespace osu.Game.Beatmaps
return beatmapModelManager.IsAvailableLocally(model);
}
public IBindable<WeakReference<BeatmapSetInfo>> ItemUpdated => beatmapModelManager.ItemUpdated;
public event Action<BeatmapSetInfo> ItemUpdated
{
add => beatmapModelManager.ItemUpdated += value;
remove => beatmapModelManager.ItemUpdated -= value;
}
public IBindable<WeakReference<BeatmapSetInfo>> ItemRemoved => beatmapModelManager.ItemRemoved;
public event Action<BeatmapSetInfo> ItemRemoved
{
add => beatmapModelManager.ItemRemoved += value;
remove => beatmapModelManager.ItemRemoved -= value;
}
public Task ImportFromStableAsync(StableStorage stableStorage)
{
@ -246,9 +270,17 @@ namespace osu.Game.Beatmaps
#region Implementation of IModelDownloader<BeatmapSetInfo>
public IBindable<WeakReference<ArchiveDownloadRequest<IBeatmapSetInfo>>> DownloadBegan => beatmapModelDownloader.DownloadBegan;
public event Action<ArchiveDownloadRequest<IBeatmapSetInfo>> DownloadBegan
{
add => beatmapModelDownloader.DownloadBegan += value;
remove => beatmapModelDownloader.DownloadBegan -= value;
}
public IBindable<WeakReference<ArchiveDownloadRequest<IBeatmapSetInfo>>> DownloadFailed => beatmapModelDownloader.DownloadFailed;
public event Action<ArchiveDownloadRequest<IBeatmapSetInfo>> DownloadFailed
{
add => beatmapModelDownloader.DownloadFailed += value;
remove => beatmapModelDownloader.DownloadFailed -= value;
}
public bool Download(IBeatmapSetInfo model, bool minimiseDownloadSize = false) =>
beatmapModelDownloader.Download(model, minimiseDownloadSize);

View File

@ -27,8 +27,12 @@ namespace osu.Game.Beatmaps
/// </summary>
public static string GetDisplayTitle(this IBeatmapMetadataInfo metadataInfo)
{
string author = string.IsNullOrEmpty(metadataInfo.Author.Username) ? string.Empty : $"({metadataInfo.Author})";
return $"{metadataInfo.Artist} - {metadataInfo.Title} {author}".Trim();
string author = string.IsNullOrEmpty(metadataInfo.Author.Username) ? string.Empty : $" ({metadataInfo.Author.Username})";
string artist = string.IsNullOrEmpty(metadataInfo.Artist) ? "unknown artist" : metadataInfo.Artist;
string title = string.IsNullOrEmpty(metadataInfo.Title) ? "unknown title" : metadataInfo.Title;
return $"{artist} - {title}{author}".Trim();
}
/// <summary>

View File

@ -11,7 +11,6 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Textures;
using osu.Framework.Logging;
@ -37,14 +36,12 @@ namespace osu.Game.Beatmaps
/// <summary>
/// Fired when a single difficulty has been hidden.
/// </summary>
public IBindable<WeakReference<BeatmapInfo>> BeatmapHidden => beatmapHidden;
private readonly Bindable<WeakReference<BeatmapInfo>> beatmapHidden = new Bindable<WeakReference<BeatmapInfo>>();
public event Action<BeatmapInfo> BeatmapHidden;
/// <summary>
/// Fired when a single difficulty has been restored.
/// </summary>
public IBindable<WeakReference<BeatmapInfo>> BeatmapRestored => beatmapRestored;
public event Action<BeatmapInfo> BeatmapRestored;
/// <summary>
/// An online lookup queue component which handles populating online beatmap metadata.
@ -56,8 +53,6 @@ namespace osu.Game.Beatmaps
/// </summary>
public IWorkingBeatmapCache WorkingBeatmapCache { private get; set; }
private readonly Bindable<WeakReference<BeatmapInfo>> beatmapRestored = new Bindable<WeakReference<BeatmapInfo>>();
public override IEnumerable<string> HandledExtensions => new[] { ".osz" };
protected override string[] HashableFileTypes => new[] { ".osu" };
@ -75,8 +70,8 @@ namespace osu.Game.Beatmaps
this.rulesets = rulesets;
beatmaps = (BeatmapStore)ModelStore;
beatmaps.BeatmapHidden += b => beatmapHidden.Value = new WeakReference<BeatmapInfo>(b);
beatmaps.BeatmapRestored += b => beatmapRestored.Value = new WeakReference<BeatmapInfo>(b);
beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b);
beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b);
beatmaps.ItemRemoved += b => WorkingBeatmapCache?.Invalidate(b);
beatmaps.ItemUpdated += obj => WorkingBeatmapCache?.Invalidate(obj);
}
@ -99,17 +94,17 @@ namespace osu.Game.Beatmaps
validateOnlineIds(beatmapSet);
bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0);
bool hadOnlineIDs = beatmapSet.Beatmaps.Any(b => b.OnlineID > 0);
if (OnlineLookupQueue != null)
await OnlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false);
// ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID.
if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0))
if (hadOnlineIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineID > 0))
{
if (beatmapSet.OnlineBeatmapSetID != null)
if (beatmapSet.OnlineID != null)
{
beatmapSet.OnlineBeatmapSetID = null;
beatmapSet.OnlineID = null;
LogForModel(beatmapSet, "Disassociating beatmap set ID due to loss of all beatmap IDs");
}
}
@ -121,27 +116,27 @@ namespace osu.Game.Beatmaps
throw new InvalidOperationException($"Cannot import {nameof(BeatmapInfo)} with null {nameof(BeatmapInfo.BaseDifficulty)}.");
// check if a set already exists with the same online id, delete if it does.
if (beatmapSet.OnlineBeatmapSetID != null)
if (beatmapSet.OnlineID != null)
{
var existingSetWithSameOnlineID = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineBeatmapSetID == beatmapSet.OnlineBeatmapSetID);
var existingSetWithSameOnlineID = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineID == beatmapSet.OnlineID);
if (existingSetWithSameOnlineID != null)
{
Delete(existingSetWithSameOnlineID);
// in order to avoid a unique key constraint, immediately remove the online ID from the previous set.
existingSetWithSameOnlineID.OnlineBeatmapSetID = null;
existingSetWithSameOnlineID.OnlineID = null;
foreach (var b in existingSetWithSameOnlineID.Beatmaps)
b.OnlineBeatmapID = null;
b.OnlineID = null;
LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been deleted.");
LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineID}). It has been deleted.");
}
}
}
private void validateOnlineIds(BeatmapSetInfo beatmapSet)
{
var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList();
var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineID.HasValue).Select(b => b.OnlineID).ToList();
// ensure all IDs are unique
if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1))
@ -152,7 +147,7 @@ namespace osu.Game.Beatmaps
}
// find any existing beatmaps in the database that have matching online ids
var existingBeatmaps = QueryBeatmaps(b => beatmapIds.Contains(b.OnlineBeatmapID)).ToList();
var existingBeatmaps = QueryBeatmaps(b => beatmapIds.Contains(b.OnlineID)).ToList();
if (existingBeatmaps.Count > 0)
{
@ -167,7 +162,7 @@ namespace osu.Game.Beatmaps
}
}
void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineBeatmapID = null);
void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineID = null);
}
/// <summary>
@ -194,7 +189,11 @@ namespace osu.Game.Beatmaps
// Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`.
// This should hopefully be temporary, assuming said clone is eventually removed.
beatmapInfo.BaseDifficulty.CopyFrom(beatmapContent.Difficulty);
// Warning: The directionality here is important. Changes have to be copied *from* beatmapContent (which comes from editor and is being saved)
// *to* the beatmapInfo (which is a database model and needs to receive values without the taiko slider velocity multiplier for correct operation).
// CopyTo() will undo such adjustments, while CopyFrom() will not.
beatmapContent.Difficulty.CopyTo(beatmapInfo.BaseDifficulty);
// All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.
beatmapContent.BeatmapInfo = beatmapInfo;
@ -216,7 +215,7 @@ namespace osu.Game.Beatmaps
var fileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)) ?? new BeatmapSetFileInfo();
// metadata may have changed; update the path with the standard format.
beatmapInfo.Path = GetValidFilename($"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.Version}].osu");
beatmapInfo.Path = GetValidFilename($"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.DifficultyName}].osu");
beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
@ -243,7 +242,7 @@ namespace osu.Game.Beatmaps
if (!base.CanSkipImport(existing, import))
return false;
return existing.Beatmaps.Any(b => b.OnlineBeatmapID != null);
return existing.Beatmaps.Any(b => b.OnlineID != null);
}
protected override bool CanReuseExisting(BeatmapSetInfo existing, BeatmapSetInfo import)
@ -251,11 +250,11 @@ namespace osu.Game.Beatmaps
if (!base.CanReuseExisting(existing, import))
return false;
var existingIds = existing.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i);
var importIds = import.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i);
var existingIds = existing.Beatmaps.Select(b => b.OnlineID).OrderBy(i => i);
var importIds = import.Beatmaps.Select(b => b.OnlineID).OrderBy(i => i);
// force re-import if we are not in a sane state.
return existing.OnlineBeatmapSetID == import.OnlineBeatmapSetID && existingIds.SequenceEqual(importIds);
return existing.OnlineID == import.OnlineID && existingIds.SequenceEqual(importIds);
}
/// <summary>
@ -350,7 +349,7 @@ namespace osu.Game.Beatmaps
protected override bool CheckLocalAvailability(BeatmapSetInfo model, IQueryable<BeatmapSetInfo> items)
=> base.CheckLocalAvailability(model, items)
|| (model.OnlineBeatmapSetID != null && items.Any(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID));
|| (model.OnlineID != null && items.Any(b => b.OnlineID == model.OnlineID));
protected override BeatmapSetInfo CreateModel(ArchiveReader reader)
{
@ -369,7 +368,7 @@ namespace osu.Game.Beatmaps
return new BeatmapSetInfo
{
OnlineBeatmapSetID = beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID,
OnlineID = beatmap.BeatmapInfo.BeatmapSet?.OnlineID,
Beatmaps = new List<BeatmapInfo>(),
Metadata = beatmap.Metadata,
DateAdded = DateTimeOffset.UtcNow
@ -408,7 +407,7 @@ namespace osu.Game.Beatmaps
beatmap.BeatmapInfo.Ruleset = ruleset;
// TODO: this should be done in a better place once we actually need to dynamically update it.
beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0;
beatmap.BeatmapInfo.StarRating = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0;
beatmap.BeatmapInfo.Length = calculateLength(beatmap);
beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength();

View File

@ -84,8 +84,8 @@ namespace osu.Game.Beatmaps
{
beatmapInfo.Status = res.Status;
beatmapInfo.BeatmapSet.Status = res.BeatmapSet?.Status ?? BeatmapSetOnlineStatus.None;
beatmapInfo.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID;
beatmapInfo.OnlineBeatmapID = res.OnlineID;
beatmapInfo.BeatmapSet.OnlineID = res.OnlineBeatmapSetID;
beatmapInfo.OnlineID = res.OnlineID;
if (beatmapInfo.Metadata != null)
beatmapInfo.Metadata.AuthorID = res.AuthorID;
@ -103,7 +103,7 @@ namespace osu.Game.Beatmaps
void fail(Exception e)
{
beatmapInfo.OnlineBeatmapID = null;
beatmapInfo.OnlineID = null;
logForModel(set, $"Online retrieval failed for {beatmapInfo} ({e.Message})");
}
}
@ -161,7 +161,7 @@ namespace osu.Game.Beatmaps
if (string.IsNullOrEmpty(beatmapInfo.MD5Hash)
&& string.IsNullOrEmpty(beatmapInfo.Path)
&& beatmapInfo.OnlineBeatmapID == null)
&& beatmapInfo.OnlineID == null)
return false;
try
@ -172,10 +172,10 @@ namespace osu.Game.Beatmaps
using (var cmd = db.CreateCommand())
{
cmd.CommandText = "SELECT beatmapset_id, beatmap_id, approved, user_id FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path";
cmd.CommandText = "SELECT beatmapset_id, beatmap_id, approved, user_id FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineID OR filename = @Path";
cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmapInfo.MD5Hash));
cmd.Parameters.Add(new SqliteParameter("@OnlineBeatmapID", beatmapInfo.OnlineBeatmapID ?? (object)DBNull.Value));
cmd.Parameters.Add(new SqliteParameter("@OnlineID", beatmapInfo.OnlineID ?? (object)DBNull.Value));
cmd.Parameters.Add(new SqliteParameter("@Path", beatmapInfo.Path));
using (var reader = cmd.ExecuteReader())
@ -186,8 +186,8 @@ namespace osu.Game.Beatmaps
beatmapInfo.Status = status;
beatmapInfo.BeatmapSet.Status = status;
beatmapInfo.BeatmapSet.OnlineBeatmapSetID = reader.GetInt32(0);
beatmapInfo.OnlineBeatmapID = reader.GetInt32(1);
beatmapInfo.BeatmapSet.OnlineID = reader.GetInt32(0);
beatmapInfo.OnlineID = reader.GetInt32(1);
if (beatmapInfo.Metadata != null)
beatmapInfo.Metadata.AuthorID = reader.GetInt32(3);

View File

@ -0,0 +1,25 @@
// 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 Newtonsoft.Json;
namespace osu.Game.Beatmaps
{
/// <summary>
/// Contains information about the current hype status of a beatmap set.
/// </summary>
public class BeatmapSetHypeStatus
{
/// <summary>
/// The current number of hypes that the set has received.
/// </summary>
[JsonProperty(@"current")]
public int Current { get; set; }
/// <summary>
/// The number of hypes required so that the set is eligible for nomination.
/// </summary>
[JsonProperty(@"required")]
public int Required { get; set; }
}
}

View File

@ -16,12 +16,13 @@ namespace osu.Game.Beatmaps
{
public int ID { get; set; }
private int? onlineBeatmapSetID;
private int? onlineID;
public int? OnlineBeatmapSetID
[Column("OnlineBeatmapSetID")]
public int? OnlineID
{
get => onlineBeatmapSetID;
set => onlineBeatmapSetID = value > 0 ? value : null;
get => onlineID;
set => onlineID = value > 0 ? value : null;
}
public DateTimeOffset DateAdded { get; set; }
@ -38,7 +39,7 @@ namespace osu.Game.Beatmaps
/// <summary>
/// The maximum star difficulty of all beatmaps in this set.
/// </summary>
public double MaxStarDifficulty => Beatmaps?.Max(b => b.StarDifficulty) ?? 0;
public double MaxStarDifficulty => Beatmaps?.Max(b => b.StarRating) ?? 0;
/// <summary>
/// The maximum playable length in milliseconds of all beatmaps in this set.
@ -74,8 +75,8 @@ namespace osu.Game.Beatmaps
if (ID != 0 && other.ID != 0)
return ID == other.ID;
if (OnlineBeatmapSetID.HasValue && other.OnlineBeatmapSetID.HasValue)
return OnlineBeatmapSetID == other.OnlineBeatmapSetID;
if (OnlineID.HasValue && other.OnlineID.HasValue)
return OnlineID == other.OnlineID;
if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash))
return Hash == other.Hash;
@ -85,7 +86,7 @@ namespace osu.Game.Beatmaps
#region Implementation of IHasOnlineID
public int OnlineID => OnlineBeatmapSetID ?? -1;
int IHasOnlineID<int>.OnlineID => OnlineID ?? -1;
#endregion

View File

@ -0,0 +1,25 @@
// 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 Newtonsoft.Json;
namespace osu.Game.Beatmaps
{
/// <summary>
/// Contains information about the current nomination status of a beatmap set.
/// </summary>
public class BeatmapSetNominationStatus
{
/// <summary>
/// The current number of nominations that the set has received.
/// </summary>
[JsonProperty(@"current")]
public int Current { get; set; }
/// <summary>
/// The number of nominations required so that the map is eligible for qualification.
/// </summary>
[JsonProperty(@"required")]
public int Required { get; set; }
}
}

View File

@ -64,7 +64,7 @@ namespace osu.Game.Beatmaps
BeatmapInfo beatmapInfo = beatmaps.Where(b => b.Ruleset.Equals(r)).OrderBy(b =>
{
double difference = b.StarDifficulty - recommendation;
double difference = b.StarRating - recommendation;
return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder
}).FirstOrDefault();

View File

@ -1,13 +1,17 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps.Drawables.Cards.Buttons;
using osu.Game.Beatmaps.Drawables.Cards.Statistics;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@ -19,6 +23,7 @@ using osuTK;
using osu.Game.Overlays.BeatmapListing.Panels;
using osu.Game.Resources.Localisation.Web;
using osuTK.Graphics;
using DownloadButton = osu.Game.Beatmaps.Drawables.Cards.Buttons.DownloadButton;
namespace osu.Game.Beatmaps.Drawables.Cards
{
@ -31,15 +36,19 @@ namespace osu.Game.Beatmaps.Drawables.Cards
private const float corner_radius = 10;
private readonly APIBeatmapSet beatmapSet;
private readonly Bindable<BeatmapSetFavouriteState> favouriteState;
private UpdateableOnlineBeatmapSetCover leftCover;
private FillFlowContainer iconArea;
private FillFlowContainer leftIconArea;
private Container rightButtonArea;
private Container mainContent;
private BeatmapCardContentBackground mainContentBackground;
private GridContainer titleContainer;
private GridContainer artistContainer;
private FillFlowContainer<BeatmapCardStatistic> statisticsContainer;
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
@ -48,6 +57,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
: base(HoverSampleSet.Submit)
{
this.beatmapSet = beatmapSet;
favouriteState = new Bindable<BeatmapSetFavouriteState>(new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount));
}
[BackgroundDependencyLoader]
@ -76,7 +86,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
RelativeSizeAxes = Axes.Both,
OnlineInfo = beatmapSet
},
iconArea = new FillFlowContainer
leftIconArea = new FillFlowContainer
{
Margin = new MarginPadding(5),
AutoSizeAxes = Axes.Both,
@ -85,6 +95,27 @@ namespace osu.Game.Beatmaps.Drawables.Cards
}
}
},
rightButtonArea = new Container
{
Name = @"Right (button) area",
Width = 30,
RelativeSizeAxes = Axes.Y,
Origin = Anchor.TopRight,
Anchor = Anchor.TopRight,
Child = new FillFlowContainer<BeatmapCardIconButton>
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 14),
Children = new BeatmapCardIconButton[]
{
new FavouriteButton(beatmapSet) { Current = favouriteState },
new DownloadButton(beatmapSet)
}
}
},
mainContent = new Container
{
Name = @"Main content",
@ -176,6 +207,15 @@ namespace osu.Game.Beatmaps.Drawables.Cards
d.AddText("mapped by ", t => t.Colour = colourProvider.Content2);
d.AddUserLink(beatmapSet.Author);
}),
statisticsContainer = new FillFlowContainer<BeatmapCardStatistic>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Alpha = 0,
ChildrenEnumerable = createStatistics()
}
}
},
new FillFlowContainer
@ -214,10 +254,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
};
if (beatmapSet.HasVideo)
iconArea.Add(new IconPill(FontAwesome.Solid.Film));
leftIconArea.Add(new IconPill(FontAwesome.Solid.Film));
if (beatmapSet.HasStoryboard)
iconArea.Add(new IconPill(FontAwesome.Solid.Image));
leftIconArea.Add(new IconPill(FontAwesome.Solid.Image));
if (beatmapSet.HasExplicitContent)
{
@ -265,6 +305,24 @@ namespace osu.Game.Beatmaps.Drawables.Cards
return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist);
}
private IEnumerable<BeatmapCardStatistic> createStatistics()
{
if (beatmapSet.HypeStatus != null)
yield return new HypesStatistic(beatmapSet.HypeStatus);
// web does not show nominations unless hypes are also present.
// see: https://github.com/ppy/osu-web/blob/8ed7d071fd1d3eaa7e43cf0e4ff55ca2fef9c07c/resources/assets/lib/beatmapset-panel.tsx#L443
if (beatmapSet.HypeStatus != null && beatmapSet.NominationStatus != null)
yield return new NominationsStatistic(beatmapSet.NominationStatus);
yield return new FavouritesStatistic(beatmapSet) { Current = favouriteState };
yield return new PlayCountStatistic(beatmapSet);
var dateStatistic = BeatmapCardDateStatistic.CreateFor(beatmapSet);
if (dateStatistic != null)
yield return dateStatistic;
}
private void updateState()
{
float targetWidth = width - height;
@ -275,6 +333,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards
mainContentBackground.Dimmed.Value = IsHovered;
leftCover.FadeColour(IsHovered ? OsuColour.Gray(0.2f) : Color4.White, TRANSITION_DURATION, Easing.OutQuint);
statisticsContainer.FadeTo(IsHovered ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
rightButtonArea.FadeTo(IsHovered ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
}
}
}

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 osu.Game.Beatmaps.Drawables.Cards.Buttons;
using osu.Game.Beatmaps.Drawables.Cards.Statistics;
namespace osu.Game.Beatmaps.Drawables.Cards
{
/// <summary>
/// Stores the current favourite state of a beatmap set.
/// Used to coordinate between <see cref="FavouriteButton"/> and <see cref="FavouritesStatistic"/>.
/// </summary>
public readonly struct BeatmapSetFavouriteState
{
/// <summary>
/// Whether the currently logged-in user has favourited this beatmap.
/// </summary>
public bool Favourited { get; }
/// <summary>
/// The number of favourites that the beatmap set has received, including the currently logged-in user.
/// </summary>
public int FavouriteCount { get; }
public BeatmapSetFavouriteState(bool favourited, int favouriteCount)
{
Favourited = favourited;
FavouriteCount = favouriteCount;
}
}
}

View File

@ -0,0 +1,46 @@
// 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.Sprites;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
{
public abstract class BeatmapCardIconButton : OsuHoverContainer
{
protected readonly SpriteIcon Icon;
private float size;
public new float Size
{
get => size;
set
{
size = value;
Icon.Size = new Vector2(size);
}
}
protected BeatmapCardIconButton()
{
Add(Icon = new SpriteIcon());
AutoSizeAxes = Axes.Both;
Size = 12;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Anchor = Origin = Anchor.Centre;
IdleColour = colourProvider.Light1;
HoverColour = colourProvider.Content1;
}
}
}

View File

@ -0,0 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
{
public class DownloadButton : BeatmapCardIconButton
{
public DownloadButton(APIBeatmapSet beatmapSet)
{
Icon.Icon = FontAwesome.Solid.FileDownload;
}
// TODO: implement behaviour
}
}

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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Framework.Logging;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
{
public class FavouriteButton : BeatmapCardIconButton, IHasCurrentValue<BeatmapSetFavouriteState>
{
private readonly BindableWithCurrent<BeatmapSetFavouriteState> current;
public Bindable<BeatmapSetFavouriteState> Current
{
get => current.Current;
set => current.Current = value;
}
private readonly APIBeatmapSet beatmapSet;
private PostBeatmapFavouriteRequest favouriteRequest;
[Resolved]
private IAPIProvider api { get; set; }
public FavouriteButton(APIBeatmapSet beatmapSet)
{
current = new BindableWithCurrent<BeatmapSetFavouriteState>(new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount));
this.beatmapSet = beatmapSet;
}
protected override void LoadComplete()
{
base.LoadComplete();
Action = toggleFavouriteStatus;
current.BindValueChanged(_ => updateState(), true);
}
private void toggleFavouriteStatus()
{
var actionType = current.Value.Favourited ? BeatmapFavouriteAction.UnFavourite : BeatmapFavouriteAction.Favourite;
favouriteRequest?.Cancel();
favouriteRequest = new PostBeatmapFavouriteRequest(beatmapSet.OnlineID, actionType);
Enabled.Value = false;
favouriteRequest.Success += () =>
{
bool favourited = actionType == BeatmapFavouriteAction.Favourite;
current.Value = new BeatmapSetFavouriteState(favourited, current.Value.FavouriteCount + (favourited ? 1 : -1));
Enabled.Value = true;
};
favouriteRequest.Failure += e =>
{
Logger.Error(e, $"Failed to {actionType.ToString().ToLower()} beatmap: {e.Message}");
Enabled.Value = true;
};
api.Queue(favouriteRequest);
}
private void updateState()
{
if (current.Value.Favourited)
{
Icon.Icon = FontAwesome.Solid.Heart;
TooltipText = BeatmapsetsStrings.ShowDetailsUnfavourite;
}
else
{
Icon.Icon = FontAwesome.Regular.Heart;
TooltipText = BeatmapsetsStrings.ShowDetailsFavourite;
}
}
}
}

View File

@ -0,0 +1,55 @@
// 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.
#nullable enable
using System;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
{
public class BeatmapCardDateStatistic : BeatmapCardStatistic
{
private readonly DateTimeOffset dateTime;
private BeatmapCardDateStatistic(DateTimeOffset dateTime)
{
this.dateTime = dateTime;
Icon = FontAwesome.Regular.CheckCircle;
Text = dateTime.ToLocalisableString(@"d MMM yyyy");
}
public override object TooltipContent => dateTime;
public override ITooltip GetCustomTooltip() => new DateTooltip();
public static BeatmapCardDateStatistic? CreateFor(IBeatmapSetOnlineInfo beatmapSetInfo)
{
var displayDate = displayDateFor(beatmapSetInfo);
if (displayDate == null)
return null;
return new BeatmapCardDateStatistic(displayDate.Value);
}
private static DateTimeOffset? displayDateFor(IBeatmapSetOnlineInfo beatmapSetInfo)
{
// reference: https://github.com/ppy/osu-web/blob/ef432c11719fd1207bec5f9194b04f0033bdf02c/resources/assets/lib/beatmapset-panel.tsx#L36-L44
switch (beatmapSetInfo.Status)
{
case BeatmapSetOnlineStatus.Ranked:
case BeatmapSetOnlineStatus.Approved:
case BeatmapSetOnlineStatus.Loved:
case BeatmapSetOnlineStatus.Qualified:
return beatmapSetInfo.Ranked;
default:
return beatmapSetInfo.LastUpdated;
}
}
}
}

View File

@ -0,0 +1,80 @@
// 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.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
{
/// <summary>
/// A single statistic shown on a beatmap card.
/// </summary>
public abstract class BeatmapCardStatistic : CompositeDrawable, IHasTooltip, IHasCustomTooltip
{
protected IconUsage Icon
{
get => spriteIcon.Icon;
set => spriteIcon.Icon = value;
}
protected LocalisableString Text
{
get => spriteText.Text;
set => spriteText.Text = value;
}
public LocalisableString TooltipText { get; protected set; }
private readonly SpriteIcon spriteIcon;
private readonly OsuSpriteText spriteText;
protected BeatmapCardStatistic()
{
AutoSizeAxes = Axes.Both;
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5, 0),
Children = new Drawable[]
{
spriteIcon = new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(10),
Margin = new MarginPadding { Top = 1 }
},
spriteText = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.Default.With(size: 14)
}
}
};
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
spriteIcon.Colour = colourProvider.Content2;
}
#region Tooltip implementation
public virtual ITooltip GetCustomTooltip() => null;
public virtual object TooltipContent => null;
#endregion
}
}

View File

@ -0,0 +1,44 @@
// 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 Humanizer;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
{
/// <summary>
/// Shows the number of favourites that a beatmap set has received.
/// </summary>
public class FavouritesStatistic : BeatmapCardStatistic, IHasCurrentValue<BeatmapSetFavouriteState>
{
private readonly BindableWithCurrent<BeatmapSetFavouriteState> current;
public Bindable<BeatmapSetFavouriteState> Current
{
get => current.Current;
set => current.Current = value;
}
public FavouritesStatistic(IBeatmapSetOnlineInfo onlineInfo)
{
current = new BindableWithCurrent<BeatmapSetFavouriteState>(new BeatmapSetFavouriteState(onlineInfo.HasFavourited, onlineInfo.FavouriteCount));
}
protected override void LoadComplete()
{
base.LoadComplete();
current.BindValueChanged(_ => updateState(), true);
}
private void updateState()
{
Icon = current.Value.Favourited ? FontAwesome.Solid.Heart : FontAwesome.Regular.Heart;
Text = current.Value.FavouriteCount.ToMetric(decimals: 1);
TooltipText = BeatmapsStrings.PanelFavourites(current.Value.FavouriteCount.ToLocalisableString(@"N0"));
}
}
}

View File

@ -0,0 +1,22 @@
// 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.Extensions.LocalisationExtensions;
using osu.Framework.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
{
/// <summary>
/// Shows the number of current hypes that a map has received, as well as the number of hypes required for nomination.
/// </summary>
public class HypesStatistic : BeatmapCardStatistic
{
public HypesStatistic(BeatmapSetHypeStatus hypeStatus)
{
Icon = FontAwesome.Solid.Bullhorn;
Text = hypeStatus.Current.ToLocalisableString();
TooltipText = BeatmapsStrings.HypeRequiredText(hypeStatus.Current.ToLocalisableString(), hypeStatus.Required.ToLocalisableString());
}
}
}

View File

@ -0,0 +1,22 @@
// 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.Extensions.LocalisationExtensions;
using osu.Framework.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
{
/// <summary>
/// Shows the number of current nominations that a map has received, as well as the number of nominations required for qualification.
/// </summary>
public class NominationsStatistic : BeatmapCardStatistic
{
public NominationsStatistic(BeatmapSetNominationStatus nominationStatus)
{
Icon = FontAwesome.Solid.ThumbsUp;
Text = nominationStatus.Current.ToLocalisableString();
TooltipText = BeatmapsStrings.NominationsRequiredText(nominationStatus.Current.ToLocalisableString(), nominationStatus.Required.ToLocalisableString());
}
}
}

View File

@ -0,0 +1,23 @@
// 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 Humanizer;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Beatmaps.Drawables.Cards.Statistics
{
/// <summary>
/// Shows the number of times the given beatmap set has been played.
/// </summary>
public class PlayCountStatistic : BeatmapCardStatistic
{
public PlayCountStatistic(IBeatmapSetOnlineInfo onlineInfo)
{
Icon = FontAwesome.Regular.PlayCircle;
Text = onlineInfo.PlayCount.ToMetric(decimals: 1);
TooltipText = BeatmapsStrings.PanelPlaycount(onlineInfo.PlayCount.ToLocalisableString(@"N0"));
}
}
}

View File

@ -251,7 +251,7 @@ namespace osu.Game.Beatmaps.Formats
break;
case @"Version":
beatmap.BeatmapInfo.Version = pair.Value;
beatmap.BeatmapInfo.DifficultyName = pair.Value;
break;
case @"Source":
@ -263,11 +263,11 @@ namespace osu.Game.Beatmaps.Formats
break;
case @"BeatmapID":
beatmap.BeatmapInfo.OnlineBeatmapID = Parsing.ParseInt(pair.Value);
beatmap.BeatmapInfo.OnlineID = Parsing.ParseInt(pair.Value);
break;
case @"BeatmapSetID":
beatmap.BeatmapInfo.BeatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = Parsing.ParseInt(pair.Value) };
beatmap.BeatmapInfo.BeatmapSet = new BeatmapSetInfo { OnlineID = Parsing.ParseInt(pair.Value) };
break;
}
}

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