1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-15 05:52:54 +08:00

Merge branch 'master' into osu-target-mod

This commit is contained in:
Bartłomiej Dach 2021-06-30 20:27:55 +02:00
commit c0613a3e4f
139 changed files with 3127 additions and 1210 deletions

View File

@ -1,3 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Realm />
<Realm DisableAnalytics="true" />
</Weavers>

View File

@ -52,7 +52,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.618.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.622.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.630.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

@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mania.Configuration;
@ -47,7 +48,7 @@ namespace osu.Game.Rulesets.Mania
private class TimeSlider : OsuSliderBar<double>
{
public override string TooltipText => Current.Value.ToString("N0") + "ms";
public override LocalisableString TooltipText => Current.Value.ToString(@"N0") + "ms";
}
}
}

View File

@ -37,11 +37,13 @@ namespace osu.Game.Rulesets.Osu.Tests
private readonly BindableBool snakingIn = new BindableBool();
private readonly BindableBool snakingOut = new BindableBool();
private IBeatmap beatmap;
private const double duration_of_span = 3605;
private const double fade_in_modifier = -1200;
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
=> new ClockBackedTestWorkingBeatmap(this.beatmap = beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
[BackgroundDependencyLoader]
private void load(RulesetConfigCache configCache)
@ -51,8 +53,16 @@ namespace osu.Game.Rulesets.Osu.Tests
config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut);
}
private Slider slider;
private DrawableSlider drawableSlider;
[SetUp]
public void Setup() => Schedule(() =>
{
slider = null;
drawableSlider = null;
});
[SetUpSteps]
public override void SetUpSteps()
{
@ -67,21 +77,19 @@ namespace osu.Game.Rulesets.Osu.Tests
base.SetUpSteps();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
double startTime = hitObjects[sliderIndex].StartTime;
addSeekStep(startTime);
retrieveDrawableSlider((Slider)hitObjects[sliderIndex]);
retrieveSlider(sliderIndex);
setSnaking(true);
ensureSnakingIn(startTime + fade_in_modifier);
addEnsureSnakingInSteps(() => slider.StartTime + fade_in_modifier);
for (int i = 0; i < sliderIndex; i++)
{
// non-final repeats should not snake out
ensureNoSnakingOut(startTime, i);
addEnsureNoSnakingOutStep(() => slider.StartTime, i);
}
// final repeat should snake out
ensureSnakingOut(startTime, sliderIndex);
addEnsureSnakingOutSteps(() => slider.StartTime, sliderIndex);
}
[TestCase(0)]
@ -93,17 +101,15 @@ namespace osu.Game.Rulesets.Osu.Tests
base.SetUpSteps();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
double startTime = hitObjects[sliderIndex].StartTime;
addSeekStep(startTime);
retrieveDrawableSlider((Slider)hitObjects[sliderIndex]);
retrieveSlider(sliderIndex);
setSnaking(false);
ensureNoSnakingIn(startTime + fade_in_modifier);
addEnsureNoSnakingInSteps(() => slider.StartTime + fade_in_modifier);
for (int i = 0; i <= sliderIndex; i++)
{
// no snaking out ever, including final repeat
ensureNoSnakingOut(startTime, i);
addEnsureNoSnakingOutStep(() => slider.StartTime, i);
}
}
@ -116,7 +122,7 @@ namespace osu.Game.Rulesets.Osu.Tests
// repeat might have a chance to update its position depending on where in the frame its hit,
// so some leniency is allowed here instead of checking strict equality
checkPositionChange(16600, sliderRepeat, positionAlmostSame);
addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionAlmostSame);
}
[Test]
@ -126,38 +132,41 @@ namespace osu.Game.Rulesets.Osu.Tests
setSnaking(true);
base.SetUpSteps();
checkPositionChange(16600, sliderRepeat, positionDecreased);
addCheckPositionChangeSteps(() => 16600, getSliderRepeat, positionDecreased);
}
private void retrieveDrawableSlider(Slider slider) => AddUntilStep($"retrieve slider @ {slider.StartTime}", () =>
(drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null);
private void ensureSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionIncreased);
private void ensureNoSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionRemainsSame);
private void ensureSnakingOut(double startTime, int repeatIndex)
private void retrieveSlider(int index)
{
var repeatTime = timeAtRepeat(startTime, repeatIndex);
AddStep("retrieve slider at index", () => slider = (Slider)beatmap.HitObjects[index]);
addSeekStep(() => slider);
AddUntilStep("retrieve drawable slider", () =>
(drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null);
}
private void addEnsureSnakingInSteps(Func<double> startTime) => addCheckPositionChangeSteps(startTime, getSliderEnd, positionIncreased);
private void addEnsureNoSnakingInSteps(Func<double> startTime) => addCheckPositionChangeSteps(startTime, getSliderEnd, positionRemainsSame);
private void addEnsureSnakingOutSteps(Func<double> startTime, int repeatIndex)
{
if (repeatIndex % 2 == 0)
checkPositionChange(repeatTime, sliderStart, positionIncreased);
addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), getSliderStart, positionIncreased);
else
checkPositionChange(repeatTime, sliderEnd, positionDecreased);
addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), getSliderEnd, positionDecreased);
}
private void ensureNoSnakingOut(double startTime, int repeatIndex) =>
checkPositionChange(timeAtRepeat(startTime, repeatIndex), positionAtRepeat(repeatIndex), positionRemainsSame);
private void addEnsureNoSnakingOutStep(Func<double> startTime, int repeatIndex)
=> addCheckPositionChangeSteps(timeAtRepeat(startTime, repeatIndex), positionAtRepeat(repeatIndex), positionRemainsSame);
private double timeAtRepeat(double startTime, int repeatIndex) => startTime + 100 + duration_of_span * repeatIndex;
private Func<Vector2> positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? (Func<Vector2>)sliderStart : sliderEnd;
private Func<double> timeAtRepeat(Func<double> startTime, int repeatIndex) => () => startTime() + 100 + duration_of_span * repeatIndex;
private Func<Vector2> positionAtRepeat(int repeatIndex) => repeatIndex % 2 == 0 ? (Func<Vector2>)getSliderStart : getSliderEnd;
private List<Vector2> sliderCurve => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve;
private Vector2 sliderStart() => sliderCurve.First();
private Vector2 sliderEnd() => sliderCurve.Last();
private List<Vector2> getSliderCurve() => ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve;
private Vector2 getSliderStart() => getSliderCurve().First();
private Vector2 getSliderEnd() => getSliderCurve().Last();
private Vector2 sliderRepeat()
private Vector2 getSliderRepeat()
{
var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == hitObjects[1]);
var drawable = Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == beatmap.HitObjects[1]);
var repeat = drawable.ChildrenOfType<Container<DrawableSliderRepeat>>().First().Children.First();
return repeat.Position;
}
@ -167,7 +176,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private bool positionDecreased(Vector2 previous, Vector2 current) => current.X < previous.X && current.Y < previous.Y;
private bool positionAlmostSame(Vector2 previous, Vector2 current) => Precision.AlmostEquals(previous, current, 1);
private void checkPositionChange(double startTime, Func<Vector2> positionToCheck, Func<Vector2, Vector2, bool> positionAssertion)
private void addCheckPositionChangeSteps(Func<double> startTime, Func<Vector2> positionToCheck, Func<Vector2, Vector2, bool> positionAssertion)
{
Vector2 previousPosition = Vector2.Zero;
@ -176,7 +185,7 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(startTime);
AddStep($"save {positionDescription} position", () => previousPosition = positionToCheck.Invoke());
addSeekStep(startTime + 100);
addSeekStep(() => startTime() + 100);
AddAssert($"{positionDescription} {assertionDescription}", () =>
{
var currentPosition = positionToCheck.Invoke();
@ -193,19 +202,21 @@ namespace osu.Game.Rulesets.Osu.Tests
});
}
private void addSeekStep(double time)
private void addSeekStep(Func<Slider> slider)
{
AddStep($"seek to {time}", () => MusicController.SeekTo(time));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
AddStep("seek to slider", () => Player.GameplayClockContainer.Seek(slider().StartTime));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(slider().StartTime, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
private void addSeekStep(Func<double> time)
{
HitObjects = hitObjects
};
AddStep("seek to time", () => Player.GameplayClockContainer.Seek(time()));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time(), Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
private readonly List<HitObject> hitObjects = new List<HitObject>
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap { HitObjects = createHitObjects() };
private static List<HitObject> createHitObjects() => new List<HitObject>
{
new Slider
{

View File

@ -217,7 +217,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void addSeekStep(double time)
{
AddStep($"seek to {time}", () => MusicController.SeekTo(time));
AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
@ -283,6 +284,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
}
public string TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty;
public LocalisableString TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty;
}
}

View File

@ -19,7 +19,9 @@ using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Scoring;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Scores.IO;
using osu.Game.Users;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
@ -185,13 +187,62 @@ namespace osu.Game.Tests.Beatmaps.IO
}
}
private string hashFile(string filename)
[Test]
public async Task TestImportThenImportWithChangedHashedFile()
{
using (var s = File.OpenRead(filename))
return s.ComputeMD5Hash();
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
{
try
{
var osu = LoadOsuIntoHost(host);
var temp = TestResources.GetTestBeatmapForImport();
string extractedFolder = $"{temp}_extracted";
Directory.CreateDirectory(extractedFolder);
try
{
var imported = await LoadOszIntoOsu(osu);
await createScoreForBeatmap(osu, imported.Beatmaps.First());
using (var zip = ZipArchive.Open(temp))
zip.WriteToDirectory(extractedFolder);
// arbitrary write to hashed file
// this triggers the special BeatmapManager.PreImport deletion/replacement flow.
using (var sw = new FileInfo(Directory.GetFiles(extractedFolder, "*.osu").First()).AppendText())
await sw.WriteLineAsync("// changed");
using (var zip = ZipArchive.Create())
{
zip.AddAllFromDirectory(extractedFolder);
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
}
var importedSecondTime = await osu.Dependencies.Get<BeatmapManager>().Import(new ImportTask(temp));
ensureLoaded(osu);
// check the newly "imported" beatmap is not the original.
Assert.IsTrue(imported.ID != importedSecondTime.ID);
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID);
}
finally
{
Directory.Delete(extractedFolder, true);
}
}
finally
{
host.Exit();
}
}
}
[Test]
[Ignore("intentionally broken by import optimisations")]
public async Task TestImportThenImportWithChangedFile()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
@ -294,6 +345,7 @@ namespace osu.Game.Tests.Beatmaps.IO
}
[Test]
[Ignore("intentionally broken by import optimisations")]
public async Task TestImportCorruptThenImport()
{
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
@ -439,12 +491,11 @@ namespace osu.Game.Tests.Beatmaps.IO
}
}
[TestCase(true)]
[TestCase(false)]
public async Task TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set)
[Test]
public async Task TestImportThenDeleteThenImportWithOnlineIDsMissing()
{
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}-{set}"))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}"))
{
try
{
@ -452,10 +503,8 @@ namespace osu.Game.Tests.Beatmaps.IO
var imported = await LoadOszIntoOsu(osu);
if (set)
imported.OnlineBeatmapSetID = 1234;
else
imported.Beatmaps.First().OnlineBeatmapID = 1234;
foreach (var b in imported.Beatmaps)
b.OnlineBeatmapID = null;
osu.Dependencies.Get<BeatmapManager>().Update(imported);
@ -895,7 +944,17 @@ namespace osu.Game.Tests.Beatmaps.IO
Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending);
}
private void checkBeatmapSetCount(OsuGameBase osu, int expected, bool includeDeletePending = false)
private static Task createScoreForBeatmap(OsuGameBase osu, BeatmapInfo beatmap)
{
return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo
{
OnlineScoreID = 2,
Beatmap = beatmap,
BeatmapInfoID = beatmap.ID
}, new ImportScoreTest.TestArchiveReader());
}
private static void checkBeatmapSetCount(OsuGameBase osu, int expected, bool includeDeletePending = false)
{
var manager = osu.Dependencies.Get<BeatmapManager>();
@ -904,12 +963,18 @@ namespace osu.Game.Tests.Beatmaps.IO
: manager.GetAllUsableBeatmapSets().Count);
}
private void checkBeatmapCount(OsuGameBase osu, int expected)
private static string hashFile(string filename)
{
using (var s = File.OpenRead(filename))
return s.ComputeMD5Hash();
}
private static void checkBeatmapCount(OsuGameBase osu, int expected)
{
Assert.AreEqual(expected, osu.Dependencies.Get<BeatmapManager>().QueryBeatmaps(_ => true).ToList().Count);
}
private void checkSingleReferencedFileCount(OsuGameBase osu, int expected)
private static void checkSingleReferencedFileCount(OsuGameBase osu, int expected)
{
Assert.AreEqual(expected, osu.Dependencies.Get<FileStore>().QueryFiles(f => f.ReferenceCount == 1).Count());
}

View File

@ -0,0 +1,105 @@
// 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.Containers;
using osu.Framework.Testing;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Chat;
using osu.Game.Tests.Visual;
using osu.Game.Users;
namespace osu.Game.Tests.Chat
{
[HeadlessTest]
public class TestSceneChannelManager : OsuTestScene
{
private ChannelManager channelManager;
private int currentMessageId;
[SetUp]
public void Setup() => Schedule(() =>
{
var container = new ChannelManagerContainer();
Child = container;
channelManager = container.ChannelManager;
});
[SetUpSteps]
public void SetUpSteps()
{
AddStep("register request handling", () =>
{
currentMessageId = 0;
((DummyAPIAccess)API).HandleRequest = req =>
{
switch (req)
{
case JoinChannelRequest joinChannel:
joinChannel.TriggerSuccess();
return true;
case PostMessageRequest postMessage:
postMessage.TriggerSuccess(new Message(++currentMessageId)
{
IsAction = postMessage.Message.IsAction,
ChannelId = postMessage.Message.ChannelId,
Content = postMessage.Message.Content,
Links = postMessage.Message.Links,
Timestamp = postMessage.Message.Timestamp,
Sender = postMessage.Message.Sender
});
return true;
}
return false;
};
});
}
[Test]
public void TestCommandsPostedToCorrectChannelWhenNotCurrent()
{
Channel channel1 = null;
Channel channel2 = null;
AddStep("join 2 rooms", () =>
{
channelManager.JoinChannel(channel1 = createChannel(1, ChannelType.Public));
channelManager.JoinChannel(channel2 = createChannel(2, ChannelType.Public));
});
AddStep("select channel 1", () => channelManager.CurrentChannel.Value = channel1);
AddStep("post /me command to channel 2", () => channelManager.PostCommand("me dances", channel2));
AddAssert("/me command received by channel 2", () => channel2.Messages.Last().Content == "dances");
AddStep("post /np command to channel 2", () => channelManager.PostCommand("np", channel2));
AddAssert("/np command received by channel 2", () => channel2.Messages.Last().Content.Contains("is listening to"));
}
private Channel createChannel(int id, ChannelType type) => new Channel(new User())
{
Id = id,
Name = $"Channel {id}",
Topic = $"Topic of channel {id} with type {type}",
Type = type,
};
private class ChannelManagerContainer : CompositeDrawable
{
[Cached]
public ChannelManager ChannelManager { get; } = new ChannelManager();
public ChannelManagerContainer()
{
InternalChild = ChannelManager;
}
}
}
}

View File

@ -0,0 +1,241 @@
// 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.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckFewHitsoundsTest
{
private CheckFewHitsounds check;
private List<HitSampleInfo> notHitsounded;
private List<HitSampleInfo> hitsounded;
[SetUp]
public void Setup()
{
check = new CheckFewHitsounds();
notHitsounded = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
hitsounded = new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL),
new HitSampleInfo(HitSampleInfo.HIT_FINISH)
};
}
[Test]
public void TestHitsounded()
{
var hitObjects = new List<HitObject>();
for (int i = 0; i < 16; ++i)
{
var samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
if ((i + 1) % 2 == 0)
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
if ((i + 1) % 3 == 0)
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE));
if ((i + 1) % 4 == 0)
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
}
assertOk(hitObjects);
}
[Test]
public void TestHitsoundedWithBreak()
{
var hitObjects = new List<HitObject>();
for (int i = 0; i < 32; ++i)
{
var samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
if ((i + 1) % 2 == 0)
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
if ((i + 1) % 3 == 0)
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE));
if ((i + 1) % 4 == 0)
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
// Leaves a gap in which no hitsounds exist or can be added, and so shouldn't be an issue.
if (i > 8 && i < 24)
continue;
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
}
assertOk(hitObjects);
}
[Test]
public void TestLightlyHitsounded()
{
var hitObjects = new List<HitObject>();
for (int i = 0; i < 30; ++i)
{
var samples = i % 8 == 0 ? hitsounded : notHitsounded;
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
}
assertLongPeriodNegligible(hitObjects, count: 3);
}
[Test]
public void TestRarelyHitsounded()
{
var hitObjects = new List<HitObject>();
for (int i = 0; i < 30; ++i)
{
var samples = (i == 0 || i == 15) ? hitsounded : notHitsounded;
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
}
// Should prompt one warning between 1st and 16th, and another between 16th and 31st.
assertLongPeriodWarning(hitObjects, count: 2);
}
[Test]
public void TestExtremelyRarelyHitsounded()
{
var hitObjects = new List<HitObject>();
for (int i = 0; i < 80; ++i)
{
var samples = i == 40 ? hitsounded : notHitsounded;
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
}
// Should prompt one problem between 1st and 41st, and another between 41st and 81st.
assertLongPeriodProblem(hitObjects, count: 2);
}
[Test]
public void TestNotHitsounded()
{
var hitObjects = new List<HitObject>();
for (int i = 0; i < 20; ++i)
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = notHitsounded });
assertNoHitsounds(hitObjects);
}
[Test]
public void TestNestedObjectsHitsounded()
{
var ticks = new List<HitObject>();
for (int i = 1; i < 16; ++i)
ticks.Add(new SliderTick { StartTime = 1000 * i, Samples = hitsounded });
var nested = new MockNestableHitObject(ticks.ToList(), 0, 16000)
{
Samples = hitsounded
};
nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
assertOk(new List<HitObject> { nested });
}
[Test]
public void TestNestedObjectsRarelyHitsounded()
{
var ticks = new List<HitObject>();
for (int i = 1; i < 16; ++i)
ticks.Add(new SliderTick { StartTime = 1000 * i, Samples = i == 0 ? hitsounded : notHitsounded });
var nested = new MockNestableHitObject(ticks.ToList(), 0, 16000)
{
Samples = hitsounded
};
nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
assertLongPeriodWarning(new List<HitObject> { nested });
}
[Test]
public void TestConcurrentObjects()
{
var hitObjects = new List<HitObject>();
var ticks = new List<HitObject>();
for (int i = 1; i < 10; ++i)
ticks.Add(new SliderTick { StartTime = 5000 * i, Samples = hitsounded });
var nested = new MockNestableHitObject(ticks.ToList(), 0, 50000)
{
Samples = notHitsounded
};
nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
hitObjects.Add(nested);
for (int i = 1; i <= 6; ++i)
hitObjects.Add(new HitCircle { StartTime = 10000 * i, Samples = notHitsounded });
assertOk(hitObjects);
}
private void assertOk(List<HitObject> hitObjects)
{
Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
}
private void assertLongPeriodProblem(List<HitObject> hitObjects, int count = 1)
{
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodProblem));
}
private void assertLongPeriodWarning(List<HitObject> hitObjects, int count = 1)
{
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodWarning));
}
private void assertLongPeriodNegligible(List<HitObject> hitObjects, int count = 1)
{
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodNegligible));
}
private void assertNoHitsounds(List<HitObject> hitObjects)
{
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Any(issue => issue.Template is CheckFewHitsounds.IssueTemplateNoHitsounds));
}
private BeatmapVerifierContext getContext(List<HitObject> hitObjects)
{
var beatmap = new Beatmap<HitObject> { HitObjects = hitObjects };
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
}
}
}

View File

@ -0,0 +1,289 @@
// 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.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckMutedObjectsTest
{
private CheckMutedObjects check;
private ControlPointInfo cpi;
private const int volume_regular = 50;
private const int volume_low = 15;
private const int volume_muted = 5;
[SetUp]
public void Setup()
{
check = new CheckMutedObjects();
cpi = new ControlPointInfo();
cpi.Add(0, new SampleControlPoint { SampleVolume = volume_regular });
cpi.Add(1000, new SampleControlPoint { SampleVolume = volume_low });
cpi.Add(2000, new SampleControlPoint { SampleVolume = volume_muted });
}
[Test]
public void TestNormalControlPointVolume()
{
var hitcircle = new HitCircle
{
StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
assertOk(new List<HitObject> { hitcircle });
}
[Test]
public void TestLowControlPointVolume()
{
var hitcircle = new HitCircle
{
StartTime = 1000,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
assertLowVolume(new List<HitObject> { hitcircle });
}
[Test]
public void TestMutedControlPointVolume()
{
var hitcircle = new HitCircle
{
StartTime = 2000,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
assertMuted(new List<HitObject> { hitcircle });
}
[Test]
public void TestNormalSampleVolume()
{
// The sample volume should take precedence over the control point volume.
var hitcircle = new HitCircle
{
StartTime = 2000,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
};
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
assertOk(new List<HitObject> { hitcircle });
}
[Test]
public void TestLowSampleVolume()
{
var hitcircle = new HitCircle
{
StartTime = 2000,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_low) }
};
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
assertLowVolume(new List<HitObject> { hitcircle });
}
[Test]
public void TestMutedSampleVolume()
{
var hitcircle = new HitCircle
{
StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) }
};
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
assertMuted(new List<HitObject> { hitcircle });
}
[Test]
public void TestNormalSampleVolumeSlider()
{
var sliderHead = new SliderHeadCircle
{
StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
var sliderTick = new SliderTick
{
StartTime = 250,
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick", volume: volume_muted) } // Should be fine.
};
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 500)
{
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
assertOk(new List<HitObject> { slider });
}
[Test]
public void TestMutedSampleVolumeSliderHead()
{
var sliderHead = new SliderHeadCircle
{
StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) }
};
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
var sliderTick = new SliderTick
{
StartTime = 250,
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
};
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 500)
{
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } // Applies to the tail.
};
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
assertMuted(new List<HitObject> { slider });
}
[Test]
public void TestMutedSampleVolumeSliderTail()
{
var sliderHead = new SliderHeadCircle
{
StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
var sliderTick = new SliderTick
{
StartTime = 250,
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
};
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 2500)
{
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) } // Applies to the tail.
};
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
assertMutedPassive(new List<HitObject> { slider });
}
[Test]
public void TestMutedControlPointVolumeSliderHead()
{
var sliderHead = new SliderHeadCircle
{
StartTime = 2000,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
var sliderTick = new SliderTick
{
StartTime = 2250,
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
};
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 2000, endTime: 2500)
{
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
};
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
assertMuted(new List<HitObject> { slider });
}
[Test]
public void TestMutedControlPointVolumeSliderTail()
{
var sliderHead = new SliderHeadCircle
{
StartTime = 0,
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
var sliderTick = new SliderTick
{
StartTime = 250,
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
};
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
// Ends after the 5% control point.
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 2500)
{
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
};
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
assertMutedPassive(new List<HitObject> { slider });
}
private void assertOk(List<HitObject> hitObjects)
{
Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
}
private void assertLowVolume(List<HitObject> hitObjects, int count = 1)
{
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckMutedObjects.IssueTemplateLowVolumeActive));
}
private void assertMuted(List<HitObject> hitObjects, int count = 1)
{
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckMutedObjects.IssueTemplateMutedActive));
}
private void assertMutedPassive(List<HitObject> hitObjects)
{
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Any(issue => issue.Template is CheckMutedObjects.IssueTemplateMutedPassive));
}
private BeatmapVerifierContext getContext(List<HitObject> hitObjects)
{
var beatmap = new Beatmap<HitObject>
{
ControlPointInfo = cpi,
HitObjects = hitObjects
};
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
}
}
}

View File

@ -0,0 +1,36 @@
// 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.Threading;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Tests.Editing.Checks
{
public sealed class MockNestableHitObject : HitObject, IHasDuration
{
private readonly IEnumerable<HitObject> toBeNested;
public MockNestableHitObject(IEnumerable<HitObject> toBeNested, double startTime, double endTime)
{
this.toBeNested = toBeNested;
StartTime = startTime;
EndTime = endTime;
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
foreach (var hitObject in toBeNested)
AddNested(hitObject);
}
public double EndTime { get; }
public double Duration
{
get => EndTime - StartTime;
set => throw new System.NotImplementedException();
}
}
}

View File

@ -17,7 +17,8 @@ namespace osu.Game.Tests
protected virtual TestOsuGameBase LoadOsuIntoHost(GameHost host, bool withBeatmap = false)
{
var osu = new TestOsuGameBase(withBeatmap);
Task.Run(() => host.Run(osu));
Task.Run(() => host.Run(osu))
.ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted);
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");

View File

@ -90,6 +90,20 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.Less(filterCriteria.DrainRate.Min, 6.1f);
}
[Test]
public void TestApplyOverallDifficultyQueries()
{
const string query = "od>4 easy od<8";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual("easy", filterCriteria.SearchText.Trim());
Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
Assert.Greater(filterCriteria.OverallDifficulty.Min, 4.0);
Assert.Less(filterCriteria.OverallDifficulty.Min, 4.1);
Assert.Greater(filterCriteria.OverallDifficulty.Max, 7.9);
Assert.Less(filterCriteria.OverallDifficulty.Max, 8.0);
}
[Test]
public void TestApplyBPMQueries()
{

View File

@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Tests.Visual.Multiplayer;
using osu.Game.Users;
@ -50,7 +51,10 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
AddStep("create room initially in gameplay", () =>
{
Room.RoomID.Value = null;
var newRoom = new Room();
newRoom.CopyFrom(SelectedRoom.Value);
newRoom.RoomID.Value = null;
Client.RoomSetupAction = room =>
{
room.State = MultiplayerRoomState.Playing;
@ -61,7 +65,7 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
});
};
RoomManager.CreateRoom(Room);
RoomManager.CreateRoom(newRoom);
});
AddUntilStep("wait for room join", () => Client.Room != null);

View File

@ -31,32 +31,24 @@ namespace osu.Game.Tests.OnlinePlay
}
[Test]
public void TestMasterClockStartsWhenAllPlayerClocksHaveFrames()
public void TestPlayerClocksStartWhenAllHaveFrames()
{
setWaiting(() => player1, false);
assertMasterState(false);
assertPlayerClockState(() => player1, false);
assertPlayerClockState(() => player2, false);
setWaiting(() => player2, false);
assertMasterState(true);
assertPlayerClockState(() => player1, true);
assertPlayerClockState(() => player2, true);
}
[Test]
public void TestMasterClockDoesNotStartWhenNoneReadyForMaximumDelayTime()
{
AddWaitStep($"wait {CatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction));
assertMasterState(false);
}
[Test]
public void TestMasterClockStartsWhenAnyReadyForMaximumDelayTime()
public void TestReadyPlayersStartWhenReadyForMaximumDelayTime()
{
setWaiting(() => player1, false);
AddWaitStep($"wait {CatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction));
assertMasterState(true);
assertPlayerClockState(() => player1, true);
assertPlayerClockState(() => player2, false);
}
[Test]
@ -153,9 +145,6 @@ namespace osu.Game.Tests.OnlinePlay
private void setPlayerClockTime(Func<TestSpectatorPlayerClock> playerClock, double offsetFromMaster)
=> AddStep($"set player clock {playerClock().Id} = master - {offsetFromMaster}", () => playerClock().Seek(master.CurrentTime - offsetFromMaster));
private void assertMasterState(bool running)
=> AddAssert($"master clock {(running ? "is" : "is not")} running", () => master.IsRunning == running);
private void assertCatchingUp(Func<TestSpectatorPlayerClock> playerClock, bool catchingUp) =>
AddAssert($"player clock {playerClock().Id} {(catchingUp ? "is" : "is not")} catching up", () => playerClock().IsCatchingUp == catchingUp);
@ -201,6 +190,11 @@ namespace osu.Game.Tests.OnlinePlay
private class TestManualClock : ManualClock, IAdjustableClock
{
public TestManualClock()
{
IsRunning = true;
}
public void Start() => IsRunning = true;
public void Stop() => IsRunning = false;

View File

@ -0,0 +1,89 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Rulesets;
using osu.Game.Skinning;
using osu.Game.Tests.Testing;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Rulesets
{
[HeadlessTest]
public class TestSceneRulesetSkinProvidingContainer : OsuTestScene
{
private SkinRequester requester;
protected override Ruleset CreateRuleset() => new TestSceneRulesetDependencies.TestRuleset();
[Test]
public void TestRulesetResources()
{
setupProviderStep();
AddAssert("ruleset texture retrieved via skin", () => requester.GetTexture("test-image") != null);
AddAssert("ruleset sample retrieved via skin", () => requester.GetSample(new SampleInfo("test-sample")) != null);
}
[Test]
public void TestEarlyAddedSkinRequester()
{
Texture textureOnLoad = null;
AddStep("setup provider", () =>
{
var rulesetSkinProvider = new RulesetSkinProvidingContainer(Ruleset.Value.CreateInstance(), Beatmap.Value.Beatmap, Beatmap.Value.Skin);
rulesetSkinProvider.Add(requester = new SkinRequester());
requester.OnLoadAsync += () => textureOnLoad = requester.GetTexture("test-image");
Child = rulesetSkinProvider;
});
AddAssert("requester got correct initial texture", () => textureOnLoad != null);
}
private void setupProviderStep()
{
AddStep("setup provider", () =>
{
Child = new RulesetSkinProvidingContainer(Ruleset.Value.CreateInstance(), Beatmap.Value.Beatmap, Beatmap.Value.Skin)
.WithChild(requester = new SkinRequester());
});
}
private class SkinRequester : Drawable, ISkin
{
private ISkinSource skin;
public event Action OnLoadAsync;
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
this.skin = skin;
OnLoadAsync?.Invoke();
}
public Drawable GetDrawableComponent(ISkinComponent component) => skin.GetDrawableComponent(component);
public Texture GetTexture(string componentName, WrapMode wrapModeS = default, WrapMode wrapModeT = default) => skin.GetTexture(componentName);
public ISample GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo);
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => skin.GetConfig<TLookup, TValue>(lookup);
}
}
}

View File

@ -43,7 +43,7 @@ namespace osu.Game.Tests.Scores.IO
OnlineScoreID = 12345,
};
var imported = await loadScoreIntoOsu(osu, toImport);
var imported = await LoadScoreIntoOsu(osu, toImport);
Assert.AreEqual(toImport.Rank, imported.Rank);
Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
@ -75,7 +75,7 @@ namespace osu.Game.Tests.Scores.IO
Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() },
};
var imported = await loadScoreIntoOsu(osu, toImport);
var imported = await LoadScoreIntoOsu(osu, toImport);
Assert.IsTrue(imported.Mods.Any(m => m is OsuModHardRock));
Assert.IsTrue(imported.Mods.Any(m => m is OsuModDoubleTime));
@ -105,7 +105,7 @@ namespace osu.Game.Tests.Scores.IO
}
};
var imported = await loadScoreIntoOsu(osu, toImport);
var imported = await LoadScoreIntoOsu(osu, toImport);
Assert.AreEqual(toImport.Statistics[HitResult.Perfect], imported.Statistics[HitResult.Perfect]);
Assert.AreEqual(toImport.Statistics[HitResult.Miss], imported.Statistics[HitResult.Miss]);
@ -136,7 +136,7 @@ namespace osu.Game.Tests.Scores.IO
}
};
var imported = await loadScoreIntoOsu(osu, toImport);
var imported = await LoadScoreIntoOsu(osu, toImport);
var beatmapManager = osu.Dependencies.Get<BeatmapManager>();
var scoreManager = osu.Dependencies.Get<ScoreManager>();
@ -144,7 +144,7 @@ namespace osu.Game.Tests.Scores.IO
beatmapManager.Delete(beatmapManager.QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == imported.Beatmap.ID)));
Assert.That(scoreManager.Query(s => s.ID == imported.ID).DeletePending, Is.EqualTo(true));
var secondImport = await loadScoreIntoOsu(osu, imported);
var secondImport = await LoadScoreIntoOsu(osu, imported);
Assert.That(secondImport, Is.Null);
}
finally
@ -163,7 +163,7 @@ namespace osu.Game.Tests.Scores.IO
{
var osu = LoadOsuIntoHost(host, true);
await loadScoreIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2 }, new TestArchiveReader());
await LoadScoreIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2 }, new TestArchiveReader());
var scoreManager = osu.Dependencies.Get<ScoreManager>();
@ -177,7 +177,7 @@ namespace osu.Game.Tests.Scores.IO
}
}
private async Task<ScoreInfo> loadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null)
public static async Task<ScoreInfo> LoadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null)
{
var beatmapManager = osu.Dependencies.Get<BeatmapManager>();
@ -190,7 +190,7 @@ namespace osu.Game.Tests.Scores.IO
return scoreManager.GetAllUsableScores().FirstOrDefault();
}
private class TestArchiveReader : ArchiveReader
internal class TestArchiveReader : ArchiveReader
{
public TestArchiveReader()
: base("test_archive")

View File

@ -52,7 +52,7 @@ namespace osu.Game.Tests.Testing
Dependencies.Get<TestRulesetConfigManager>() != null);
}
private class TestRuleset : Ruleset
public class TestRuleset : Ruleset
{
public override string Description => string.Empty;
public override string ShortName => string.Empty;

View File

@ -2,8 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@ -41,8 +39,6 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved]
private OsuGameBase game { get; set; }
private int nextFrame;
private BeatmapSetInfo importedBeatmap;
private int importedBeatmapId;
@ -51,8 +47,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{
base.SetUpSteps();
AddStep("reset sent frames", () => nextFrame = 0);
AddStep("import beatmap", () =>
{
importedBeatmap = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).Result;
@ -105,7 +99,8 @@ namespace osu.Game.Tests.Visual.Gameplay
waitForPlayer();
checkPaused(true);
sendFrames(1000); // send enough frames to ensure play won't be paused
// send enough frames to ensure play won't be paused
sendFrames(100);
checkPaused(false);
}
@ -114,12 +109,12 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestSpectatingDuringGameplay()
{
start();
sendFrames(300);
loadSpectatingScreen();
waitForPlayer();
AddStep("advance frame count", () => nextFrame = 300);
sendFrames();
sendFrames(300);
AddUntilStep("playing from correct point in time", () => player.ChildrenOfType<DrawableRuleset>().First().FrameStableClock.CurrentTime > 30000);
}
@ -220,11 +215,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void sendFrames(int count = 10)
{
AddStep("send frames", () =>
{
testSpectatorClient.SendFrames(streamingUser.Id, nextFrame, count);
nextFrame += count;
});
AddStep("send frames", () => testSpectatorClient.SendFrames(streamingUser.Id, count));
}
private void loadSpectatingScreen()
@ -232,14 +223,5 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("load screen", () => LoadScreen(spectatorScreen = new SoloSpectator(streamingUser)));
AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded);
}
internal class TestUserLookupCache : UserLookupCache
{
protected override Task<User> ComputeValueAsync(int lookup, CancellationToken token = default) => Task.FromResult(new User
{
Id = lookup,
Username = $"User {lookup}"
});
}
}
}

View File

@ -1,61 +0,0 @@
// 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.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
public abstract class RoomManagerTestScene : RoomTestScene
{
[Cached(Type = typeof(IRoomManager))]
protected TestRoomManager RoomManager { get; } = new TestRoomManager();
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("clear rooms", () => RoomManager.Rooms.Clear());
}
protected void AddRooms(int count, RulesetInfo ruleset = null)
{
AddStep("add rooms", () =>
{
for (int i = 0; i < count; i++)
{
var room = new Room
{
RoomID = { Value = i },
Name = { Value = $"Room {i}" },
Host = { Value = new User { Username = "Host" } },
EndDate = { Value = DateTimeOffset.Now + TimeSpan.FromSeconds(10) },
Category = { Value = i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal }
};
if (ruleset != null)
{
room.Playlist.Add(new PlaylistItem
{
Ruleset = { Value = ruleset },
Beatmap =
{
Value = new BeatmapInfo
{
Metadata = new BeatmapMetadata()
}
}
});
}
RoomManager.Rooms.Add(room);
}
});
}
}
}

View File

@ -1,35 +0,0 @@
// 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.Online.Rooms;
using osu.Game.Screens.OnlinePlay;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestRoomManager : IRoomManager
{
public event Action RoomsUpdated
{
add { }
remove { }
}
public readonly BindableList<Room> Rooms = new BindableList<Room>();
public IBindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>(true);
IBindableList<Room> IRoomManager.Rooms => Rooms;
public void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null) => Rooms.Add(room);
public void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
{
}
public void PartRoom()
{
}
}
}

View File

@ -4,17 +4,21 @@
using System;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneLoungeRoomInfo : RoomTestScene
public class TestSceneLoungeRoomInfo : OnlinePlayTestScene
{
[SetUp]
public new void Setup() => Schedule(() =>
{
SelectedRoom.Value = new Room();
Child = new RoomInfo
{
Anchor = Anchor.Centre,
@ -23,15 +27,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
};
});
public override void SetUpSteps()
{
// Todo: Temp
}
[Test]
public void TestNonSelectedRoom()
{
AddStep("set null room", () => Room.RoomID.Value = null);
AddStep("set null room", () => SelectedRoom.Value.RoomID.Value = null);
}
[Test]
@ -39,11 +38,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("set open room", () =>
{
Room.RoomID.Value = 0;
Room.Name.Value = "Room 0";
Room.Host.Value = new User { Username = "peppy", Id = 2 };
Room.EndDate.Value = DateTimeOffset.Now.AddMonths(1);
Room.Status.Value = new RoomStatusOpen();
SelectedRoom.Value.RoomID.Value = 0;
SelectedRoom.Value.Name.Value = "Room 0";
SelectedRoom.Value.Host.Value = new User { Username = "peppy", Id = 2 };
SelectedRoom.Value.EndDate.Value = DateTimeOffset.Now.AddMonths(1);
SelectedRoom.Value.Status.Value = new RoomStatusOpen();
});
}
}

View File

@ -3,24 +3,26 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Tests.Visual.OnlinePlay;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneLoungeRoomsContainer : RoomManagerTestScene
public class TestSceneLoungeRoomsContainer : OnlinePlayTestScene
{
protected new BasicTestRoomManager RoomManager => (BasicTestRoomManager)base.RoomManager;
private RoomsContainer container;
[BackgroundDependencyLoader]
private void load()
[SetUp]
public new void Setup() => Schedule(() =>
{
Child = container = new RoomsContainer
{
@ -29,12 +31,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
Width = 0.5f,
JoinRequested = joinRequested
};
}
});
[Test]
public void TestBasicListChanges()
{
AddRooms(3);
AddStep("add rooms", () => RoomManager.AddRooms(3));
AddAssert("has 3 rooms", () => container.Rooms.Count == 3);
AddStep("remove first room", () => RoomManager.Rooms.Remove(RoomManager.Rooms.FirstOrDefault()));
@ -51,7 +53,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestKeyboardNavigation()
{
AddRooms(3);
AddStep("add rooms", () => RoomManager.AddRooms(3));
AddAssert("no selection", () => checkRoomSelected(null));
@ -72,7 +74,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestClickDeselection()
{
AddRooms(1);
AddStep("add room", () => RoomManager.AddRooms(1));
AddAssert("no selection", () => checkRoomSelected(null));
@ -91,7 +93,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestStringFiltering()
{
AddRooms(4);
AddStep("add rooms", () => RoomManager.AddRooms(4));
AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4);
@ -107,21 +109,21 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestRulesetFiltering()
{
AddRooms(2, new OsuRuleset().RulesetInfo);
AddRooms(3, new CatchRuleset().RulesetInfo);
AddStep("add rooms", () => RoomManager.AddRooms(2, new OsuRuleset().RulesetInfo));
AddStep("add rooms", () => RoomManager.AddRooms(3, new CatchRuleset().RulesetInfo));
// Todo: What even is this case...?
AddStep("set empty filter criteria", () => container.Filter(null));
AddUntilStep("5 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 5);
AddStep("filter osu! rooms", () => container.Filter(new FilterCriteria { Ruleset = new OsuRuleset().RulesetInfo }));
AddUntilStep("2 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 2);
AddStep("filter catch rooms", () => container.Filter(new FilterCriteria { Ruleset = new CatchRuleset().RulesetInfo }));
AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3);
}
private bool checkRoomSelected(Room room) => Room == room;
private bool checkRoomSelected(Room room) => SelectedRoom.Value == room;
private void joinRequested(Room room) => room.Status.Value = new JoinedRoomStatus();

View File

@ -11,11 +11,12 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual.OnlinePlay;
using osuTK;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMatchBeatmapDetailArea : RoomTestScene
public class TestSceneMatchBeatmapDetailArea : OnlinePlayTestScene
{
[Resolved]
private BeatmapManager beatmapManager { get; set; }
@ -26,6 +27,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUp]
public new void Setup() => Schedule(() =>
{
SelectedRoom.Value = new Room();
Child = new MatchBeatmapDetailArea
{
Anchor = Anchor.Centre,
@ -37,9 +40,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void createNewItem()
{
Room.Playlist.Add(new PlaylistItem
SelectedRoom.Value.Playlist.Add(new PlaylistItem
{
ID = Room.Playlist.Count,
ID = SelectedRoom.Value.Playlist.Count,
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
RequiredMods =

View File

@ -7,46 +7,49 @@ using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMatchHeader : RoomTestScene
public class TestSceneMatchHeader : OnlinePlayTestScene
{
public TestSceneMatchHeader()
{
Child = new Header();
}
[SetUp]
public new void Setup() => Schedule(() =>
{
Room.Playlist.Add(new PlaylistItem
SelectedRoom.Value = new Room
{
Beatmap =
Name = { Value = "A very awesome room" },
Host = { Value = new User { Id = 2, Username = "peppy" } },
Playlist =
{
Value = new BeatmapInfo
new PlaylistItem
{
Metadata = new BeatmapMetadata
Beatmap =
{
Title = "Title",
Artist = "Artist",
AuthorString = "Author",
Value = new BeatmapInfo
{
Metadata = new BeatmapMetadata
{
Title = "Title",
Artist = "Artist",
AuthorString = "Author",
},
Version = "Version",
Ruleset = new OsuRuleset().RulesetInfo
}
},
Version = "Version",
Ruleset = new OsuRuleset().RulesetInfo
RequiredMods =
{
new OsuModDoubleTime(),
new OsuModNoFail(),
new OsuModRelax(),
}
}
},
RequiredMods =
{
new OsuModDoubleTime(),
new OsuModNoFail(),
new OsuModRelax(),
}
});
};
Room.Name.Value = "A very awesome room";
Room.Host.Value = new User { Id = 2, Username = "peppy" };
Child = new Header();
});
}
}

View File

@ -2,72 +2,74 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using Newtonsoft.Json;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMatchLeaderboard : RoomTestScene
public class TestSceneMatchLeaderboard : OnlinePlayTestScene
{
protected override bool UseOnlineAPI => true;
public TestSceneMatchLeaderboard()
{
Add(new MatchLeaderboard
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Size = new Vector2(550f, 450f),
Scope = MatchLeaderboardScope.Overall,
});
}
[BackgroundDependencyLoader]
private void load(IAPIProvider api)
private void load()
{
var req = new GetRoomScoresRequest();
req.Success += v => { };
req.Failure += _ => { };
((DummyAPIAccess)API).HandleRequest = r =>
{
switch (r)
{
case GetRoomLeaderboardRequest leaderboardRequest:
leaderboardRequest.TriggerSuccess(new APILeaderboard
{
Leaderboard = new List<APIUserScoreAggregate>
{
new APIUserScoreAggregate
{
UserID = 2,
User = new User { Id = 2, Username = "peppy" },
TotalScore = 995533,
RoomID = 3,
CompletedBeatmaps = 1,
TotalAttempts = 6,
Accuracy = 0.9851
},
new APIUserScoreAggregate
{
UserID = 1040328,
User = new User { Id = 1040328, Username = "smoogipoo" },
TotalScore = 981100,
RoomID = 3,
CompletedBeatmaps = 1,
TotalAttempts = 9,
Accuracy = 0.937
}
}
});
return true;
}
api.Queue(req);
return false;
};
}
[SetUp]
public new void Setup() => Schedule(() =>
{
Room.RoomID.Value = 3;
SelectedRoom.Value = new Room { RoomID = { Value = 3 } };
Child = new MatchLeaderboard
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Size = new Vector2(550f, 450f),
Scope = MatchLeaderboardScope.Overall,
};
});
private class GetRoomScoresRequest : APIRequest<List<RoomScore>>
{
protected override string Target => "rooms/3/leaderboard";
}
private class RoomScore
{
[JsonProperty("user")]
public User User { get; set; }
[JsonProperty("accuracy")]
public double Accuracy { get; set; }
[JsonProperty("total_score")]
public int TotalScore { get; set; }
[JsonProperty("pp")]
public double PP { get; set; }
[JsonProperty("attempts")]
public int TotalAttempts { get; set; }
[JsonProperty("completed")]
public int CompletedAttempts { get; set; }
}
}
}

View File

@ -3,74 +3,40 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Database;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual.Spectator;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiSpectatorLeaderboard : MultiplayerTestScene
{
[Cached(typeof(SpectatorClient))]
private TestSpectatorClient spectatorClient = new TestSpectatorClient();
[Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache();
protected override Container<Drawable> Content => content;
private readonly Container content;
private readonly Dictionary<int, ManualClock> clocks = new Dictionary<int, ManualClock>
{
{ PLAYER_1_ID, new ManualClock() },
{ PLAYER_2_ID, new ManualClock() }
};
public TestSceneMultiSpectatorLeaderboard()
{
base.Content.AddRange(new Drawable[]
{
spectatorClient,
lookupCache,
content = new Container { RelativeSizeAxes = Axes.Both }
});
}
private Dictionary<int, ManualClock> clocks;
private MultiSpectatorLeaderboard leaderboard;
[SetUpSteps]
public new void SetUpSteps()
{
MultiSpectatorLeaderboard leaderboard = null;
AddStep("reset", () =>
{
Clear();
foreach (var (userId, clock) in clocks)
clocks = new Dictionary<int, ManualClock>
{
spectatorClient.EndPlay(userId);
clock.CurrentTime = 0;
}
{ PLAYER_1_ID, new ManualClock() },
{ PLAYER_2_ID, new ManualClock() }
};
foreach (var (userId, _) in clocks)
SpectatorClient.StartPlay(userId, 0);
});
AddStep("create leaderboard", () =>
{
foreach (var (userId, _) in clocks)
spectatorClient.StartPlay(userId, 0);
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
var scoreProcessor = new OsuScoreProcessor();
scoreProcessor.ApplyBeatmap(playable);
@ -96,10 +62,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
// For player 2, send frames in sets of 10.
for (int i = 0; i < 100; i++)
{
spectatorClient.SendFrames(PLAYER_1_ID, i, 1);
SpectatorClient.SendFrames(PLAYER_1_ID, 1);
if (i % 10 == 0)
spectatorClient.SendFrames(PLAYER_2_ID, i, 10);
SpectatorClient.SendFrames(PLAYER_2_ID, 10);
}
});
@ -145,17 +111,5 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void assertCombo(int userId, int expectedCombo)
=> AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.Id == userId).Combo.Value == expectedCombo);
private class TestUserLookupCache : UserLookupCache
{
protected override Task<User> ComputeValueAsync(int lookup, CancellationToken token = default)
{
return Task.FromResult(new User
{
Id = lookup,
Username = $"User {lookup}"
});
}
}
}
}

View File

@ -3,31 +3,20 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Visual.Spectator;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiSpectatorScreen : MultiplayerTestScene
{
[Cached(typeof(SpectatorClient))]
private TestSpectatorClient spectatorClient = new TestSpectatorClient();
[Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache();
[Resolved]
private OsuGameBase game { get; set; }
@ -37,7 +26,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
private MultiSpectatorScreen spectatorScreen;
private readonly List<int> playingUserIds = new List<int>();
private readonly Dictionary<int, int> nextFrame = new Dictionary<int, int>();
private BeatmapSetInfo importedSet;
private BeatmapInfo importedBeatmap;
@ -51,25 +39,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
importedBeatmapId = importedBeatmap.OnlineBeatmapID ?? -1;
}
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("reset sent frames", () => nextFrame.Clear());
AddStep("add streaming client", () =>
{
Remove(spectatorClient);
Add(spectatorClient);
});
AddStep("finish previous gameplay", () =>
{
foreach (var id in playingUserIds)
spectatorClient.EndPlay(id);
playingUserIds.Clear();
});
}
[SetUp]
public new void Setup() => Schedule(() => playingUserIds.Clear());
[Test]
public void TestDelayedStart()
@ -80,18 +51,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
Client.CurrentMatchPlayingUserIds.Add(PLAYER_2_ID);
playingUserIds.Add(PLAYER_1_ID);
playingUserIds.Add(PLAYER_2_ID);
nextFrame[PLAYER_1_ID] = 0;
nextFrame[PLAYER_2_ID] = 0;
});
loadSpectateScreen(false);
AddWaitStep("wait a bit", 10);
AddStep("load player first_player_id", () => spectatorClient.StartPlay(PLAYER_1_ID, importedBeatmapId));
AddStep("load player first_player_id", () => SpectatorClient.StartPlay(PLAYER_1_ID, importedBeatmapId));
AddUntilStep("one player added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 1);
AddWaitStep("wait a bit", 10);
AddStep("load player second_player_id", () => spectatorClient.StartPlay(PLAYER_2_ID, importedBeatmapId));
AddStep("load player second_player_id", () => SpectatorClient.StartPlay(PLAYER_2_ID, importedBeatmapId));
AddUntilStep("two players added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 2);
}
@ -107,6 +76,23 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddWaitStep("wait a bit", 20);
}
[Test]
public void TestTimeDoesNotProgressWhileAllPlayersPaused()
{
start(new[] { PLAYER_1_ID, PLAYER_2_ID });
loadSpectateScreen();
sendFrames(PLAYER_1_ID, 40);
sendFrames(PLAYER_2_ID, 20);
checkPaused(PLAYER_2_ID, true);
checkPausedInstant(PLAYER_1_ID, false);
AddAssert("master clock still running", () => this.ChildrenOfType<MasterGameplayClockContainer>().Single().IsRunning);
checkPaused(PLAYER_1_ID, true);
AddUntilStep("master clock paused", () => !this.ChildrenOfType<MasterGameplayClockContainer>().Single().IsRunning);
}
[Test]
public void TestPlayersMustStartSimultaneously()
{
@ -182,7 +168,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
// Send initial frames for both players. A few more for player 1.
sendFrames(PLAYER_1_ID, 1000);
sendFrames(PLAYER_2_ID, 10);
sendFrames(PLAYER_2_ID, 30);
checkPausedInstant(PLAYER_1_ID, false);
checkPausedInstant(PLAYER_2_ID, false);
@ -210,8 +196,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
sendFrames(PLAYER_1_ID, 10);
sendFrames(PLAYER_2_ID, 20);
assertMuted(PLAYER_1_ID, false);
assertMuted(PLAYER_2_ID, true);
checkPaused(PLAYER_1_ID, false);
assertOneNotMuted();
checkPaused(PLAYER_1_ID, true);
assertMuted(PLAYER_1_ID, true);
@ -229,6 +215,36 @@ namespace osu.Game.Tests.Visual.Multiplayer
assertMuted(PLAYER_2_ID, true);
}
[Test]
public void TestSpectatingDuringGameplay()
{
var players = new[] { PLAYER_1_ID, PLAYER_2_ID };
start(players);
sendFrames(players, 300);
loadSpectateScreen();
sendFrames(players, 300);
AddUntilStep("playing from correct point in time", () => this.ChildrenOfType<DrawableRuleset>().All(r => r.FrameStableClock.CurrentTime > 30000));
}
[Test]
public void TestSpectatingDuringGameplayWithLateFrames()
{
start(new[] { PLAYER_1_ID, PLAYER_2_ID });
sendFrames(new[] { PLAYER_1_ID, PLAYER_2_ID }, 300);
loadSpectateScreen();
sendFrames(PLAYER_1_ID, 300);
AddWaitStep("wait maximum start delay seconds", (int)(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction));
checkPaused(PLAYER_1_ID, false);
sendFrames(PLAYER_2_ID, 300);
AddUntilStep("player 2 playing from correct point in time", () => getPlayer(PLAYER_2_ID).ChildrenOfType<DrawableRuleset>().Single().FrameStableClock.CurrentTime > 30000);
}
private void loadSpectateScreen(bool waitForPlayerLoad = true)
{
AddStep("load screen", () =>
@ -242,8 +258,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded));
}
private void start(int userId, int? beatmapId = null) => start(new[] { userId }, beatmapId);
private void start(int[] userIds, int? beatmapId = null)
{
AddStep("start play", () =>
@ -251,23 +265,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
foreach (int id in userIds)
{
Client.CurrentMatchPlayingUserIds.Add(id);
spectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId);
SpectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId);
playingUserIds.Add(id);
nextFrame[id] = 0;
}
});
}
private void finish(int userId)
{
AddStep("end play", () =>
{
spectatorClient.EndPlay(userId);
playingUserIds.Remove(userId);
nextFrame.Remove(userId);
});
}
private void sendFrames(int userId, int count = 10) => sendFrames(new[] { userId }, count);
private void sendFrames(int[] userIds, int count = 10)
@ -275,10 +278,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("send frames", () =>
{
foreach (int id in userIds)
{
spectatorClient.SendFrames(id, nextFrame[id], count);
nextFrame[id] += count;
}
SpectatorClient.SendFrames(id, count);
});
}
@ -286,7 +286,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
=> AddUntilStep($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType<GameplayClockContainer>().First().GameplayClock.IsRunning != state);
private void checkPausedInstant(int userId, bool state)
=> AddAssert($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType<GameplayClockContainer>().First().GameplayClock.IsRunning != state);
{
checkPaused(userId, state);
// Todo: The following should work, but is broken because SpectatorScreen retrieves the WorkingBeatmap via the BeatmapManager, bypassing the test scene clock and running real-time.
// AddAssert($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType<GameplayClockContainer>().First().GameplayClock.IsRunning != state);
}
private void assertOneNotMuted() => AddAssert("one player not muted", () => spectatorScreen.ChildrenOfType<PlayerArea>().Count(p => !p.Mute) == 1);
private void assertMuted(int userId, bool muted)
=> AddAssert($"{userId} {(muted ? "is" : "is not")} muted", () => getInstance(userId).Mute == muted);
@ -297,17 +304,5 @@ namespace osu.Game.Tests.Visual.Multiplayer
private Player getPlayer(int userId) => getInstance(userId).ChildrenOfType<Player>().Single();
private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType<PlayerArea>().Single(p => p.UserId == userId);
internal class TestUserLookupCache : UserLookupCache
{
protected override Task<User> ComputeValueAsync(int lookup, CancellationToken token = default)
{
return Task.FromResult(new User
{
Id = lookup,
Username = $"User {lookup}"
});
}
}
}
}

View File

@ -18,6 +18,7 @@ using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.OnlinePlay.Multiplayer;
@ -30,14 +31,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayer : ScreenTestScene
{
private TestMultiplayer multiplayerScreen;
private BeatmapManager beatmaps;
private RulesetStore rulesets;
private BeatmapSetInfo importedSet;
private TestMultiplayerClient client => multiplayerScreen.Client;
private Room room => client.APIRoom;
private DependenciesScreen dependenciesScreen;
private TestMultiplayer multiplayerScreen;
private TestMultiplayerClient client;
public TestSceneMultiplayer()
{
@ -229,30 +229,43 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void loadMultiplayer()
{
AddStep("show", () =>
AddStep("create multiplayer screen", () => multiplayerScreen = new TestMultiplayer());
AddStep("load dependencies", () =>
{
multiplayerScreen = new TestMultiplayer();
client = new TestMultiplayerClient(multiplayerScreen.RoomManager);
// Needs to be added at a higher level since the multiplayer screen becomes non-current.
Child = multiplayerScreen.Client;
// The screen gets suspended so it stops receiving updates.
Child = client;
LoadScreen(multiplayerScreen);
LoadScreen(dependenciesScreen = new DependenciesScreen(client));
});
AddUntilStep("wait for loaded", () => multiplayerScreen.IsLoaded);
AddUntilStep("wait for dependencies to load", () => dependenciesScreen.IsLoaded);
AddStep("load multiplayer", () => LoadScreen(multiplayerScreen));
AddUntilStep("wait for multiplayer to load", () => multiplayerScreen.IsLoaded);
}
private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer
/// <summary>
/// Used for the sole purpose of adding <see cref="TestMultiplayerClient"/> as a resolvable dependency.
/// </summary>
private class DependenciesScreen : OsuScreen
{
[Cached(typeof(MultiplayerClient))]
public readonly TestMultiplayerClient Client;
public TestMultiplayer()
public DependenciesScreen(TestMultiplayerClient client)
{
Client = new TestMultiplayerClient((TestMultiplayerRoomManager)RoomManager);
Client = client;
}
}
protected override RoomManager CreateRoomManager() => new TestMultiplayerRoomManager();
private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer
{
public new TestMultiplayerRoomManager RoomManager { get; private set; }
protected override RoomManager CreateRoomManager() => RoomManager = new TestMultiplayerRoomManager();
}
}
}

View File

@ -6,12 +6,11 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy;
@ -19,37 +18,20 @@ using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual.Online;
using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Tests.Visual.Spectator;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerGameplayLeaderboard : MultiplayerTestScene
{
private const int users = 16;
private static IEnumerable<int> users => Enumerable.Range(0, 16);
[Cached(typeof(SpectatorClient))]
private TestMultiplayerSpectatorClient spectatorClient = new TestMultiplayerSpectatorClient();
[Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestSceneCurrentlyPlayingDisplay.TestUserLookupCache();
public new TestMultiplayerSpectatorClient SpectatorClient => (TestMultiplayerSpectatorClient)OnlinePlayDependencies?.SpectatorClient;
private MultiplayerGameplayLeaderboard leaderboard;
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
private OsuConfigManager config;
public TestSceneMultiplayerGameplayLeaderboard()
{
base.Content.Children = new Drawable[]
{
spectatorClient,
lookupCache,
Content
};
}
[BackgroundDependencyLoader]
private void load()
{
@ -59,7 +41,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUpSteps]
public override void SetUpSteps()
{
AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = lookupCache.GetUserAsync(1).Result);
AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = LookupCache.GetUserAsync(1).Result);
AddStep("create leaderboard", () =>
{
@ -70,14 +52,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
for (int i = 0; i < users; i++)
spectatorClient.StartPlay(i, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
foreach (var user in users)
SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
spectatorClient.Schedule(() =>
{
Client.CurrentMatchPlayingUserIds.Clear();
Client.CurrentMatchPlayingUserIds.AddRange(spectatorClient.PlayingUsers);
});
// Todo: This is REALLY bad.
Client.CurrentMatchPlayingUserIds.AddRange(users);
Children = new Drawable[]
{
@ -86,7 +65,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
scoreProcessor.ApplyBeatmap(playable);
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, spectatorClient.PlayingUsers.ToArray())
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, users.ToArray())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -100,24 +79,32 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestScoreUpdates()
{
AddRepeatStep("update state", () => spectatorClient.RandomlyUpdateState(), 100);
AddRepeatStep("update state", () => SpectatorClient.RandomlyUpdateState(), 100);
AddToggleStep("switch compact mode", expanded => leaderboard.Expanded.Value = expanded);
}
[Test]
public void TestUserQuit()
{
AddRepeatStep("mark user quit", () => Client.CurrentMatchPlayingUserIds.RemoveAt(0), users);
foreach (var user in users)
AddStep($"mark user {user} quit", () => Client.RemoveUser(LookupCache.GetUserAsync(user).Result.AsNonNull()));
}
[Test]
public void TestChangeScoringMode()
{
AddRepeatStep("update state", () => spectatorClient.RandomlyUpdateState(), 5);
AddRepeatStep("update state", () => SpectatorClient.RandomlyUpdateState(), 5);
AddStep("change to classic", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Classic));
AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised));
}
protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies();
protected class TestDependencies : MultiplayerTestSceneDependencies
{
protected override TestSpectatorClient CreateSpectatorClient() => new TestMultiplayerSpectatorClient();
}
public class TestMultiplayerSpectatorClient : TestSpectatorClient
{
private readonly Dictionary<int, FrameHeader> lastHeaders = new Dictionary<int, FrameHeader>();

View File

@ -1,7 +1,7 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
@ -10,18 +10,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerMatchFooter : MultiplayerTestScene
{
[Cached]
private readonly OnlinePlayBeatmapAvailabilityTracker availablilityTracker = new OnlinePlayBeatmapAvailabilityTracker();
[BackgroundDependencyLoader]
private void load()
[SetUp]
public new void Setup() => Schedule(() =>
{
SelectedRoom.Value = new Room();
Child = new MultiplayerMatchFooter
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Height = 50
};
}
});
}
}

View File

@ -29,7 +29,7 @@ using osu.Game.Screens.Select;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerMatchSongSelect : RoomTestScene
public class TestSceneMultiplayerMatchSongSelect : MultiplayerTestScene
{
private BeatmapManager manager;
private RulesetStore rulesets;

View File

@ -49,13 +49,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUp]
public new void Setup() => Schedule(() =>
{
Room.Name.Value = "Test Room";
SelectedRoom.Value = new Room { Name = { Value = "Test Room" } };
});
[SetUpSteps]
public void SetupSteps()
{
AddStep("load match", () => LoadScreen(screen = new MultiplayerMatchSubScreen(Room)));
AddStep("load match", () => LoadScreen(screen = new MultiplayerMatchSubScreen(SelectedRoom.Value)));
AddUntilStep("wait for load", () => screen.IsCurrentScreen());
}
@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("set playlist", () =>
{
Room.Playlist.Add(new PlaylistItem
SelectedRoom.Value.Playlist.Add(new PlaylistItem
{
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("set playlist", () =>
{
Room.Playlist.Add(new PlaylistItem
SelectedRoom.Value.Playlist.Add(new PlaylistItem
{
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
@ -102,7 +102,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("set playlist", () =>
{
Room.Playlist.Add(new PlaylistItem
SelectedRoom.Value.Playlist.Add(new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },

View File

@ -22,8 +22,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerParticipantsList : MultiplayerTestScene
{
[SetUp]
public new void Setup() => Schedule(createNewParticipantsList);
[SetUpSteps]
public void SetupSteps()
{
createNewParticipantsList();
}
[Test]
public void TestAddUser()
@ -88,7 +91,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestCorrectInitialState()
{
AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
AddStep("recreate list", createNewParticipantsList);
createNewParticipantsList();
checkProgressBarVisibility(true);
}
@ -233,7 +236,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void createNewParticipantsList()
{
Child = new ParticipantsList { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Y, Size = new Vector2(380, 0.7f) };
ParticipantsList participantsList = null;
AddStep("create new list", () => Child = participantsList = new ParticipantsList
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
Size = new Vector2(380, 0.7f)
});
AddUntilStep("wait for list to load", () => participantsList.IsLoaded);
}
private void checkProgressBarVisibility(bool visible) =>

View File

@ -27,7 +27,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
public class TestSceneMultiplayerReadyButton : MultiplayerTestScene
{
private MultiplayerReadyButton button;
private OnlinePlayBeatmapAvailabilityTracker beatmapTracker;
private BeatmapSetInfo importedSet;
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
@ -43,18 +42,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
Add(beatmapTracker = new OnlinePlayBeatmapAvailabilityTracker
{
SelectedItem = { BindTarget = selectedItem }
});
Dependencies.Cache(beatmapTracker);
}
[SetUp]
public new void Setup() => Schedule(() =>
{
AvailabilityTracker.SelectedItem.BindTo(selectedItem);
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
selectedItem.Value = new PlaylistItem

View File

@ -3,37 +3,38 @@
using System;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual.OnlinePlay;
namespace osu.Game.Tests.Visual.Multiplayer
{
[HeadlessTest]
public class TestSceneMultiplayerRoomManager : RoomTestScene
public class TestSceneMultiplayerRoomManager : MultiplayerTestScene
{
private TestMultiplayerRoomContainer roomContainer;
private TestMultiplayerRoomManager roomManager => roomContainer.RoomManager;
protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies();
public TestSceneMultiplayerRoomManager()
: base(false)
{
}
[Test]
public void TestPollsInitially()
{
AddStep("create room manager with a few rooms", () =>
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
roomManager.CreateRoom(createRoom(r => r.Name.Value = "1"));
roomManager.PartRoom();
roomManager.CreateRoom(createRoom(r => r.Name.Value = "2"));
roomManager.PartRoom();
roomManager.ClearRooms();
});
RoomManager.CreateRoom(createRoom(r => r.Name.Value = "1"));
RoomManager.PartRoom();
RoomManager.CreateRoom(createRoom(r => r.Name.Value = "2"));
RoomManager.PartRoom();
RoomManager.ClearRooms();
});
AddAssert("manager polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 2);
AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value);
AddAssert("manager polled for rooms", () => ((RoomManager)RoomManager).Rooms.Count == 2);
AddAssert("initial rooms received", () => RoomManager.InitialRoomsReceived.Value);
}
[Test]
@ -41,19 +42,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create room manager with a few rooms", () =>
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
roomManager.CreateRoom(createRoom());
roomManager.PartRoom();
roomManager.CreateRoom(createRoom());
roomManager.PartRoom();
});
RoomManager.CreateRoom(createRoom());
RoomManager.PartRoom();
RoomManager.CreateRoom(createRoom());
RoomManager.PartRoom();
});
AddStep("disconnect", () => roomContainer.Client.Disconnect());
AddStep("disconnect", () => Client.Disconnect());
AddAssert("rooms cleared", () => ((RoomManager)roomManager).Rooms.Count == 0);
AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value);
AddAssert("rooms cleared", () => ((RoomManager)RoomManager).Rooms.Count == 0);
AddAssert("initial rooms not received", () => !RoomManager.InitialRoomsReceived.Value);
}
[Test]
@ -61,20 +59,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create room manager with a few rooms", () =>
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
roomManager.CreateRoom(createRoom());
roomManager.PartRoom();
roomManager.CreateRoom(createRoom());
roomManager.PartRoom();
});
RoomManager.CreateRoom(createRoom());
RoomManager.PartRoom();
RoomManager.CreateRoom(createRoom());
RoomManager.PartRoom();
});
AddStep("disconnect", () => roomContainer.Client.Disconnect());
AddStep("connect", () => roomContainer.Client.Connect());
AddStep("disconnect", () => Client.Disconnect());
AddStep("connect", () => Client.Connect());
AddAssert("manager polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 2);
AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value);
AddAssert("manager polled for rooms", () => ((RoomManager)RoomManager).Rooms.Count == 2);
AddAssert("initial rooms received", () => RoomManager.InitialRoomsReceived.Value);
}
[Test]
@ -82,15 +77,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create room manager with a room", () =>
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
roomManager.CreateRoom(createRoom());
roomManager.ClearRooms();
});
RoomManager.CreateRoom(createRoom());
RoomManager.ClearRooms();
});
AddAssert("manager not polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 0);
AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value);
AddAssert("manager not polled for rooms", () => ((RoomManager)RoomManager).Rooms.Count == 0);
AddAssert("initial rooms not received", () => !RoomManager.InitialRoomsReceived.Value);
}
[Test]
@ -98,13 +90,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create room manager with a room", () =>
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
roomManager.CreateRoom(createRoom());
});
RoomManager.CreateRoom(createRoom());
});
AddUntilStep("multiplayer room joined", () => roomContainer.Client.Room != null);
AddUntilStep("multiplayer room joined", () => Client.Room != null);
}
[Test]
@ -112,14 +101,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create room manager with a room", () =>
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
roomManager.CreateRoom(createRoom());
roomManager.PartRoom();
});
RoomManager.CreateRoom(createRoom());
RoomManager.PartRoom();
});
AddAssert("multiplayer room parted", () => roomContainer.Client.Room == null);
AddAssert("multiplayer room parted", () => Client.Room == null);
}
[Test]
@ -127,16 +113,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create room manager with a room", () =>
{
createRoomManager().With(d => d.OnLoadComplete += _ =>
{
var r = createRoom();
roomManager.CreateRoom(r);
roomManager.PartRoom();
roomManager.JoinRoom(r);
});
var r = createRoom();
RoomManager.CreateRoom(r);
RoomManager.PartRoom();
RoomManager.JoinRoom(r);
});
AddUntilStep("multiplayer room joined", () => roomContainer.Client.Room != null);
AddUntilStep("multiplayer room joined", () => Client.Room != null);
}
private Room createRoom(Action<Room> initFunc = null)
@ -161,18 +144,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
return room;
}
private TestMultiplayerRoomManager createRoomManager()
private class TestDependencies : MultiplayerTestSceneDependencies
{
Child = roomContainer = new TestMultiplayerRoomContainer
public TestDependencies()
{
RoomManager =
{
TimeBetweenListingPolls = { Value = 1 },
TimeBetweenSelectionPolls = { Value = 1 }
}
};
return roomManager;
// Need to set these values as early as possible.
RoomManager.TimeBetweenListingPolls.Value = 1;
RoomManager.TimeBetweenSelectionPolls.Value = 1;
}
}
}
}

View File

@ -37,40 +37,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
private IDisposable readyClickOperation;
protected override Container<Drawable> Content => content;
private readonly Container content;
public TestSceneMultiplayerSpectateButton()
{
base.Content.Add(content = new Container
{
RelativeSizeAxes = Axes.Both
});
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
return dependencies;
}
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
var beatmapTracker = new OnlinePlayBeatmapAvailabilityTracker { SelectedItem = { BindTarget = selectedItem } };
base.Content.Add(beatmapTracker);
Dependencies.Cache(beatmapTracker);
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
}
[SetUp]
public new void Setup() => Schedule(() =>
{
AvailabilityTracker.SelectedItem.BindTo(selectedItem);
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
selectedItem.Value = new PlaylistItem

View File

@ -14,16 +14,18 @@ using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Tests.Visual.OnlinePlay;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestScenePlaylistsSongSelect : RoomTestScene
public class TestScenePlaylistsSongSelect : OnlinePlayTestScene
{
[Resolved]
private BeatmapManager beatmapManager { get; set; }
@ -85,6 +87,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("reset", () =>
{
SelectedRoom.Value = new Room();
Ruleset.Value = new OsuRuleset().RulesetInfo;
Beatmap.SetDefault();
SelectedMods.Value = Array.Empty<Mod>();
@ -98,14 +101,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestItemAddedIfEmptyOnStart()
{
AddStep("finalise selection", () => songSelect.FinaliseSelection());
AddAssert("playlist has 1 item", () => Room.Playlist.Count == 1);
AddAssert("playlist has 1 item", () => SelectedRoom.Value.Playlist.Count == 1);
}
[Test]
public void TestItemAddedWhenCreateNewItemClicked()
{
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem());
AddAssert("playlist has 1 item", () => Room.Playlist.Count == 1);
AddAssert("playlist has 1 item", () => SelectedRoom.Value.Playlist.Count == 1);
}
[Test]
@ -113,7 +116,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem());
AddStep("finalise selection", () => songSelect.FinaliseSelection());
AddAssert("playlist has 1 item", () => Room.Playlist.Count == 1);
AddAssert("playlist has 1 item", () => SelectedRoom.Value.Playlist.Count == 1);
}
[Test]
@ -121,7 +124,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem());
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem());
AddAssert("playlist has 2 items", () => Room.Playlist.Count == 2);
AddAssert("playlist has 2 items", () => SelectedRoom.Value.Playlist.Count == 2);
}
[Test]
@ -131,13 +134,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem());
AddStep("rearrange", () =>
{
var item = Room.Playlist[0];
Room.Playlist.RemoveAt(0);
Room.Playlist.Add(item);
var item = SelectedRoom.Value.Playlist[0];
SelectedRoom.Value.Playlist.RemoveAt(0);
SelectedRoom.Value.Playlist.Add(item);
});
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem());
AddAssert("new item has id 2", () => Room.Playlist.Last().ID == 2);
AddAssert("new item has id 2", () => SelectedRoom.Value.Playlist.Last().ID == 2);
}
/// <summary>
@ -151,8 +154,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("change mod rate", () => ((OsuModDoubleTime)SelectedMods.Value[0]).SpeedChange.Value = 2);
AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem());
AddAssert("item 1 has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)Room.Playlist.First().RequiredMods[0]).SpeedChange.Value));
AddAssert("item 2 has rate 2", () => Precision.AlmostEquals(2, ((OsuModDoubleTime)Room.Playlist.Last().RequiredMods[0]).SpeedChange.Value));
AddAssert("item 1 has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)SelectedRoom.Value.Playlist.First().RequiredMods[0]).SpeedChange.Value));
AddAssert("item 2 has rate 2", () => Precision.AlmostEquals(2, ((OsuModDoubleTime)SelectedRoom.Value.Playlist.Last().RequiredMods[0]).SpeedChange.Value));
}
/// <summary>
@ -174,7 +177,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem());
AddStep("change stored mod rate", () => mod.SpeedChange.Value = 2);
AddAssert("item has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)Room.Playlist.First().RequiredMods[0]).SpeedChange.Value));
AddAssert("item has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)SelectedRoom.Value.Playlist.First().RequiredMods[0]).SpeedChange.Value));
}
private class TestPlaylistsSongSelect : PlaylistsSongSelect

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@ -14,6 +15,8 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Online
{
@ -23,6 +26,8 @@ namespace osu.Game.Tests.Visual.Online
private BeatmapListingOverlay overlay;
private BeatmapListingSearchControl searchControl => overlay.ChildrenOfType<BeatmapListingSearchControl>().Single();
[BackgroundDependencyLoader]
private void load()
{
@ -39,6 +44,16 @@ namespace osu.Game.Tests.Visual.Online
return true;
};
AddStep("initialize dummy", () =>
{
// non-supporter user
((DummyAPIAccess)API).LocalUser.Value = new User
{
Username = "TestBot",
Id = API.LocalUser.Value.Id + 1,
};
});
}
[Test]
@ -58,13 +73,164 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep("placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true);
}
[Test]
public void TestUserWithoutSupporterUsesSupporterOnlyFiltersWithoutResults()
{
AddStep("fetch for 0 beatmaps", () => fetchFor());
AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false);
// only Rank Achieved filter
setRankAchievedFilter(new[] { ScoreRank.XH });
supporterRequiredPlaceholderShown();
setRankAchievedFilter(Array.Empty<ScoreRank>());
notFoundPlaceholderShown();
// only Played filter
setPlayedFilter(SearchPlayed.Played);
supporterRequiredPlaceholderShown();
setPlayedFilter(SearchPlayed.Any);
notFoundPlaceholderShown();
// both RankAchieved and Played filters
setRankAchievedFilter(new[] { ScoreRank.XH });
setPlayedFilter(SearchPlayed.Played);
supporterRequiredPlaceholderShown();
setRankAchievedFilter(Array.Empty<ScoreRank>());
setPlayedFilter(SearchPlayed.Any);
notFoundPlaceholderShown();
}
[Test]
public void TestUserWithSupporterUsesSupporterOnlyFiltersWithoutResults()
{
AddStep("fetch for 0 beatmaps", () => fetchFor());
AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true);
// only Rank Achieved filter
setRankAchievedFilter(new[] { ScoreRank.XH });
notFoundPlaceholderShown();
setRankAchievedFilter(Array.Empty<ScoreRank>());
notFoundPlaceholderShown();
// only Played filter
setPlayedFilter(SearchPlayed.Played);
notFoundPlaceholderShown();
setPlayedFilter(SearchPlayed.Any);
notFoundPlaceholderShown();
// both Rank Achieved and Played filters
setRankAchievedFilter(new[] { ScoreRank.XH });
setPlayedFilter(SearchPlayed.Played);
notFoundPlaceholderShown();
setRankAchievedFilter(Array.Empty<ScoreRank>());
setPlayedFilter(SearchPlayed.Any);
notFoundPlaceholderShown();
}
[Test]
public void TestUserWithoutSupporterUsesSupporterOnlyFiltersWithResults()
{
AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet));
AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false);
// only Rank Achieved filter
setRankAchievedFilter(new[] { ScoreRank.XH });
supporterRequiredPlaceholderShown();
setRankAchievedFilter(Array.Empty<ScoreRank>());
noPlaceholderShown();
// only Played filter
setPlayedFilter(SearchPlayed.Played);
supporterRequiredPlaceholderShown();
setPlayedFilter(SearchPlayed.Any);
noPlaceholderShown();
// both Rank Achieved and Played filters
setRankAchievedFilter(new[] { ScoreRank.XH });
setPlayedFilter(SearchPlayed.Played);
supporterRequiredPlaceholderShown();
setRankAchievedFilter(Array.Empty<ScoreRank>());
setPlayedFilter(SearchPlayed.Any);
noPlaceholderShown();
}
[Test]
public void TestUserWithSupporterUsesSupporterOnlyFiltersWithResults()
{
AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet));
AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true);
// only Rank Achieved filter
setRankAchievedFilter(new[] { ScoreRank.XH });
noPlaceholderShown();
setRankAchievedFilter(Array.Empty<ScoreRank>());
noPlaceholderShown();
// only Played filter
setPlayedFilter(SearchPlayed.Played);
noPlaceholderShown();
setPlayedFilter(SearchPlayed.Any);
noPlaceholderShown();
// both Rank Achieved and Played filters
setRankAchievedFilter(new[] { ScoreRank.XH });
setPlayedFilter(SearchPlayed.Played);
noPlaceholderShown();
setRankAchievedFilter(Array.Empty<ScoreRank>());
setPlayedFilter(SearchPlayed.Any);
noPlaceholderShown();
}
private void fetchFor(params BeatmapSetInfo[] beatmaps)
{
setsForResponse.Clear();
setsForResponse.AddRange(beatmaps.Select(b => new TestAPIBeatmapSet(b)));
// trigger arbitrary change for fetching.
overlay.ChildrenOfType<BeatmapListingSearchControl>().Single().Query.TriggerChange();
searchControl.Query.TriggerChange();
}
private void setRankAchievedFilter(ScoreRank[] ranks)
{
AddStep($"set Rank Achieved filter to [{string.Join(',', ranks)}]", () =>
{
searchControl.Ranks.Clear();
searchControl.Ranks.AddRange(ranks);
});
}
private void setPlayedFilter(SearchPlayed played)
{
AddStep($"set Played filter to {played}", () => searchControl.Played.Value = played);
}
private void supporterRequiredPlaceholderShown()
{
AddUntilStep("\"supporter required\" placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.SupporterRequiredDrawable>().SingleOrDefault()?.IsPresent == true);
}
private void notFoundPlaceholderShown()
{
AddUntilStep("\"no maps found\" placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true);
}
private void noPlaceholderShown()
{
AddUntilStep("no placeholder shown", () =>
!overlay.ChildrenOfType<BeatmapListingOverlay.SupporterRequiredDrawable>().Any()
&& !overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().Any());
}
private class TestAPIBeatmapSet : APIBeatmapSet

View File

@ -82,7 +82,8 @@ namespace osu.Game.Tests.Visual.Online
{
switch (req)
{
case JoinChannelRequest _:
case JoinChannelRequest joinChannel:
joinChannel.TriggerSuccess();
return true;
}

View File

@ -3,25 +3,21 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Graphics.Containers;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Tests.Visual.Multiplayer;
using osu.Game.Tests.Visual.OnlinePlay;
namespace osu.Game.Tests.Visual.Playlists
{
public class TestScenePlaylistsLoungeSubScreen : RoomManagerTestScene
public class TestScenePlaylistsLoungeSubScreen : OnlinePlayTestScene
{
private LoungeSubScreen loungeScreen;
protected new BasicTestRoomManager RoomManager => (BasicTestRoomManager)base.RoomManager;
[BackgroundDependencyLoader]
private void load()
{
}
private LoungeSubScreen loungeScreen;
public override void SetUpSteps()
{
@ -37,7 +33,7 @@ namespace osu.Game.Tests.Visual.Playlists
[Test]
public void TestScrollSelectedIntoView()
{
AddRooms(30);
AddStep("add rooms", () => RoomManager.AddRooms(30));
AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms.First()));

View File

@ -3,7 +3,6 @@
using System;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -12,26 +11,28 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Tests.Visual.OnlinePlay;
namespace osu.Game.Tests.Visual.Playlists
{
public class TestScenePlaylistsMatchSettingsOverlay : RoomTestScene
public class TestScenePlaylistsMatchSettingsOverlay : OnlinePlayTestScene
{
[Cached(Type = typeof(IRoomManager))]
private TestRoomManager roomManager = new TestRoomManager();
protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
private TestRoomSettings settings;
protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies();
[SetUp]
public new void Setup() => Schedule(() =>
{
settings = new TestRoomSettings
SelectedRoom.Value = new Room();
Child = settings = new TestRoomSettings
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible }
};
Child = settings;
});
[Test]
@ -39,19 +40,19 @@ namespace osu.Game.Tests.Visual.Playlists
{
AddStep("clear name and beatmap", () =>
{
Room.Name.Value = "";
Room.Playlist.Clear();
SelectedRoom.Value.Name.Value = "";
SelectedRoom.Value.Playlist.Clear();
});
AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value);
AddStep("set name", () => Room.Name.Value = "Room name");
AddStep("set name", () => SelectedRoom.Value.Name.Value = "Room name");
AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value);
AddStep("set beatmap", () => Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } }));
AddStep("set beatmap", () => SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } }));
AddAssert("button enabled", () => settings.ApplyButton.Enabled.Value);
AddStep("clear name", () => Room.Name.Value = "");
AddStep("clear name", () => SelectedRoom.Value.Name.Value = "");
AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value);
}
@ -67,9 +68,9 @@ namespace osu.Game.Tests.Visual.Playlists
{
settings.NameField.Current.Value = expected_name;
settings.DurationField.Current.Value = expectedDuration;
Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } });
SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } });
roomManager.CreateRequested = r =>
RoomManager.CreateRequested = r =>
{
createdRoom = r;
return true;
@ -88,11 +89,11 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("setup", () =>
{
Room.Name.Value = "Test Room";
Room.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } });
SelectedRoom.Value.Name.Value = "Test Room";
SelectedRoom.Value.Playlist.Add(new PlaylistItem { Beatmap = { Value = CreateBeatmap(Ruleset.Value).BeatmapInfo } });
fail = true;
roomManager.CreateRequested = _ => !fail;
RoomManager.CreateRequested = _ => !fail;
});
AddAssert("error not displayed", () => !settings.ErrorText.IsPresent);
@ -119,7 +120,12 @@ namespace osu.Game.Tests.Visual.Playlists
public OsuSpriteText ErrorText => ((MatchSettings)Settings).ErrorText;
}
private class TestRoomManager : IRoomManager
private class TestDependencies : OnlinePlayTestSceneDependencies
{
protected override IRoomManager CreateRoomManager() => new TestRoomManager();
}
protected class TestRoomManager : IRoomManager
{
public const string FAILED_TEXT = "failed";

View File

@ -3,21 +3,23 @@
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Playlists
{
public class TestScenePlaylistsParticipantsList : RoomTestScene
public class TestScenePlaylistsParticipantsList : OnlinePlayTestScene
{
[SetUp]
public new void Setup() => Schedule(() =>
{
Room.RoomID.Value = 7;
SelectedRoom.Value = new Room { RoomID = { Value = 7 } };
for (int i = 0; i < 50; i++)
{
Room.RecentParticipants.Add(new User
SelectedRoom.Value.RecentParticipants.Add(new User
{
Username = "peppy",
Statistics = new UserStatistics { GlobalRank = 1234 },

View File

@ -15,20 +15,17 @@ using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Users;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Playlists
{
public class TestScenePlaylistsRoomSubScreen : RoomTestScene
public class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene
{
[Cached(typeof(IRoomManager))]
private readonly TestRoomManager roomManager = new TestRoomManager();
private BeatmapManager manager;
private RulesetStore rulesets;
@ -40,8 +37,6 @@ namespace osu.Game.Tests.Visual.Playlists
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
manager.Import(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Wait();
((DummyAPIAccess)API).HandleRequest = req =>
{
switch (req)
@ -58,7 +53,9 @@ namespace osu.Game.Tests.Visual.Playlists
[SetUpSteps]
public void SetupSteps()
{
AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(Room)));
AddStep("set room", () => SelectedRoom.Value = new Room());
AddStep("ensure has beatmap", () => manager.Import(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Wait());
AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(SelectedRoom.Value)));
AddUntilStep("wait for load", () => match.IsCurrentScreen());
}
@ -67,12 +64,12 @@ namespace osu.Game.Tests.Visual.Playlists
{
AddStep("set room properties", () =>
{
Room.RoomID.Value = 1;
Room.Name.Value = "my awesome room";
Room.Host.Value = new User { Id = 2, Username = "peppy" };
Room.RecentParticipants.Add(Room.Host.Value);
Room.EndDate.Value = DateTimeOffset.Now.AddMinutes(5);
Room.Playlist.Add(new PlaylistItem
SelectedRoom.Value.RoomID.Value = 1;
SelectedRoom.Value.Name.Value = "my awesome room";
SelectedRoom.Value.Host.Value = new User { Id = 2, Username = "peppy" };
SelectedRoom.Value.RecentParticipants.Add(SelectedRoom.Value.Host.Value);
SelectedRoom.Value.EndDate.Value = DateTimeOffset.Now.AddMinutes(5);
SelectedRoom.Value.Playlist.Add(new PlaylistItem
{
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo }
@ -88,9 +85,9 @@ namespace osu.Game.Tests.Visual.Playlists
{
AddStep("set room properties", () =>
{
Room.Name.Value = "my awesome room";
Room.Host.Value = new User { Id = 2, Username = "peppy" };
Room.Playlist.Add(new PlaylistItem
SelectedRoom.Value.Name.Value = "my awesome room";
SelectedRoom.Value.Host.Value = new User { Id = 2, Username = "peppy" };
SelectedRoom.Value.Playlist.Add(new PlaylistItem
{
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo }
@ -104,17 +101,34 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("first playlist item selected", () => match.SelectedItem.Value == Room.Playlist[0]);
AddAssert("first playlist item selected", () => match.SelectedItem.Value == SelectedRoom.Value.Playlist[0]);
}
[Test]
public void TestBeatmapUpdatedOnReImport()
{
BeatmapSetInfo importedSet = null;
TestBeatmap beatmap = null;
// this step is required to make sure the further imports actually get online IDs.
// all the playlist logic relies on online ID matching.
AddStep("remove all matching online IDs", () =>
{
beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo);
var existing = manager.QueryBeatmapSets(s => s.OnlineBeatmapSetID == beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID).ToList();
foreach (var s in existing)
{
s.OnlineBeatmapSetID = null;
foreach (var b in s.Beatmaps)
b.OnlineBeatmapID = null;
manager.Update(s);
}
});
AddStep("import altered beatmap", () =>
{
var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo);
beatmap.BeatmapInfo.BaseDifficulty.CircleSize = 1;
importedSet = manager.Import(beatmap.BeatmapInfo.BeatmapSet).Result;
@ -122,9 +136,9 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("load room", () =>
{
Room.Name.Value = "my awesome room";
Room.Host.Value = new User { Id = 2, Username = "peppy" };
Room.Playlist.Add(new PlaylistItem
SelectedRoom.Value.Name.Value = "my awesome room";
SelectedRoom.Value.Host.Value = new User { Id = 2, Username = "peppy" };
SelectedRoom.Value.Playlist.Add(new PlaylistItem
{
Beatmap = { Value = importedSet.Beatmaps[0] },
Ruleset = { Value = new OsuRuleset().RulesetInfo }
@ -155,30 +169,5 @@ namespace osu.Game.Tests.Visual.Playlists
{
}
}
private class TestRoomManager : IRoomManager
{
public event Action RoomsUpdated
{
add => throw new NotImplementedException();
remove => throw new NotImplementedException();
}
public IBindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>(true);
public IBindableList<Room> Rooms { get; } = new BindableList<Room>();
public void CreateRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null)
{
room.RoomID.Value = 1;
onSuccess?.Invoke(room);
}
public void JoinRoom(Room room, Action<Room> onSuccess = null, Action<string> onError = null) => onSuccess?.Invoke(room);
public void PartRoom()
{
}
}
}
}

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osuTK;
@ -59,7 +60,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private class Icon : Container, IHasTooltip
{
public string TooltipText { get; }
public LocalisableString TooltipText { get; }
public SpriteIcon SpriteIcon { get; }

View File

@ -149,7 +149,8 @@ namespace osu.Game.Tournament.Tests.NonVisual
private TournamentGameBase loadOsu(GameHost host)
{
var osu = new TournamentGameBase();
Task.Run(() => host.Run(osu));
Task.Run(() => host.Run(osu))
.ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted);
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
return osu;
}

View File

@ -55,7 +55,8 @@ namespace osu.Game.Tournament.Tests.NonVisual
private TournamentGameBase loadOsu(GameHost host)
{
var osu = new TournamentGameBase();
Task.Run(() => host.Run(osu));
Task.Run(() => host.Run(osu))
.ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted);
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
return osu;
}

View File

@ -147,7 +147,7 @@ namespace osu.Game.Tournament.Screens.Editors
[Resolved]
protected IAPIProvider API { get; private set; }
private readonly Bindable<string> beatmapId = new Bindable<string>();
private readonly Bindable<int?> beatmapId = new Bindable<int?>();
private readonly Bindable<string> mods = new Bindable<string>();
@ -220,14 +220,12 @@ namespace osu.Game.Tournament.Screens.Editors
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
beatmapId.Value = Model.ID.ToString();
beatmapId.BindValueChanged(idString =>
beatmapId.Value = Model.ID;
beatmapId.BindValueChanged(id =>
{
int.TryParse(idString.NewValue, out var parsed);
Model.ID = id.NewValue ?? 0;
Model.ID = parsed;
if (idString.NewValue != idString.OldValue)
if (id.NewValue != id.OldValue)
Model.BeatmapInfo = null;
if (Model.BeatmapInfo != null)

View File

@ -147,7 +147,7 @@ namespace osu.Game.Tournament.Screens.Editors
[Resolved]
protected IAPIProvider API { get; private set; }
private readonly Bindable<string> beatmapId = new Bindable<string>();
private readonly Bindable<int?> beatmapId = new Bindable<int?>();
private readonly Bindable<string> score = new Bindable<string>();
@ -228,16 +228,12 @@ namespace osu.Game.Tournament.Screens.Editors
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
beatmapId.Value = Model.ID.ToString();
beatmapId.BindValueChanged(idString =>
beatmapId.Value = Model.ID;
beatmapId.BindValueChanged(id =>
{
int parsed;
Model.ID = id.NewValue ?? 0;
int.TryParse(idString.NewValue, out parsed);
Model.ID = parsed;
if (idString.NewValue != idString.OldValue)
if (id.NewValue != id.OldValue)
Model.BeatmapInfo = null;
if (Model.BeatmapInfo != null)

View File

@ -214,7 +214,7 @@ namespace osu.Game.Tournament.Screens.Editors
[Resolved]
private TournamentGameBase game { get; set; }
private readonly Bindable<string> userId = new Bindable<string>();
private readonly Bindable<int?> userId = new Bindable<int?>();
private readonly Container drawableContainer;
@ -278,14 +278,12 @@ namespace osu.Game.Tournament.Screens.Editors
[BackgroundDependencyLoader]
private void load()
{
userId.Value = user.Id.ToString();
userId.BindValueChanged(idString =>
userId.Value = user.Id;
userId.BindValueChanged(id =>
{
int.TryParse(idString.NewValue, out var parsed);
user.Id = id.NewValue ?? 0;
user.Id = parsed;
if (idString.NewValue != idString.OldValue)
if (id.NewValue != id.OldValue)
user.Username = string.Empty;
if (!string.IsNullOrEmpty(user.Username))

View File

@ -181,8 +181,13 @@ namespace osu.Game.Beatmaps
if (existingOnlineId != null)
{
Delete(existingOnlineId);
beatmaps.PurgeDeletable(s => s.ID == existingOnlineId.ID);
LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been purged.");
// in order to avoid a unique key constraint, immediately remove the online ID from the previous set.
existingOnlineId.OnlineBeatmapSetID = null;
foreach (var b in existingOnlineId.Beatmaps)
b.OnlineBeatmapID = null;
LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been deleted.");
}
}
}
@ -191,8 +196,6 @@ namespace osu.Game.Beatmaps
{
var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList();
LogForModel(beatmapSet, $"Validating online IDs for {beatmapSet.Beatmaps.Count} beatmaps...");
// ensure all IDs are unique
if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1))
{
@ -319,6 +322,14 @@ namespace osu.Game.Beatmaps
/// <returns>The first result for the provided query, or null if no results were found.</returns>
public BeatmapSetInfo QueryBeatmapSet(Expression<Func<BeatmapSetInfo, bool>> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query);
protected override bool CanSkipImport(BeatmapSetInfo existing, BeatmapSetInfo import)
{
if (!base.CanReuseExisting(existing, import))
return false;
return existing.Beatmaps.Any(b => b.OnlineBeatmapID != null);
}
protected override bool CanReuseExisting(BeatmapSetInfo existing, BeatmapSetInfo import)
{
if (!base.CanReuseExisting(existing, import))

View File

@ -48,7 +48,6 @@ namespace osu.Game.Beatmaps
public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken)
{
LogForModel(beatmapSet, "Performing online lookups...");
return Task.WhenAll(beatmapSet.Beatmaps.Select(b => UpdateAsync(beatmapSet, b, cancellationToken)).ToArray());
}

View File

@ -31,7 +31,7 @@ namespace osu.Game.Configuration
{
public LocalisableString Label { get; }
public string Description { get; }
public LocalisableString Description { get; }
public int? OrderPosition { get; }

View File

@ -78,7 +78,7 @@ namespace osu.Game.Database
private readonly Bindable<WeakReference<TModel>> itemRemoved = new Bindable<WeakReference<TModel>>();
public virtual IEnumerable<string> HandledExtensions => new[] { ".zip" };
public virtual IEnumerable<string> HandledExtensions => new[] { @".zip" };
protected readonly FileStore Files;
@ -99,7 +99,7 @@ namespace osu.Game.Database
ModelStore.ItemUpdated += item => handleEvent(() => itemUpdated.Value = new WeakReference<TModel>(item));
ModelStore.ItemRemoved += item => handleEvent(() => itemRemoved.Value = new WeakReference<TModel>(item));
exportStorage = storage.GetStorageForDirectory("exports");
exportStorage = storage.GetStorageForDirectory(@"exports");
Files = new FileStore(contextFactory, storage);
@ -282,7 +282,7 @@ namespace osu.Game.Database
}
catch (Exception e)
{
LogForModel(model, $"Model creation of {archive.Name} failed.", e);
LogForModel(model, @$"Model creation of {archive.Name} failed.", e);
return null;
}
@ -309,6 +309,12 @@ namespace osu.Game.Database
Logger.Log($"{prefix} {message}", LoggingTarget.Database);
}
/// <summary>
/// Whether the implementation overrides <see cref="ComputeHash"/> with a custom implementation.
/// Custom hash implementations must bypass the early exit in the import flow (see <see cref="computeHashFast"/> usage).
/// </summary>
protected virtual bool HasCustomHashFunction => false;
/// <summary>
/// Create a SHA-2 hash from the provided archive based on file content of all files matching <see cref="HashableFileTypes"/>.
/// </summary>
@ -317,7 +323,11 @@ namespace osu.Game.Database
/// </remarks>
protected virtual string ComputeHash(TModel item, ArchiveReader reader = null)
{
// for now, concatenate all .osu files in the set to create a unique hash.
if (reader != null)
// fast hashing for cases where the item's files may not be populated.
return computeHashFast(reader);
// for now, concatenate all hashable files in the set to create a unique hash.
MemoryStream hashable = new MemoryStream();
foreach (TFileModel file in item.Files.Where(f => HashableFileTypes.Any(ext => f.Filename.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f.Filename))
@ -329,9 +339,6 @@ namespace osu.Game.Database
if (hashable.Length > 0)
return hashable.ComputeSHA2Hash();
if (reader != null)
return reader.Name.ComputeSHA2Hash();
return item.Hash;
}
@ -348,19 +355,48 @@ namespace osu.Game.Database
delayEvents();
bool checkedExisting = false;
TModel existing = null;
if (archive != null && !HasCustomHashFunction)
{
// this is a fast bail condition to improve large import performance.
item.Hash = computeHashFast(archive);
checkedExisting = true;
existing = CheckForExisting(item);
if (existing != null)
{
// bare minimum comparisons
//
// note that this should really be checking filesizes on disk (of existing files) for some degree of sanity.
// or alternatively doing a faster hash check. either of these require database changes and reprocessing of existing files.
if (CanSkipImport(existing, item) &&
getFilenames(existing.Files).SequenceEqual(getShortenedFilenames(archive).Select(p => p.shortened).OrderBy(f => f)))
{
LogForModel(item, @$"Found existing (optimised) {HumanisedModelName} for {item} (ID {existing.ID}) skipping import.");
Undelete(existing);
return existing;
}
LogForModel(item, @"Found existing (optimised) but failed pre-check.");
}
}
void rollback()
{
if (!Delete(item))
{
// We may have not yet added the model to the underlying table, but should still clean up files.
LogForModel(item, "Dereferencing files for incomplete import.");
LogForModel(item, @"Dereferencing files for incomplete import.");
Files.Dereference(item.Files.Select(f => f.FileInfo).ToArray());
}
}
try
{
LogForModel(item, "Beginning import...");
LogForModel(item, @"Beginning import...");
item.Files = archive != null ? createFileInfos(archive, Files) : new List<TFileModel>();
item.Hash = ComputeHash(item, archive);
@ -371,22 +407,24 @@ namespace osu.Game.Database
{
try
{
if (!write.IsTransactionLeader) throw new InvalidOperationException($"Ensure there is no parent transaction so errors can correctly be handled by {this}");
if (!write.IsTransactionLeader) throw new InvalidOperationException(@$"Ensure there is no parent transaction so errors can correctly be handled by {this}");
var existing = CheckForExisting(item);
if (!checkedExisting)
existing = CheckForExisting(item);
if (existing != null)
{
if (CanReuseExisting(existing, item))
{
Undelete(existing);
LogForModel(item, $"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) skipping import.");
LogForModel(item, @$"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) skipping import.");
// existing item will be used; rollback new import and exit early.
rollback();
flushEvents(true);
return existing;
}
LogForModel(item, @"Found existing but failed re-use check.");
Delete(existing);
ModelStore.PurgeDeletable(s => s.ID == existing.ID);
}
@ -403,12 +441,12 @@ namespace osu.Game.Database
}
}
LogForModel(item, "Import successfully completed!");
LogForModel(item, @"Import successfully completed!");
}
catch (Exception e)
{
if (!(e is TaskCanceledException))
LogForModel(item, "Database import or population failed and has been rolled back.", e);
LogForModel(item, @"Database import or population failed and has been rolled back.", e);
rollback();
flushEvents(false);
@ -428,7 +466,7 @@ namespace osu.Game.Database
var retrievedItem = ModelStore.ConsumableItems.FirstOrDefault(s => s.ID == item.ID);
if (retrievedItem == null)
throw new ArgumentException("Specified model could not be found", nameof(item));
throw new ArgumentException(@"Specified model could not be found", nameof(item));
using (var outputStream = exportStorage.GetStream($"{getValidFilename(item.ToString())}{HandledExtensions.First()}", FileAccess.Write, FileMode.Create))
ExportModelTo(retrievedItem, outputStream);
@ -637,6 +675,22 @@ namespace osu.Game.Database
}
}
private string computeHashFast(ArchiveReader reader)
{
MemoryStream hashable = new MemoryStream();
foreach (var file in reader.Filenames.Where(f => HashableFileTypes.Any(ext => f.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f))
{
using (Stream s = reader.GetStream(file))
s.CopyTo(hashable);
}
if (hashable.Length > 0)
return hashable.ComputeSHA2Hash();
return reader.Name.ComputeSHA2Hash();
}
/// <summary>
/// Create all required <see cref="IO.FileInfo"/>s for the provided archive, adding them to the global file store.
/// </summary>
@ -644,18 +698,14 @@ namespace osu.Game.Database
{
var fileInfos = new List<TFileModel>();
string prefix = reader.Filenames.GetCommonPrefix();
if (!(prefix.EndsWith('/') || prefix.EndsWith('\\')))
prefix = string.Empty;
// import files to manager
foreach (string file in reader.Filenames)
foreach (var filenames in getShortenedFilenames(reader))
{
using (Stream s = reader.GetStream(file))
using (Stream s = reader.GetStream(filenames.original))
{
fileInfos.Add(new TFileModel
{
Filename = file.Substring(prefix.Length).ToStandardisedPath(),
Filename = filenames.shortened,
FileInfo = files.Add(s)
});
}
@ -664,6 +714,17 @@ namespace osu.Game.Database
return fileInfos;
}
private IEnumerable<(string original, string shortened)> getShortenedFilenames(ArchiveReader reader)
{
string prefix = reader.Filenames.GetCommonPrefix();
if (!(prefix.EndsWith('/') || prefix.EndsWith('\\')))
prefix = string.Empty;
// import files to manager
foreach (string file in reader.Filenames)
yield return (file, file.Substring(prefix.Length).ToStandardisedPath());
}
#region osu-stable import
/// <summary>
@ -696,7 +757,7 @@ namespace osu.Game.Database
{
string fullPath = storage.GetFullPath(ImportFromStablePath);
Logger.Log($"Folder \"{fullPath}\" not available in the target osu!stable installation to import {HumanisedModelName}s.", LoggingTarget.Information, LogLevel.Error);
Logger.Log(@$"Folder ""{fullPath}"" not available in the target osu!stable installation to import {HumanisedModelName}s.", LoggingTarget.Information, LogLevel.Error);
return Task.CompletedTask;
}
@ -727,7 +788,7 @@ namespace osu.Game.Database
/// <param name="model">The model to populate.</param>
/// <param name="archive">The archive to use as a reference for population. May be null.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
protected virtual Task Populate(TModel model, [CanBeNull] ArchiveReader archive, CancellationToken cancellationToken = default) => Task.CompletedTask;
protected abstract Task Populate(TModel model, [CanBeNull] ArchiveReader archive, CancellationToken cancellationToken = default);
/// <summary>
/// Perform any final actions before the import to database executes.
@ -744,6 +805,15 @@ namespace osu.Game.Database
/// <returns>An existing model which matches the criteria to skip importing, else null.</returns>
protected TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash);
/// <summary>
/// Whether inport can be skipped after finding an existing import early in the process.
/// Only valid when <see cref="ComputeHash"/> is not overridden.
/// </summary>
/// <param name="existing">The existing model.</param>
/// <param name="import">The newly imported model.</param>
/// <returns>Whether to skip this import completely.</returns>
protected virtual bool CanSkipImport(TModel existing, TModel import) => true;
/// <summary>
/// After an existing <typeparamref name="TModel"/> is found during an import process, the default behaviour is to use/restore the existing
/// item and skip the import. This method allows changing that behaviour.
@ -771,7 +841,7 @@ namespace osu.Game.Database
private DbSet<TModel> queryModel() => ContextFactory.Get().Set<TModel>();
protected virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace("Info", "").ToLower()}";
protected virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}";
#region Event handling / delaying

View File

@ -26,6 +26,11 @@ namespace osu.Game.Database
/// </summary>
private readonly object writeLock = new object();
/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections.
/// </summary>
private readonly SemaphoreSlim blockingLock = new SemaphoreSlim(1);
private static readonly GlobalStatistic<int> reads = GlobalStatistics.Get<int>("Realm", "Get (Read)");
private static readonly GlobalStatistic<int> writes = GlobalStatistics.Get<int>("Realm", "Get (Write)");
private static readonly GlobalStatistic<int> refreshes = GlobalStatistics.Get<int>("Realm", "Dirty Refreshes");
@ -33,17 +38,12 @@ namespace osu.Game.Database
private static readonly GlobalStatistic<int> pending_writes = GlobalStatistics.Get<int>("Realm", "Pending writes");
private static readonly GlobalStatistic<int> active_usages = GlobalStatistics.Get<int>("Realm", "Active usages");
private readonly ManualResetEventSlim blockingResetEvent = new ManualResetEventSlim(true);
private Realm context;
public Realm Context
{
get
{
if (IsDisposed)
throw new InvalidOperationException($"Attempted to access {nameof(Context)} on a disposed context factory");
if (context == null)
{
context = createContext();
@ -64,7 +64,7 @@ namespace osu.Game.Database
public RealmUsage GetForRead()
{
reads.Value++;
return new RealmUsage(this);
return new RealmUsage(createContext());
}
public RealmWriteUsage GetForWrite()
@ -73,8 +73,34 @@ namespace osu.Game.Database
pending_writes.Value++;
Monitor.Enter(writeLock);
return new RealmWriteUsage(createContext(), writeComplete);
}
return new RealmWriteUsage(this);
/// <summary>
/// Flush any active contexts and block any further writes.
/// </summary>
/// <remarks>
/// This should be used in places we need to ensure no ongoing reads/writes are occurring with realm.
/// ie. to move the realm backing file to a new location.
/// </remarks>
/// <returns>An <see cref="IDisposable"/> which should be disposed to end the blocking section.</returns>
public IDisposable BlockAllOperations()
{
if (IsDisposed)
throw new ObjectDisposedException(nameof(RealmContextFactory));
Logger.Log(@"Blocking realm operations.", LoggingTarget.Database);
blockingLock.Wait();
flushContexts();
return new InvokeOnDisposal<RealmContextFactory>(this, endBlockingSection);
static void endBlockingSection(RealmContextFactory factory)
{
factory.blockingLock.Release();
Logger.Log(@"Restoring realm operations.", LoggingTarget.Database);
}
}
protected override void Update()
@ -87,15 +113,31 @@ namespace osu.Game.Database
private Realm createContext()
{
blockingResetEvent.Wait();
contexts_created.Value++;
return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true))
try
{
SchemaVersion = schema_version,
MigrationCallback = onMigration,
});
if (IsDisposed)
throw new ObjectDisposedException(nameof(RealmContextFactory));
blockingLock.Wait();
contexts_created.Value++;
return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true))
{
SchemaVersion = schema_version,
MigrationCallback = onMigration,
});
}
finally
{
blockingLock.Release();
}
}
private void writeComplete()
{
Monitor.Exit(writeLock);
pending_writes.Value--;
}
private void onMigration(Migration migration, ulong lastSchemaVersion)
@ -109,28 +151,10 @@ namespace osu.Game.Database
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
BlockAllOperations();
}
public IDisposable BlockAllOperations()
{
blockingResetEvent.Reset();
flushContexts();
return new InvokeOnDisposal<RealmContextFactory>(this, r => endBlockingSection());
}
private void endBlockingSection()
{
blockingResetEvent.Set();
}
private void flushContexts()
{
Logger.Log(@"Flushing realm contexts...", LoggingTarget.Database);
var previousContext = context;
context = null;
@ -139,6 +163,20 @@ namespace osu.Game.Database
Thread.Sleep(50);
previousContext?.Dispose();
Logger.Log(@"Realm contexts flushed.", LoggingTarget.Database);
}
protected override void Dispose(bool isDisposing)
{
if (!IsDisposed)
{
// intentionally block all operations indefinitely. this ensures that nothing can start consuming a new context after disposal.
BlockAllOperations();
blockingLock?.Dispose();
}
base.Dispose(isDisposing);
}
/// <summary>
@ -148,13 +186,10 @@ namespace osu.Game.Database
{
public readonly Realm Realm;
protected readonly RealmContextFactory Factory;
internal RealmUsage(RealmContextFactory factory)
internal RealmUsage(Realm context)
{
active_usages.Value++;
Factory = factory;
Realm = factory.createContext();
Realm = context;
}
/// <summary>
@ -172,11 +207,13 @@ namespace osu.Game.Database
/// </summary>
public class RealmWriteUsage : RealmUsage
{
private readonly Action onWriteComplete;
private readonly Transaction transaction;
internal RealmWriteUsage(RealmContextFactory factory)
: base(factory)
internal RealmWriteUsage(Realm context, Action onWriteComplete)
: base(context)
{
this.onWriteComplete = onWriteComplete;
transaction = Realm.BeginWrite();
}
@ -200,8 +237,7 @@ namespace osu.Game.Database
base.Dispose();
Monitor.Exit(Factory.writeLock);
pending_writes.Value--;
onWriteComplete();
}
}
}

View File

@ -4,12 +4,13 @@
using Markdig.Syntax.Inlines;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
namespace osu.Game.Graphics.Containers.Markdown
{
public class OsuMarkdownImage : MarkdownImage, IHasTooltip
{
public string TooltipText { get; }
public LocalisableString TooltipText { get; }
public OsuMarkdownImage(LinkInline linkInline)
: base(linkInline.Url)

View File

@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Graphics.Containers
@ -24,7 +25,7 @@ namespace osu.Game.Graphics.Containers
this.sampleSet = sampleSet;
}
public virtual string TooltipText { get; set; }
public virtual LocalisableString TooltipText { get; set; }
[BackgroundDependencyLoader]
private void load()

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Graphics.Cursor
@ -32,7 +33,7 @@ namespace osu.Game.Graphics.Cursor
public override bool SetContent(object content)
{
if (!(content is string contentString))
if (!(content is LocalisableString contentString))
return false;
if (contentString == text.Text) return true;

View File

@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Platform;
using osuTK;
using osuTK.Graphics;
@ -58,6 +59,6 @@ namespace osu.Game.Graphics.UserInterface
return true;
}
public string TooltipText => "view in browser";
public LocalisableString TooltipText => "view in browser";
}
}

View File

@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Platform;
namespace osu.Game.Graphics.UserInterface
@ -105,7 +106,7 @@ namespace osu.Game.Graphics.UserInterface
private class CapsWarning : SpriteIcon, IHasTooltip
{
public string TooltipText => @"caps lock is active";
public LocalisableString TooltipText => "caps lock is active";
public CapsWarning()
{

View File

@ -14,6 +14,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
namespace osu.Game.Graphics.UserInterface
{
@ -34,7 +35,7 @@ namespace osu.Game.Graphics.UserInterface
private readonly Box rightBox;
private readonly Container nubContainer;
public virtual string TooltipText { get; private set; }
public virtual LocalisableString TooltipText { get; private set; }
/// <summary>
/// Whether to format the tooltip as a percentage or the actual value.

View File

@ -9,11 +9,11 @@ namespace osu.Game.Online.API.Requests
{
public class PostMessageRequest : APIRequest<Message>
{
private readonly Message message;
public readonly Message Message;
public PostMessageRequest(Message message)
{
this.message = message;
Message = message;
}
protected override WebRequest CreateWebRequest()
@ -21,12 +21,12 @@ namespace osu.Game.Online.API.Requests
var req = base.CreateWebRequest();
req.Method = HttpMethod.Post;
req.AddParameter(@"is_action", message.IsAction.ToString().ToLowerInvariant());
req.AddParameter(@"message", message.Content);
req.AddParameter(@"is_action", Message.IsAction.ToString().ToLowerInvariant());
req.AddParameter(@"message", Message.Content);
return req;
}
protected override string Target => $@"chat/channels/{message.ChannelId}/messages";
protected override string Target => $@"chat/channels/{Message.ChannelId}/messages";
}
}

View File

@ -225,7 +225,7 @@ namespace osu.Game.Online.Chat
switch (command)
{
case "np":
AddInternal(new NowPlayingCommand());
AddInternal(new NowPlayingCommand(target));
break;
case "me":
@ -235,7 +235,7 @@ namespace osu.Game.Online.Chat
break;
}
PostMessage(content, true);
PostMessage(content, true, target);
break;
case "join":

View File

@ -21,6 +21,17 @@ namespace osu.Game.Online.Chat
[Resolved]
private Bindable<WorkingBeatmap> currentBeatmap { get; set; }
private readonly Channel target;
/// <summary>
/// Creates a new <see cref="NowPlayingCommand"/> to post the currently-playing beatmap to a parenting <see cref="IChannelPostTarget"/>.
/// </summary>
/// <param name="target">The target channel to post to. If <c>null</c>, the currently-selected channel will be posted to.</param>
public NowPlayingCommand(Channel target = null)
{
this.target = target;
}
protected override void LoadComplete()
{
base.LoadComplete();
@ -48,7 +59,7 @@ namespace osu.Game.Online.Chat
var beatmapString = beatmap.OnlineBeatmapID.HasValue ? $"[{api.WebsiteRootUrl}/b/{beatmap.OnlineBeatmapID} {beatmap}]" : beatmap.ToString();
channelManager.PostMessage($"is {verb} {beatmapString}", true);
channelManager.PostMessage($"is {verb} {beatmapString}", true, target);
Expire();
}
}

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@ -295,7 +296,7 @@ namespace osu.Game.Online.Leaderboards
public override bool Contains(Vector2 screenSpacePos) => content.Contains(screenSpacePos);
public string TooltipText { get; }
public LocalisableString TooltipText { get; }
public ScoreComponentLabel(LeaderboardScoreStatistic statistic)
{
@ -365,7 +366,7 @@ namespace osu.Game.Online.Leaderboards
};
}
public string TooltipText { get; }
public LocalisableString TooltipText { get; }
}
public class LeaderboardScoreStatistic

View File

@ -24,6 +24,7 @@ using osu.Framework.Graphics.Performance;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input;
using osu.Framework.Logging;
using osu.Framework.Threading;
using osu.Game.Audio;
using osu.Game.Database;
using osu.Game.Input;
@ -156,6 +157,8 @@ namespace osu.Game
private readonly BindableNumber<double> globalTrackVolumeAdjust = new BindableNumber<double>(GLOBAL_TRACK_VOLUME_ADJUST);
private IBindable<GameThreadState> updateThreadState;
public OsuGameBase()
{
UseDevelopmentServer = DebugUtils.IsDebugBuild;
@ -182,6 +185,10 @@ namespace osu.Game
dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage));
dependencies.Cache(realmFactory = new RealmContextFactory(Storage));
updateThreadState = Host.UpdateThread.State.GetBoundCopy();
updateThreadState.BindValueChanged(updateThreadStateChanged);
AddInternal(realmFactory);
dependencies.CacheAs(Storage);
@ -356,6 +363,23 @@ namespace osu.Game
Ruleset.BindValueChanged(onRulesetChanged);
}
private IDisposable blocking;
private void updateThreadStateChanged(ValueChangedEvent<GameThreadState> state)
{
switch (state.NewValue)
{
case GameThreadState.Running:
blocking?.Dispose();
blocking = null;
break;
case GameThreadState.Paused:
blocking = realmFactory.BlockAllOperations();
break;
}
}
protected override void LoadComplete()
{
base.LoadComplete();
@ -398,11 +422,15 @@ namespace osu.Game
public void Migrate(string path)
{
Logger.Log($@"Migrating osu! data from ""{Storage.GetFullPath(string.Empty)}"" to ""{path}""...");
using (realmFactory.BlockAllOperations())
{
contextFactory.FlushConnections();
(Storage as OsuStorage)?.Migrate(Host.GetStorage(path));
}
Logger.Log(@"Migration complete!");
}
protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager();

View File

@ -10,11 +10,13 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
using osu.Game.Resources.Localisation.Web;
using osuTK;
using osuTK.Graphics;
@ -23,9 +25,9 @@ namespace osu.Game.Overlays.BeatmapListing
public class BeatmapListingFilterControl : CompositeDrawable
{
/// <summary>
/// Fired when a search finishes. Contains only new items in the case of pagination.
/// Fired when a search finishes.
/// </summary>
public Action<List<BeatmapSetInfo>> SearchFinished;
public Action<SearchResult> SearchFinished;
/// <summary>
/// Fired when search criteria change.
@ -212,7 +214,25 @@ namespace osu.Game.Overlays.BeatmapListing
lastResponse = response;
getSetsRequest = null;
SearchFinished?.Invoke(sets);
// check if a non-supporter used supporter-only filters
if (!api.LocalUser.Value.IsSupporter)
{
List<LocalisableString> filters = new List<LocalisableString>();
if (searchControl.Played.Value != SearchPlayed.Any)
filters.Add(BeatmapsStrings.ListingSearchFiltersPlayed);
if (searchControl.Ranks.Any())
filters.Add(BeatmapsStrings.ListingSearchFiltersRank);
if (filters.Any())
{
SearchFinished?.Invoke(SearchResult.SupporterOnlyFilters(filters));
return;
}
}
SearchFinished?.Invoke(SearchResult.ResultsReturned(sets));
};
api.Queue(getSetsRequest);
@ -237,5 +257,53 @@ namespace osu.Game.Overlays.BeatmapListing
base.Dispose(isDisposing);
}
/// <summary>
/// Indicates the type of result of a user-requested beatmap search.
/// </summary>
public enum SearchResultType
{
/// <summary>
/// Actual results have been returned from API.
/// </summary>
ResultsReturned,
/// <summary>
/// The user is not a supporter, but used supporter-only search filters.
/// </summary>
SupporterOnlyFilters
}
/// <summary>
/// Describes the result of a user-requested beatmap search.
/// </summary>
public struct SearchResult
{
public SearchResultType Type { get; private set; }
/// <summary>
/// Contains the beatmap sets returned from API.
/// Valid for read if and only if <see cref="Type"/> is <see cref="SearchResultType.ResultsReturned"/>.
/// </summary>
public List<BeatmapSetInfo> Results { get; private set; }
/// <summary>
/// Contains the names of supporter-only filters requested by the user.
/// Valid for read if and only if <see cref="Type"/> is <see cref="SearchResultType.SupporterOnlyFilters"/>.
/// </summary>
public List<LocalisableString> SupporterOnlyFiltersUsed { get; private set; }
public static SearchResult ResultsReturned(List<BeatmapSetInfo> results) => new SearchResult
{
Type = SearchResultType.ResultsReturned,
Results = results
};
public static SearchResult SupporterOnlyFilters(List<LocalisableString> filters) => new SearchResult
{
Type = SearchResultType.SupporterOnlyFilters,
SupporterOnlyFiltersUsed = filters
};
}
}
}

View File

@ -7,6 +7,7 @@ using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Localisation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -15,7 +16,9 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.BeatmapListing.Panels;
using osu.Game.Resources.Localisation.Web;
@ -33,6 +36,7 @@ namespace osu.Game.Overlays
private Container panelTarget;
private FillFlowContainer<BeatmapPanel> foundContent;
private NotFoundDrawable notFoundContent;
private SupporterRequiredDrawable supporterRequiredContent;
private BeatmapListingFilterControl filterControl;
public BeatmapListingOverlay()
@ -76,6 +80,7 @@ namespace osu.Game.Overlays
{
foundContent = new FillFlowContainer<BeatmapPanel>(),
notFoundContent = new NotFoundDrawable(),
supporterRequiredContent = new SupporterRequiredDrawable(),
}
}
},
@ -115,9 +120,16 @@ namespace osu.Game.Overlays
private Task panelLoadDelegate;
private void onSearchFinished(List<BeatmapSetInfo> beatmaps)
private void onSearchFinished(BeatmapListingFilterControl.SearchResult searchResult)
{
var newPanels = beatmaps.Select<BeatmapSetInfo, BeatmapPanel>(b => new GridBeatmapPanel(b)
if (searchResult.Type == BeatmapListingFilterControl.SearchResultType.SupporterOnlyFilters)
{
supporterRequiredContent.UpdateText(searchResult.SupporterOnlyFiltersUsed);
addContentToPlaceholder(supporterRequiredContent);
return;
}
var newPanels = searchResult.Results.Select<BeatmapSetInfo, BeatmapPanel>(b => new GridBeatmapPanel(b)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
@ -128,7 +140,7 @@ namespace osu.Game.Overlays
//No matches case
if (!newPanels.Any())
{
LoadComponentAsync(notFoundContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
addContentToPlaceholder(notFoundContent);
return;
}
@ -170,9 +182,9 @@ namespace osu.Game.Overlays
{
var transform = lastContent.FadeOut(100, Easing.OutQuint);
if (lastContent == notFoundContent)
if (lastContent == notFoundContent || lastContent == supporterRequiredContent)
{
// not found display may be used multiple times, so don't expire/dispose it.
// the placeholders may be used multiple times, so don't expire/dispose them.
transform.Schedule(() => panelTarget.Remove(lastContent));
}
else
@ -240,6 +252,67 @@ namespace osu.Game.Overlays
}
}
// TODO: localisation requires Text/LinkFlowContainer support for localising strings with links inside
// (https://github.com/ppy/osu-framework/issues/4530)
public class SupporterRequiredDrawable : CompositeDrawable
{
private LinkFlowContainer supporterRequiredText;
public SupporterRequiredDrawable()
{
RelativeSizeAxes = Axes.X;
Height = 225;
Alpha = 0;
}
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
AddInternal(new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Texture = textures.Get(@"Online/supporter-required"),
},
supporterRequiredText = new LinkFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Bottom = 10 },
},
}
});
}
public void UpdateText(List<LocalisableString> filters)
{
supporterRequiredText.Clear();
supporterRequiredText.AddText(
BeatmapsStrings.ListingSearchSupporterFilterQuoteDefault(string.Join(" and ", filters), "").ToString(),
t =>
{
t.Font = OsuFont.GetFont(size: 16);
t.Colour = Colour4.White;
}
);
supporterRequiredText.AddLink(BeatmapsStrings.ListingSearchSupporterFilterQuoteLinkText.ToString(), @"/store/products/supporter-tag");
}
}
private const double time_between_fetches = 500;
private double lastFetchDisplayedTime;

View File

@ -95,7 +95,7 @@ namespace osu.Game.Overlays.BeatmapSet
{
private readonly OsuSpriteText value;
public string TooltipText { get; }
public LocalisableString TooltipText { get; }
public LocalisableString Value
{

View File

@ -7,6 +7,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
@ -28,7 +29,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
private readonly IBindable<User> localUser = new Bindable<User>();
public string TooltipText
public LocalisableString TooltipText
{
get
{

View File

@ -7,6 +7,7 @@ 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.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
@ -26,7 +27,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
private readonly bool noVideo;
public string TooltipText => button.Enabled.Value ? "download this beatmap" : "login to download";
public LocalisableString TooltipText => button.Enabled.Value ? "download this beatmap" : "login to download";
private readonly IBindable<User> localUser = new Bindable<User>();

View File

@ -20,6 +20,7 @@ using System;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Extensions.IEnumerableExtensions;
using System.Collections.Specialized;
using osu.Framework.Localisation;
using osu.Game.Overlays.Comments.Buttons;
namespace osu.Game.Overlays.Comments
@ -395,7 +396,7 @@ namespace osu.Game.Overlays.Comments
private class ParentUsername : FillFlowContainer, IHasTooltip
{
public string TooltipText => getParentMessage();
public LocalisableString TooltipText => getParentMessage();
private readonly Comment parentComment;
@ -427,7 +428,7 @@ namespace osu.Game.Overlays.Comments
if (parentComment == null)
return string.Empty;
return parentComment.HasMessage ? parentComment.Message : parentComment.IsDeleted ? @"deleted" : string.Empty;
return parentComment.HasMessage ? parentComment.Message : parentComment.IsDeleted ? "deleted" : string.Empty;
}
}
}

View File

@ -14,6 +14,7 @@ using System;
using System.Linq;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
@ -34,7 +35,7 @@ namespace osu.Game.Overlays.Mods
/// </summary>
public Action<Mod> SelectionChanged;
public string TooltipText => (SelectedMod?.Description ?? Mods.FirstOrDefault()?.Description) ?? string.Empty;
public LocalisableString TooltipText => (SelectedMod?.Description ?? Mods.FirstOrDefault()?.Description) ?? string.Empty;
private const Easing mod_switch_easing = Easing.InOutSine;
private const double mod_switch_duration = 120;

View File

@ -11,6 +11,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Framework.Allocation;
using osuTK.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
namespace osu.Game.Overlays
{
@ -56,7 +57,7 @@ namespace osu.Game.Overlays
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
public string TooltipText => $@"{Value} view";
public LocalisableString TooltipText => $@"{Value} view";
private readonly SpriteIcon icon;

View File

@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Localisation;
using osu.Game.Users;
using osuTK;
@ -42,6 +43,6 @@ namespace osu.Game.Overlays.Profile.Header.Components
InternalChild.FadeInFromZero(200);
}
public string TooltipText => badge.Description;
public LocalisableString TooltipText => badge.Description;
}
}

View File

@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osuTK;
namespace osu.Game.Overlays.Profile.Header.Components
@ -14,7 +15,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public readonly BindableBool DetailsVisible = new BindableBool();
public override string TooltipText => DetailsVisible.Value ? "collapse" : "expand";
public override LocalisableString TooltipText => DetailsVisible.Value ? "collapse" : "expand";
private SpriteIcon icon;

View File

@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Users;
namespace osu.Game.Overlays.Profile.Header.Components
@ -12,7 +13,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public readonly Bindable<User> User = new Bindable<User>();
public override string TooltipText => "followers";
public override LocalisableString TooltipText => "followers";
protected override IconUsage Icon => FontAwesome.Solid.User;

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Users;
@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public readonly Bindable<User> User = new Bindable<User>();
public string TooltipText { get; }
public LocalisableString TooltipText { get; }
private OsuSpriteText levelText;

View File

@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public readonly Bindable<User> User = new Bindable<User>();
public string TooltipText { get; }
public LocalisableString TooltipText { get; }
private Bar levelProgressBar;
private OsuSpriteText levelProgressText;

View File

@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Users;
namespace osu.Game.Overlays.Profile.Header.Components
@ -12,7 +13,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public readonly Bindable<User> User = new Bindable<User>();
public override string TooltipText => "mapping subscribers";
public override LocalisableString TooltipText => "mapping subscribers";
protected override IconUsage Icon => FontAwesome.Solid.Bell;

View File

@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.Chat;
using osu.Game.Users;
@ -16,7 +17,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public readonly Bindable<User> User = new Bindable<User>();
public override string TooltipText => "send message";
public override LocalisableString TooltipText => "send message";
[Resolved(CanBeNull = true)]
private ChannelManager channelManager { get; set; }

View File

@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
using osu.Game.Users;
namespace osu.Game.Overlays.Profile.Header.Components
@ -14,7 +15,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public readonly Bindable<User> User = new Bindable<User>();
public string TooltipText { get; set; }
public LocalisableString TooltipText { get; set; }
private OverlinedInfoContainer info;

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Overlays.Profile.Header.Components
@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
private readonly FillFlowContainer iconContainer;
private readonly CircularContainer content;
public string TooltipText => "osu!supporter";
public LocalisableString TooltipText => "osu!supporter";
public int SupportLevel
{

View File

@ -143,7 +143,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
private class PlayCountText : CompositeDrawable, IHasTooltip
{
public string TooltipText => "times played";
public LocalisableString TooltipText => "times played";
public PlayCountText(int playCount)
{

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
@ -76,7 +77,7 @@ namespace osu.Game.Overlays
UpdateState();
}
public string TooltipText => "revert to default";
public LocalisableString TooltipText => "revert to default";
protected override bool OnHover(HoverEvent e)
{

View File

@ -0,0 +1,49 @@
// 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.Extensions.Color4Extensions;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osuTK.Graphics;
namespace osu.Game.Overlays.Settings
{
public class OutlinedTextBox : OsuTextBox
{
private const float border_thickness = 3;
private Color4 borderColourFocused;
private Color4 borderColourUnfocused;
[BackgroundDependencyLoader]
private void load(OsuColour colour)
{
borderColourUnfocused = colour.Gray4.Opacity(0.5f);
borderColourFocused = BorderColour;
updateBorder();
}
protected override void OnFocus(FocusEvent e)
{
base.OnFocus(e);
updateBorder();
}
protected override void OnFocusLost(FocusLostEvent e)
{
base.OnFocusLost(e);
updateBorder();
}
private void updateBorder()
{
BorderThickness = border_thickness;
BorderColour = HasFocus ? borderColourFocused : borderColourUnfocused;
}
}
}

View File

@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
@ -32,7 +33,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
private class OffsetSlider : OsuSliderBar<double>
{
public override string TooltipText => Current.Value.ToString(@"0ms");
public override LocalisableString TooltipText => Current.Value.ToString(@"0ms");
}
}
}

View File

@ -233,7 +233,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private class UIScaleSlider : OsuSliderBar<float>
{
public override string TooltipText => base.TooltipText + "x";
public override LocalisableString TooltipText => base.TooltipText + "x";
}
private class ResolutionSettingsDropdown : SettingsDropdown<Size>

View File

@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Input.Handlers.Mouse;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input;
@ -116,7 +117,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
private class SensitivitySlider : OsuSliderBar<double>
{
public override string TooltipText => Current.Disabled ? "enable high precision mouse to adjust sensitivity" : $"{base.TooltipText}x";
public override LocalisableString TooltipText => Current.Disabled ? "enable high precision mouse to adjust sensitivity" : $"{base.TooltipText}x";
}
}
}

View File

@ -1,6 +1,7 @@
// 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.Localisation;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings.Sections
@ -10,6 +11,6 @@ namespace osu.Game.Overlays.Settings.Sections
/// </summary>
internal class SizeSlider : OsuSliderBar<float>
{
public override string TooltipText => Current.Value.ToString(@"0.##x");
public override LocalisableString TooltipText => Current.Value.ToString(@"0.##x");
}
}

View File

@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
@ -44,7 +45,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
private class TimeSlider : OsuSliderBar<float>
{
public override string TooltipText => Current.Value.ToString("N0") + "ms";
public override LocalisableString TooltipText => Current.Value.ToString(@"N0") + "ms";
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
@ -62,12 +63,12 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
private class MaximumStarsSlider : StarsSlider
{
public override string TooltipText => Current.IsDefault ? "no limit" : base.TooltipText;
public override LocalisableString TooltipText => Current.IsDefault ? "no limit" : base.TooltipText;
}
private class StarsSlider : OsuSliderBar<double>
{
public override string TooltipText => Current.Value.ToString(@"0.## stars");
public override LocalisableString TooltipText => Current.Value.ToString(@"0.## stars");
}
}
}

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings
@ -17,14 +18,15 @@ namespace osu.Game.Overlays.Settings
Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS };
}
public string TooltipText { get; set; }
public LocalisableString TooltipText { get; set; }
public override IEnumerable<string> FilterTerms
{
get
{
if (TooltipText != null)
return base.FilterTerms.Append(TooltipText);
if (TooltipText != default)
// TODO: this won't work as intended once the tooltip text is translated.
return base.FilterTerms.Append(TooltipText.ToString());
return base.FilterTerms;
}

View File

@ -35,7 +35,7 @@ namespace osu.Game.Overlays.Settings
public bool ShowsDefaultIndicator = true;
public string TooltipText { get; set; }
public LocalisableString TooltipText { get; set; }
[Resolved]
private OsuColour colours { get; set; }

View File

@ -1,19 +1,65 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings
{
public class SettingsNumberBox : SettingsItem<string>
public class SettingsNumberBox : SettingsItem<int?>
{
protected override Drawable CreateControl() => new NumberBox
protected override Drawable CreateControl() => new NumberControl
{
Margin = new MarginPadding { Top = 5 },
RelativeSizeAxes = Axes.X,
Margin = new MarginPadding { Top = 5 }
};
public class NumberBox : SettingsTextBox.TextBox
private sealed class NumberControl : CompositeDrawable, IHasCurrentValue<int?>
{
private readonly BindableWithCurrent<int?> current = new BindableWithCurrent<int?>();
public Bindable<int?> Current
{
get => current.Current;
set => current.Current = value;
}
public NumberControl()
{
AutoSizeAxes = Axes.Y;
OutlinedNumberBox numberBox;
InternalChildren = new[]
{
numberBox = new OutlinedNumberBox
{
Margin = new MarginPadding { Top = 5 },
RelativeSizeAxes = Axes.X,
CommitOnFocusLost = true
}
};
numberBox.Current.BindValueChanged(e =>
{
int? value = null;
if (int.TryParse(e.NewValue, out var intVal))
value = intVal;
current.Value = value;
});
Current.BindValueChanged(e =>
{
numberBox.Current.Value = e.NewValue?.ToString();
});
}
}
private class OutlinedNumberBox : OutlinedTextBox
{
protected override bool CanAddCharacter(char character) => char.IsNumber(character);
}

View File

@ -1,60 +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 osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osuTK.Graphics;
namespace osu.Game.Overlays.Settings
{
public class SettingsTextBox : SettingsItem<string>
{
protected override Drawable CreateControl() => new TextBox
protected override Drawable CreateControl() => new OutlinedTextBox
{
Margin = new MarginPadding { Top = 5 },
RelativeSizeAxes = Axes.X,
CommitOnFocusLost = true,
CommitOnFocusLost = true
};
public class TextBox : OsuTextBox
{
private const float border_thickness = 3;
private Color4 borderColourFocused;
private Color4 borderColourUnfocused;
[BackgroundDependencyLoader]
private void load(OsuColour colour)
{
borderColourUnfocused = colour.Gray4.Opacity(0.5f);
borderColourFocused = BorderColour;
updateBorder();
}
protected override void OnFocus(FocusEvent e)
{
base.OnFocus(e);
updateBorder();
}
protected override void OnFocusLost(FocusLostEvent e)
{
base.OnFocusLost(e);
updateBorder();
}
private void updateBorder()
{
BorderThickness = border_thickness;
BorderColour = HasFocus ? borderColourFocused : borderColourUnfocused;
}
}
}
}

View File

@ -159,28 +159,6 @@ namespace osu.Game.Overlays.Toolbar
};
}
private RealmKeyBinding realmKeyBinding;
protected override void LoadComplete()
{
base.LoadComplete();
if (Hotkey == null) return;
realmKeyBinding = realmFactory.Context.All<RealmKeyBinding>().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.ActionInt == (int)Hotkey.Value);
if (realmKeyBinding != null)
{
realmKeyBinding.PropertyChanged += (sender, args) =>
{
if (args.PropertyName == nameof(realmKeyBinding.KeyCombinationString))
updateKeyBindingTooltip();
};
}
updateKeyBindingTooltip();
}
protected override bool OnMouseDown(MouseDownEvent e) => true;
protected override bool OnClick(ClickEvent e)
@ -196,6 +174,7 @@ namespace osu.Game.Overlays.Toolbar
HoverBackground.FadeIn(200);
tooltipContainer.FadeIn(100);
return base.OnHover(e);
}
@ -222,6 +201,10 @@ namespace osu.Game.Overlays.Toolbar
private void updateKeyBindingTooltip()
{
if (Hotkey == null) return;
var realmKeyBinding = realmFactory.Context.All<RealmKeyBinding>().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.ActionInt == (int)Hotkey.Value);
if (realmKeyBinding != null)
{
var keyBindingString = realmKeyBinding.KeyCombination.ReadableString();

View File

@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Edit
// Audio
new CheckAudioPresence(),
new CheckAudioQuality(),
new CheckMutedObjects(),
new CheckFewHitsounds(),
// Compose
new CheckUnsnappedObjects(),

View File

@ -0,0 +1,164 @@
// 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.Audio;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Edit.Checks
{
public class CheckFewHitsounds : ICheck
{
/// <summary>
/// 2 measures (4/4) of 120 BPM, typically makes up a few patterns in the map.
/// This is almost always ok, but can still be useful for the mapper to make sure hitsounding coverage is good.
/// </summary>
private const int negligible_threshold_time = 4000;
/// <summary>
/// 4 measures (4/4) of 120 BPM, typically makes up a large portion of a section in the song.
/// This is ok if the section is a quiet intro, for example.
/// </summary>
private const int warning_threshold_time = 8000;
/// <summary>
/// 12 measures (4/4) of 120 BPM, typically makes up multiple sections in the song.
/// </summary>
private const int problem_threshold_time = 24000;
// Should pass at least this many objects without hitsounds to be considered an issue (should work for Easy diffs too).
private const int warning_threshold_objects = 4;
private const int problem_threshold_objects = 16;
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Audio, "Few or no hitsounds");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateLongPeriodProblem(this),
new IssueTemplateLongPeriodWarning(this),
new IssueTemplateLongPeriodNegligible(this),
new IssueTemplateNoHitsounds(this)
};
private bool mapHasHitsounds;
private int objectsWithoutHitsounds;
private double lastHitsoundTime;
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
if (!context.Beatmap.HitObjects.Any())
yield break;
mapHasHitsounds = false;
objectsWithoutHitsounds = 0;
lastHitsoundTime = context.Beatmap.HitObjects.First().StartTime;
var hitObjectsIncludingNested = new List<HitObject>();
foreach (var hitObject in context.Beatmap.HitObjects)
{
// Samples play on the end of objects. Some objects have nested objects to accomplish playing them elsewhere (e.g. slider head/repeat).
foreach (var nestedHitObject in hitObject.NestedHitObjects)
hitObjectsIncludingNested.Add(nestedHitObject);
hitObjectsIncludingNested.Add(hitObject);
}
var hitObjectsByEndTime = hitObjectsIncludingNested.OrderBy(o => o.GetEndTime()).ToList();
var hitObjectCount = hitObjectsByEndTime.Count;
for (int i = 0; i < hitObjectCount; ++i)
{
var hitObject = hitObjectsByEndTime[i];
// This is used to perform an update at the end so that the period after the last hitsounded object can be an issue.
bool isLastObject = i == hitObjectCount - 1;
foreach (var issue in applyHitsoundUpdate(hitObject, isLastObject))
yield return issue;
}
if (!mapHasHitsounds)
yield return new IssueTemplateNoHitsounds(this).Create();
}
private IEnumerable<Issue> applyHitsoundUpdate(HitObject hitObject, bool isLastObject = false)
{
var time = hitObject.GetEndTime();
bool hasHitsound = hitObject.Samples.Any(isHitsound);
bool couldHaveHitsound = hitObject.Samples.Any(isHitnormal);
// Only generating issues on hitsounded or last objects ensures we get one issue per long period.
// If there are no hitsounds we let the "No hitsounds" template take precedence.
if (hasHitsound || (isLastObject && mapHasHitsounds))
{
var timeWithoutHitsounds = time - lastHitsoundTime;
if (timeWithoutHitsounds > problem_threshold_time && objectsWithoutHitsounds > problem_threshold_objects)
yield return new IssueTemplateLongPeriodProblem(this).Create(lastHitsoundTime, timeWithoutHitsounds);
else if (timeWithoutHitsounds > warning_threshold_time && objectsWithoutHitsounds > warning_threshold_objects)
yield return new IssueTemplateLongPeriodWarning(this).Create(lastHitsoundTime, timeWithoutHitsounds);
else if (timeWithoutHitsounds > negligible_threshold_time && objectsWithoutHitsounds > warning_threshold_objects)
yield return new IssueTemplateLongPeriodNegligible(this).Create(lastHitsoundTime, timeWithoutHitsounds);
}
if (hasHitsound)
{
mapHasHitsounds = true;
objectsWithoutHitsounds = 0;
lastHitsoundTime = time;
}
else if (couldHaveHitsound)
++objectsWithoutHitsounds;
}
private bool isHitsound(HitSampleInfo sample) => HitSampleInfo.AllAdditions.Any(sample.Name.Contains);
private bool isHitnormal(HitSampleInfo sample) => sample.Name.Contains(HitSampleInfo.HIT_NORMAL);
public abstract class IssueTemplateLongPeriod : IssueTemplate
{
protected IssueTemplateLongPeriod(ICheck check, IssueType type)
: base(check, type, "Long period without hitsounds ({0:F1} seconds).")
{
}
public Issue Create(double time, double duration) => new Issue(this, duration / 1000f) { Time = time };
}
public class IssueTemplateLongPeriodProblem : IssueTemplateLongPeriod
{
public IssueTemplateLongPeriodProblem(ICheck check)
: base(check, IssueType.Problem)
{
}
}
public class IssueTemplateLongPeriodWarning : IssueTemplateLongPeriod
{
public IssueTemplateLongPeriodWarning(ICheck check)
: base(check, IssueType.Warning)
{
}
}
public class IssueTemplateLongPeriodNegligible : IssueTemplateLongPeriod
{
public IssueTemplateLongPeriodNegligible(ICheck check)
: base(check, IssueType.Negligible)
{
}
}
public class IssueTemplateNoHitsounds : IssueTemplate
{
public IssueTemplateNoHitsounds(ICheck check)
: base(check, IssueType.Problem, "There are no hitsounds.")
{
}
public Issue Create() => new Issue(this);
}
}
}

View File

@ -0,0 +1,158 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Utils;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Edit.Checks
{
public class CheckMutedObjects : ICheck
{
/// <summary>
/// Volume percentages lower than or equal to this are typically inaudible.
/// </summary>
private const int muted_threshold = 5;
/// <summary>
/// Volume percentages lower than or equal to this can sometimes be inaudible depending on sample used and music volume.
/// </summary>
private const int low_volume_threshold = 20;
private enum EdgeType
{
Head,
Repeat,
Tail,
None
}
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Audio, "Low volume hitobjects");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateMutedActive(this),
new IssueTemplateLowVolumeActive(this),
new IssueTemplateMutedPassive(this)
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
foreach (var hitObject in context.Beatmap.HitObjects)
{
// Worth keeping in mind: The samples of an object always play at its end time.
// Objects like spinners have no sound at its start because of this, while hold notes have nested objects to accomplish this.
foreach (var nestedHitObject in hitObject.NestedHitObjects)
{
foreach (var issue in getVolumeIssues(hitObject, nestedHitObject))
yield return issue;
}
foreach (var issue in getVolumeIssues(hitObject))
yield return issue;
}
}
private IEnumerable<Issue> getVolumeIssues(HitObject hitObject, HitObject sampledHitObject = null)
{
sampledHitObject ??= hitObject;
if (!sampledHitObject.Samples.Any())
yield break;
// Samples that allow themselves to be overridden by control points have a volume of 0.
int maxVolume = sampledHitObject.Samples.Max(sample => sample.Volume > 0 ? sample.Volume : sampledHitObject.SampleControlPoint.SampleVolume);
double samplePlayTime = sampledHitObject.GetEndTime();
EdgeType edgeType = getEdgeAtTime(hitObject, samplePlayTime);
// We only care about samples played on the edges of objects, not ones like spinnerspin or slidertick.
if (edgeType == EdgeType.None)
yield break;
string postfix = hitObject is IHasDuration ? edgeType.ToString().ToLower() : null;
if (maxVolume <= muted_threshold)
{
if (edgeType == EdgeType.Head)
yield return new IssueTemplateMutedActive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix);
else
yield return new IssueTemplateMutedPassive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix);
}
else if (maxVolume <= low_volume_threshold && edgeType == EdgeType.Head)
{
yield return new IssueTemplateLowVolumeActive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix);
}
}
private EdgeType getEdgeAtTime(HitObject hitObject, double time)
{
if (Precision.AlmostEquals(time, hitObject.StartTime, 1f))
return EdgeType.Head;
if (Precision.AlmostEquals(time, hitObject.GetEndTime(), 1f))
return EdgeType.Tail;
if (hitObject is IHasRepeats hasRepeats)
{
double spanDuration = hasRepeats.Duration / hasRepeats.SpanCount();
if (spanDuration <= 0)
// Prevents undefined behaviour in cases like where zero/negative-length sliders/hold notes exist.
return EdgeType.None;
double spans = (time - hitObject.StartTime) / spanDuration;
double acceptableDifference = 1 / spanDuration; // 1 ms of acceptable difference, as with head/tail above.
if (Precision.AlmostEquals(spans, Math.Ceiling(spans), acceptableDifference) ||
Precision.AlmostEquals(spans, Math.Floor(spans), acceptableDifference))
{
return EdgeType.Repeat;
}
}
return EdgeType.None;
}
public abstract class IssueTemplateMuted : IssueTemplate
{
protected IssueTemplateMuted(ICheck check, IssueType type, string unformattedMessage)
: base(check, type, unformattedMessage)
{
}
public Issue Create(HitObject hitobject, double volume, double time, string postfix = "")
{
string objectName = hitobject.GetType().Name;
if (!string.IsNullOrEmpty(postfix))
objectName += " " + postfix;
return new Issue(hitobject, this, objectName, volume) { Time = time };
}
}
public class IssueTemplateMutedActive : IssueTemplateMuted
{
public IssueTemplateMutedActive(ICheck check)
: base(check, IssueType.Problem, "{0} has a volume of {1:0%}. Clickable objects must have clearly audible feedback.")
{
}
}
public class IssueTemplateLowVolumeActive : IssueTemplateMuted
{
public IssueTemplateLowVolumeActive(ICheck check)
: base(check, IssueType.Warning, "{0} has a volume of {1:0%}, ensure this is audible.")
{
}
}
public class IssueTemplateMutedPassive : IssueTemplateMuted
{
public IssueTemplateMutedPassive(ICheck check)
: base(check, IssueType.Negligible, "{0} has a volume of {1:0%}, ensure there is no distinct sound here in the song if inaudible.")
{
}
}
}
}

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