diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 0c6b80e97e..fc61573416 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1 +1,2 @@
+github: ppy
custom: https://osu.ppy.sh/home/support
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 29cbdd2d37..0da1f9636b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -79,9 +79,14 @@ jobs:
run: |
# TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
# FIXME: Suppress warnings from templates project
- dotnet codefilesanity | while read -r line; do
- echo "::warning::$line"
- done
+ exit_code=0
+ while read -r line; do
+ if [[ ! -z "$line" ]]; then
+ echo "::error::$line"
+ exit_code=1
+ fi
+ done <<< $(dotnet codefilesanity)
+ exit $exit_code
# Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
# - name: .NET Format (Dry Run)
diff --git a/osu.Android.props b/osu.Android.props
index 956093b2ac..db62667fc2 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,7 +52,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
index 0ba775e5c7..37f1a846ad 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs
@@ -45,8 +45,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
{
new Spinner
{
- Duration = 2000,
- Position = OsuPlayfield.BASE_SIZE / 2
+ Duration = 6000,
+ Position = OsuPlayfield.BASE_SIZE / 2,
}
}
},
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
index 9da583a073..52ab39cfbd 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs
@@ -30,6 +30,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneSpinnerRotation : TestSceneOsuPlayer
{
+ private const double spinner_start_time = 100;
+ private const double spinner_duration = 6000;
+
[Resolved]
private AudioManager audioManager { get; set; }
@@ -77,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Tests
double finalTrackerRotation = 0, trackerRotationTolerance = 0;
double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
- addSeekStep(5000);
+ addSeekStep(spinner_start_time + 5000);
AddStep("retrieve disc rotation", () =>
{
finalTrackerRotation = drawableSpinner.RotationTracker.Rotation;
@@ -90,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests
});
AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation);
- addSeekStep(2500);
+ addSeekStep(spinner_start_time + 2500);
AddAssert("disc rotation rewound",
// we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in.
// due to the exponential damping applied we're allowing a larger margin of error of about 10%
@@ -102,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Tests
// cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
() => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100));
- addSeekStep(5000);
+ addSeekStep(spinner_start_time + 5000);
AddAssert("is disc rotation almost same",
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance));
AddAssert("is symbol rotation almost same",
@@ -140,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test]
public void TestSpinnerNormalBonusRewinding()
{
- addSeekStep(1000);
+ addSeekStep(spinner_start_time + 1000);
AddAssert("player score matching expected bonus score", () =>
{
@@ -201,24 +204,9 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0));
}
- private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay
- {
- Frames = scoreReplay
- .Frames
- .Cast()
- .Select(replayFrame =>
- {
- var adjustedTime = replayFrame.Time * rate;
- return new OsuReplayFrame(adjustedTime, replayFrame.Position, replayFrame.Actions.ToArray());
- })
- .Cast()
- .ToList()
- };
-
private void addSeekStep(double time)
{
AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
-
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
@@ -241,7 +229,8 @@ namespace osu.Game.Rulesets.Osu.Tests
new Spinner
{
Position = new Vector2(256, 192),
- EndTime = 6000,
+ StartTime = spinner_start_time,
+ Duration = spinner_duration
},
}
};
diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs
index 245981cd9b..3e8b6091fd 100644
--- a/osu.Game.Tests/Database/GeneralUsageTests.cs
+++ b/osu.Game.Tests/Database/GeneralUsageTests.cs
@@ -1,3 +1,6 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
using System;
using System.Threading;
using System.Threading.Tasks;
diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs
new file mode 100644
index 0000000000..33aa1afb89
--- /dev/null
+++ b/osu.Game.Tests/Database/RealmLiveTests.cs
@@ -0,0 +1,213 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Database;
+using osu.Game.Models;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Tests.Database
+{
+ public class RealmLiveTests : RealmTest
+ {
+ [Test]
+ public void TestLiveCastability()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ RealmLive beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive();
+
+ ILive iBeatmap = beatmap;
+
+ Assert.AreEqual(0, iBeatmap.Value.Length);
+ });
+ }
+
+ [Test]
+ public void TestValueAccessWithOpenContext()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ RealmLive? liveBeatmap = null;
+ Task.Factory.StartNew(() =>
+ {
+ using (var threadContext = realmFactory.CreateContext())
+ {
+ var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ liveBeatmap = beatmap.ToLive();
+ }
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+
+ Debug.Assert(liveBeatmap != null);
+
+ Task.Factory.StartNew(() =>
+ {
+ Assert.DoesNotThrow(() =>
+ {
+ using (realmFactory.CreateContext())
+ {
+ var resolved = liveBeatmap.Value;
+
+ Assert.IsTrue(resolved.Realm.IsClosed);
+ Assert.IsTrue(resolved.IsValid);
+
+ // can access properties without a crash.
+ Assert.IsFalse(resolved.Hidden);
+ }
+ });
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+ });
+ }
+
+ [Test]
+ public void TestScopedReadWithoutContext()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ RealmLive? liveBeatmap = null;
+ Task.Factory.StartNew(() =>
+ {
+ using (var threadContext = realmFactory.CreateContext())
+ {
+ var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ liveBeatmap = beatmap.ToLive();
+ }
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+
+ Debug.Assert(liveBeatmap != null);
+
+ Task.Factory.StartNew(() =>
+ {
+ liveBeatmap.PerformRead(beatmap =>
+ {
+ Assert.IsTrue(beatmap.IsValid);
+ Assert.IsFalse(beatmap.Hidden);
+ });
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+ });
+ }
+
+ [Test]
+ public void TestScopedWriteWithoutContext()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ RealmLive? liveBeatmap = null;
+ Task.Factory.StartNew(() =>
+ {
+ using (var threadContext = realmFactory.CreateContext())
+ {
+ var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ liveBeatmap = beatmap.ToLive();
+ }
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+
+ Debug.Assert(liveBeatmap != null);
+
+ Task.Factory.StartNew(() =>
+ {
+ liveBeatmap.PerformWrite(beatmap => { beatmap.Hidden = true; });
+ liveBeatmap.PerformRead(beatmap => { Assert.IsTrue(beatmap.Hidden); });
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+ });
+ }
+
+ [Test]
+ public void TestValueAccessWithoutOpenContextFails()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ RealmLive? liveBeatmap = null;
+ Task.Factory.StartNew(() =>
+ {
+ using (var threadContext = realmFactory.CreateContext())
+ {
+ var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ liveBeatmap = beatmap.ToLive();
+ }
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+
+ Debug.Assert(liveBeatmap != null);
+
+ Task.Factory.StartNew(() =>
+ {
+ Assert.Throws(() =>
+ {
+ var unused = liveBeatmap.Value;
+ });
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+ });
+ }
+
+ [Test]
+ public void TestLiveAssumptions()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ int changesTriggered = 0;
+
+ using (var updateThreadContext = realmFactory.CreateContext())
+ {
+ updateThreadContext.All().SubscribeForNotifications(gotChange);
+ RealmLive? liveBeatmap = null;
+
+ Task.Factory.StartNew(() =>
+ {
+ using (var threadContext = realmFactory.CreateContext())
+ {
+ var ruleset = CreateRuleset();
+ var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ // add a second beatmap to ensure that a full refresh occurs below.
+ // not just a refresh from the resolved Live.
+ threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
+
+ liveBeatmap = beatmap.ToLive();
+ }
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+
+ Debug.Assert(liveBeatmap != null);
+
+ // not yet seen by main context
+ Assert.AreEqual(0, updateThreadContext.All().Count());
+ Assert.AreEqual(0, changesTriggered);
+
+ var resolved = liveBeatmap.Value;
+
+ // retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
+ Assert.AreEqual(2, updateThreadContext.All().Count());
+ Assert.AreEqual(1, changesTriggered);
+
+ // even though the realm that this instance was resolved for was closed, it's still valid.
+ Assert.IsTrue(resolved.Realm.IsClosed);
+ Assert.IsTrue(resolved.IsValid);
+
+ // can access properties without a crash.
+ Assert.IsFalse(resolved.Hidden);
+
+ updateThreadContext.Write(r =>
+ {
+ // can use with the main context.
+ r.Remove(resolved);
+ });
+ }
+
+ void gotChange(IRealmCollection sender, ChangeSet changes, Exception error)
+ {
+ changesTriggered++;
+ }
+ });
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs b/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs
new file mode 100644
index 0000000000..f3a4f10210
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs
@@ -0,0 +1,104 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Moq;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Storyboards;
+using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Resources;
+using FileInfo = osu.Game.IO.FileInfo;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ [TestFixture]
+ public class CheckAudioInVideoTest
+ {
+ private CheckAudioInVideo check;
+ private IBeatmap beatmap;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckAudioInVideo();
+ beatmap = new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ BeatmapSet = new BeatmapSetInfo
+ {
+ Files = new List(new[]
+ {
+ new BeatmapSetFileInfo
+ {
+ Filename = "abc123.mp4",
+ FileInfo = new FileInfo { Hash = "abcdef" }
+ }
+ })
+ }
+ }
+ };
+ }
+
+ [Test]
+ public void TestRegularVideoFile()
+ {
+ using (var resourceStream = TestResources.OpenResource("Videos/test-video.mp4"))
+ Assert.IsEmpty(check.Run(getContext(resourceStream)));
+ }
+
+ [Test]
+ public void TestVideoFileWithAudio()
+ {
+ using (var resourceStream = TestResources.OpenResource("Videos/test-video-with-audio.mp4"))
+ {
+ var issues = check.Run(getContext(resourceStream)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateHasAudioTrack);
+ }
+ }
+
+ [Test]
+ public void TestVideoFileWithTrackButNoAudio()
+ {
+ using (var resourceStream = TestResources.OpenResource("Videos/test-video-with-track-but-no-audio.mp4"))
+ {
+ var issues = check.Run(getContext(resourceStream)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateHasAudioTrack);
+ }
+ }
+
+ [Test]
+ public void TestMissingFile()
+ {
+ beatmap.BeatmapInfo.BeatmapSet.Files.Clear();
+
+ var issues = check.Run(getContext(null)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateMissingFile);
+ }
+
+ private BeatmapVerifierContext getContext(Stream resourceStream)
+ {
+ var storyboard = new Storyboard();
+ var layer = storyboard.GetLayer("Video");
+ layer.Add(new StoryboardVideo("abc123.mp4", 0));
+
+ var mockWorkingBeatmap = new Mock(beatmap, null, null);
+ mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(resourceStream);
+ mockWorkingBeatmap.As().SetupGet(w => w.Storyboard).Returns(storyboard);
+
+ return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs
new file mode 100644
index 0000000000..9b090591bc
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs
@@ -0,0 +1,128 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using ManagedBass;
+using Moq;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Tests.Beatmaps;
+using osu.Game.Tests.Resources;
+using osuTK.Audio;
+using FileInfo = osu.Game.IO.FileInfo;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ [TestFixture]
+ public class CheckTooShortAudioFilesTest
+ {
+ private CheckTooShortAudioFiles check;
+ private IBeatmap beatmap;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckTooShortAudioFiles();
+ beatmap = new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ BeatmapSet = new BeatmapSetInfo
+ {
+ Files = new List(new[]
+ {
+ new BeatmapSetFileInfo
+ {
+ Filename = "abc123.wav",
+ FileInfo = new FileInfo { Hash = "abcdef" }
+ }
+ })
+ }
+ }
+ };
+
+ // 0 = No output device. This still allows decoding.
+ if (!Bass.Init(0) && Bass.LastError != Errors.Already)
+ throw new AudioException("Could not initialize Bass.");
+ }
+
+ [Test]
+ public void TestDifferentExtension()
+ {
+ beatmap.BeatmapInfo.BeatmapSet.Files.Clear();
+ beatmap.BeatmapInfo.BeatmapSet.Files.Add(new BeatmapSetFileInfo
+ {
+ Filename = "abc123.jpg",
+ FileInfo = new FileInfo { Hash = "abcdef" }
+ });
+
+ // Should fail to load, but not produce an error due to the extension not being expected to load.
+ Assert.IsEmpty(check.Run(getContext(null, allowMissing: true)));
+ }
+
+ [Test]
+ public void TestRegularAudioFile()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/test-sample.mp3"))
+ {
+ Assert.IsEmpty(check.Run(getContext(resourceStream)));
+ }
+ }
+
+ [Test]
+ public void TestBlankAudioFile()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/blank.wav"))
+ {
+ // This is a 0 ms duration audio file, commonly used to silence sliderslides/ticks, and so should be fine.
+ Assert.IsEmpty(check.Run(getContext(resourceStream)));
+ }
+ }
+
+ [Test]
+ public void TestTooShortAudioFile()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/test-sample-cut.mp3"))
+ {
+ var issues = check.Run(getContext(resourceStream)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckTooShortAudioFiles.IssueTemplateTooShort);
+ }
+ }
+
+ [Test]
+ public void TestMissingAudioFile()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/missing.mp3"))
+ {
+ Assert.IsEmpty(check.Run(getContext(resourceStream, allowMissing: true)));
+ }
+ }
+
+ [Test]
+ public void TestCorruptAudioFile()
+ {
+ using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav"))
+ {
+ var issues = check.Run(getContext(resourceStream)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckTooShortAudioFiles.IssueTemplateBadFormat);
+ }
+ }
+
+ private BeatmapVerifierContext getContext(Stream resourceStream, bool allowMissing = false)
+ {
+ var mockWorkingBeatmap = new Mock(beatmap, null, null);
+ mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(resourceStream);
+
+ return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Editing/Checks/CheckZeroByteFilesTest.cs b/osu.Game.Tests/Editing/Checks/CheckZeroByteFilesTest.cs
new file mode 100644
index 0000000000..c9adc030c1
--- /dev/null
+++ b/osu.Game.Tests/Editing/Checks/CheckZeroByteFilesTest.cs
@@ -0,0 +1,86 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Moq;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Checks;
+using osu.Game.Rulesets.Objects;
+using FileInfo = osu.Game.IO.FileInfo;
+
+namespace osu.Game.Tests.Editing.Checks
+{
+ [TestFixture]
+ public class CheckZeroByteFilesTest
+ {
+ private CheckZeroByteFiles check;
+ private IBeatmap beatmap;
+
+ [SetUp]
+ public void Setup()
+ {
+ check = new CheckZeroByteFiles();
+ beatmap = new Beatmap
+ {
+ BeatmapInfo = new BeatmapInfo
+ {
+ BeatmapSet = new BeatmapSetInfo
+ {
+ Files = new List(new[]
+ {
+ new BeatmapSetFileInfo
+ {
+ Filename = "abc123.jpg",
+ FileInfo = new FileInfo { Hash = "abcdef" }
+ }
+ })
+ }
+ }
+ };
+ }
+
+ [Test]
+ public void TestNonZeroBytes()
+ {
+ Assert.IsEmpty(check.Run(getContext(byteLength: 44)));
+ }
+
+ [Test]
+ public void TestZeroBytes()
+ {
+ var issues = check.Run(getContext(byteLength: 0)).ToList();
+
+ Assert.That(issues, Has.Count.EqualTo(1));
+ Assert.That(issues.Single().Template is CheckZeroByteFiles.IssueTemplateZeroBytes);
+ }
+
+ [Test]
+ public void TestMissing()
+ {
+ Assert.IsEmpty(check.Run(getContextMissing()));
+ }
+
+ private BeatmapVerifierContext getContext(long byteLength)
+ {
+ var mockStream = new Mock();
+ mockStream.Setup(s => s.Length).Returns(byteLength);
+
+ var mockWorkingBeatmap = new Mock();
+ mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(mockStream.Object);
+
+ return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
+ }
+
+ private BeatmapVerifierContext getContextMissing()
+ {
+ var mockWorkingBeatmap = new Mock();
+ mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns((Stream)null);
+
+ return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
+ }
+ }
+}
diff --git a/osu.Game.Tests/ImportTest.cs b/osu.Game.Tests/ImportTest.cs
index e888f51e98..dbeb453d4d 100644
--- a/osu.Game.Tests/ImportTest.cs
+++ b/osu.Game.Tests/ImportTest.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Tests
protected virtual TestOsuGameBase LoadOsuIntoHost(GameHost host, bool withBeatmap = false)
{
var osu = new TestOsuGameBase(withBeatmap);
- Task.Run(() => host.Run(osu))
+ Task.Factory.StartNew(() => host.Run(osu), TaskCreationOptions.LongRunning)
.ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted);
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
diff --git a/osu.Game.Tests/Resources/Samples/blank.wav b/osu.Game.Tests/Resources/Samples/blank.wav
new file mode 100644
index 0000000000..878bf23cea
Binary files /dev/null and b/osu.Game.Tests/Resources/Samples/blank.wav differ
diff --git a/osu.Game.Tests/Resources/Samples/corrupt.wav b/osu.Game.Tests/Resources/Samples/corrupt.wav
new file mode 100644
index 0000000000..87c7de4b7b
Binary files /dev/null and b/osu.Game.Tests/Resources/Samples/corrupt.wav differ
diff --git a/osu.Game.Tests/Resources/Samples/test-sample-cut.mp3 b/osu.Game.Tests/Resources/Samples/test-sample-cut.mp3
new file mode 100644
index 0000000000..003fe23dca
Binary files /dev/null and b/osu.Game.Tests/Resources/Samples/test-sample-cut.mp3 differ
diff --git a/osu.Game.Tests/Resources/Videos/test-video-with-audio.mp4 b/osu.Game.Tests/Resources/Videos/test-video-with-audio.mp4
new file mode 100644
index 0000000000..5d380ab50c
Binary files /dev/null and b/osu.Game.Tests/Resources/Videos/test-video-with-audio.mp4 differ
diff --git a/osu.Game.Tests/Resources/Videos/test-video-with-track-but-no-audio.mp4 b/osu.Game.Tests/Resources/Videos/test-video-with-track-but-no-audio.mp4
new file mode 100644
index 0000000000..7cdd1939e9
Binary files /dev/null and b/osu.Game.Tests/Resources/Videos/test-video-with-track-but-no-audio.mp4 differ
diff --git a/osu.Game.Tests/Resources/Videos/test-video.mp4 b/osu.Game.Tests/Resources/Videos/test-video.mp4
new file mode 100644
index 0000000000..795483c096
Binary files /dev/null and b/osu.Game.Tests/Resources/Videos/test-video.mp4 differ
diff --git a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs
index 211543a881..0107632f6e 100644
--- a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs
+++ b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs
@@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@@ -18,16 +19,19 @@ namespace osu.Game.Tests.Visual.Audio
{
public class TestSceneAudioFilter : OsuTestScene
{
- private OsuSpriteText lowpassText;
- private AudioFilter lowpassFilter;
+ private OsuSpriteText lowPassText;
+ private AudioFilter lowPassFilter;
- private OsuSpriteText highpassText;
- private AudioFilter highpassFilter;
+ private OsuSpriteText highPassText;
+ private AudioFilter highPassFilter;
private Track track;
private WaveformTestBeatmap beatmap;
+ private OsuSliderBar lowPassSlider;
+ private OsuSliderBar highPassSlider;
+
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
@@ -38,53 +42,89 @@ namespace osu.Game.Tests.Visual.Audio
{
Children = new Drawable[]
{
- lowpassFilter = new AudioFilter(audio.TrackMixer),
- highpassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
- lowpassText = new OsuSpriteText
+ lowPassFilter = new AudioFilter(audio.TrackMixer),
+ highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
+ lowPassText = new OsuSpriteText
{
Padding = new MarginPadding(20),
- Text = $"Low Pass: {lowpassFilter.Cutoff.Value}hz",
+ Text = $"Low Pass: {lowPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40)
},
- new OsuSliderBar
+ lowPassSlider = new OsuSliderBar
{
Width = 500,
Height = 50,
Padding = new MarginPadding(20),
- Current = { BindTarget = lowpassFilter.Cutoff }
+ Current = new BindableInt
+ {
+ MinValue = 0,
+ MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF,
+ }
},
- highpassText = new OsuSpriteText
+ highPassText = new OsuSpriteText
{
Padding = new MarginPadding(20),
- Text = $"High Pass: {highpassFilter.Cutoff.Value}hz",
+ Text = $"High Pass: {highPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40)
},
- new OsuSliderBar
+ highPassSlider = new OsuSliderBar
{
Width = 500,
Height = 50,
Padding = new MarginPadding(20),
- Current = { BindTarget = highpassFilter.Cutoff }
+ Current = new BindableInt
+ {
+ MinValue = 0,
+ MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF,
+ }
}
}
});
- lowpassFilter.Cutoff.ValueChanged += e => lowpassText.Text = $"Low Pass: {e.NewValue}hz";
- highpassFilter.Cutoff.ValueChanged += e => highpassText.Text = $"High Pass: {e.NewValue}hz";
+
+ lowPassSlider.Current.ValueChanged += e =>
+ {
+ lowPassText.Text = $"Low Pass: {e.NewValue}hz";
+ lowPassFilter.Cutoff = e.NewValue;
+ };
+
+ highPassSlider.Current.ValueChanged += e =>
+ {
+ highPassText.Text = $"High Pass: {e.NewValue}hz";
+ highPassFilter.Cutoff = e.NewValue;
+ };
}
+ #region Overrides of Drawable
+
+ protected override void Update()
+ {
+ base.Update();
+ highPassSlider.Current.Value = highPassFilter.Cutoff;
+ lowPassSlider.Current.Value = lowPassFilter.Cutoff;
+ }
+
+ #endregion
+
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Play Track", () => track.Start());
+
+ AddStep("Reset filters", () =>
+ {
+ lowPassFilter.Cutoff = AudioFilter.MAX_LOWPASS_CUTOFF;
+ highPassFilter.Cutoff = 0;
+ });
+
waitTrackPlay();
}
[Test]
- public void TestLowPass()
+ public void TestLowPassSweep()
{
AddStep("Filter Sweep", () =>
{
- lowpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
+ lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
.CutoffTo(0, 2000, Easing.OutCubic);
});
@@ -92,7 +132,7 @@ namespace osu.Game.Tests.Visual.Audio
AddStep("Filter Sweep (reverse)", () =>
{
- lowpassFilter.CutoffTo(0).Then()
+ lowPassFilter.CutoffTo(0).Then()
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
});
@@ -101,11 +141,11 @@ namespace osu.Game.Tests.Visual.Audio
}
[Test]
- public void TestHighPass()
+ public void TestHighPassSweep()
{
AddStep("Filter Sweep", () =>
{
- highpassFilter.CutoffTo(0).Then()
+ highPassFilter.CutoffTo(0).Then()
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
});
@@ -113,7 +153,7 @@ namespace osu.Game.Tests.Visual.Audio
AddStep("Filter Sweep (reverse)", () =>
{
- highpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
+ highPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
.CutoffTo(0, 2000, Easing.OutCubic);
});
diff --git a/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs b/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs
index b14684200f..319a768e65 100644
--- a/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs
+++ b/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
public static TournamentGameBase LoadTournament(GameHost host, TournamentGameBase tournament = null)
{
tournament ??= new TournamentGameBase();
- Task.Run(() => host.Run(tournament))
+ Task.Factory.StartNew(() => host.Run(tournament), TaskCreationOptions.LongRunning)
.ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted);
WaitForOrAssert(() => tournament.IsLoaded, @"osu! failed to start in a reasonable amount of time");
return tournament;
diff --git a/osu.Game/Audio/Effects/AudioFilter.cs b/osu.Game/Audio/Effects/AudioFilter.cs
index ee48bdd7d9..d2a39e9db7 100644
--- a/osu.Game/Audio/Effects/AudioFilter.cs
+++ b/osu.Game/Audio/Effects/AudioFilter.cs
@@ -4,7 +4,6 @@
using System.Diagnostics;
using ManagedBass.Fx;
using osu.Framework.Audio.Mixing;
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
namespace osu.Game.Audio.Effects
@@ -21,10 +20,25 @@ namespace osu.Game.Audio.Effects
private readonly BQFParameters filter;
private readonly BQFType type;
+ private bool isAttached;
+
+ private int cutoff;
+
///
- /// The current cutoff of this filter.
+ /// The cutoff frequency of this filter.
///
- public BindableNumber Cutoff { get; }
+ public int Cutoff
+ {
+ get => cutoff;
+ set
+ {
+ if (value == cutoff)
+ return;
+
+ cutoff = value;
+ updateFilter(cutoff);
+ }
+ }
///
/// A Component that implements a BASS FX BiQuad Filter Effect.
@@ -36,102 +50,96 @@ namespace osu.Game.Audio.Effects
this.mixer = mixer;
this.type = type;
- int initialCutoff;
-
- switch (type)
- {
- case BQFType.HighPass:
- initialCutoff = 1;
- break;
-
- case BQFType.LowPass:
- initialCutoff = MAX_LOWPASS_CUTOFF;
- break;
-
- default:
- initialCutoff = 500; // A default that should ensure audio remains audible for other filters.
- break;
- }
-
- Cutoff = new BindableNumber(initialCutoff)
- {
- MinValue = 1,
- MaxValue = MAX_LOWPASS_CUTOFF
- };
-
filter = new BQFParameters
{
lFilter = type,
- fCenter = initialCutoff,
fBandwidth = 0,
- fQ = 0.7f // This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0)
+ // This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0)
+ fQ = 0.7f
};
- // Don't start attached if this is low-pass or high-pass filter (as they have special auto-attach/detach logic)
- if (type != BQFType.LowPass && type != BQFType.HighPass)
- attachFilter();
-
- Cutoff.ValueChanged += updateFilter;
+ Cutoff = getInitialCutoff(type);
}
- private void attachFilter()
+ private int getInitialCutoff(BQFType type)
{
- Debug.Assert(!mixer.Effects.Contains(filter));
- mixer.Effects.Add(filter);
- }
-
- private void detachFilter()
- {
- Debug.Assert(mixer.Effects.Contains(filter));
- mixer.Effects.Remove(filter);
- }
-
- private void updateFilter(ValueChangedEvent cutoff)
- {
- // Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz.
- if (type == BQFType.LowPass)
+ switch (type)
{
- if (cutoff.NewValue >= MAX_LOWPASS_CUTOFF)
- {
- detachFilter();
- return;
- }
+ case BQFType.HighPass:
+ return 1;
- if (cutoff.OldValue >= MAX_LOWPASS_CUTOFF && cutoff.NewValue < MAX_LOWPASS_CUTOFF)
- attachFilter();
+ case BQFType.LowPass:
+ return MAX_LOWPASS_CUTOFF;
+
+ default:
+ return 500; // A default that should ensure audio remains audible for other filters.
+ }
+ }
+
+ private void updateFilter(int newValue)
+ {
+ switch (type)
+ {
+ case BQFType.LowPass:
+ // Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz.
+ if (newValue >= MAX_LOWPASS_CUTOFF)
+ {
+ ensureDetached();
+ return;
+ }
+
+ break;
+
+ // Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz.
+ case BQFType.HighPass:
+ if (newValue <= 1)
+ {
+ ensureDetached();
+ return;
+ }
+
+ break;
}
- // Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz.
- if (type == BQFType.HighPass)
- {
- if (cutoff.NewValue <= 1)
- {
- detachFilter();
- return;
- }
-
- if (cutoff.OldValue <= 1 && cutoff.NewValue > 1)
- attachFilter();
- }
+ ensureAttached();
var filterIndex = mixer.Effects.IndexOf(filter);
+
if (filterIndex < 0) return;
if (mixer.Effects[filterIndex] is BQFParameters existingFilter)
{
- existingFilter.fCenter = cutoff.NewValue;
+ existingFilter.fCenter = newValue;
// required to update effect with new parameters.
mixer.Effects[filterIndex] = existingFilter;
}
}
+ private void ensureAttached()
+ {
+ if (isAttached)
+ return;
+
+ Debug.Assert(!mixer.Effects.Contains(filter));
+ mixer.Effects.Add(filter);
+ isAttached = true;
+ }
+
+ private void ensureDetached()
+ {
+ if (!isAttached)
+ return;
+
+ Debug.Assert(mixer.Effects.Contains(filter));
+ mixer.Effects.Remove(filter);
+ isAttached = false;
+ }
+
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
-
- if (mixer.Effects.Contains(filter))
- detachFilter();
+ ensureDetached();
}
}
}
diff --git a/osu.Game/Audio/Effects/ITransformableFilter.cs b/osu.Game/Audio/Effects/ITransformableFilter.cs
index e4de4cf8ff..fb6a924f68 100644
--- a/osu.Game/Audio/Effects/ITransformableFilter.cs
+++ b/osu.Game/Audio/Effects/ITransformableFilter.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
@@ -12,7 +11,7 @@ namespace osu.Game.Audio.Effects
///
/// The filter cutoff.
///
- BindableNumber Cutoff { get; }
+ int Cutoff { get; set; }
}
public static class FilterableAudioComponentExtensions
@@ -40,7 +39,7 @@ namespace osu.Game.Audio.Effects
public static TransformSequence CutoffTo(this T component, int newCutoff, double duration, TEasing easing)
where T : class, ITransformableFilter, IDrawable
where TEasing : IEasingFunction
- => component.TransformBindableTo(component.Cutoff, newCutoff, duration, easing);
+ => component.TransformTo(nameof(component.Cutoff), newCutoff, duration, easing);
///
/// Smoothly adjusts filter cutoff over time.
@@ -49,6 +48,6 @@ namespace osu.Game.Audio.Effects
public static TransformSequence CutoffTo(this TransformSequence sequence, int newCutoff, double duration, TEasing easing)
where T : class, ITransformableFilter, IDrawable
where TEasing : IEasingFunction
- => sequence.Append(o => o.TransformBindableTo(o.Cutoff, newCutoff, duration, easing));
+ => sequence.Append(o => o.TransformTo(nameof(o.Cutoff), newCutoff, duration, easing));
}
}
diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs
new file mode 100644
index 0000000000..abb69644d6
--- /dev/null
+++ b/osu.Game/Database/RealmLive.cs
@@ -0,0 +1,111 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Threading;
+using Realms;
+
+#nullable enable
+
+namespace osu.Game.Database
+{
+ ///
+ /// Provides a method of working with realm objects over longer application lifetimes.
+ ///
+ /// The underlying object type.
+ public class RealmLive : ILive where T : RealmObject, IHasGuidPrimaryKey
+ {
+ public Guid ID { get; }
+
+ private readonly SynchronizationContext? fetchedContext;
+ private readonly int fetchedThreadId;
+
+ ///
+ /// The original live data used to create this instance.
+ ///
+ private readonly T data;
+
+ ///
+ /// Construct a new instance of live realm data.
+ ///
+ /// The realm data.
+ public RealmLive(T data)
+ {
+ this.data = data;
+
+ fetchedContext = SynchronizationContext.Current;
+ fetchedThreadId = Thread.CurrentThread.ManagedThreadId;
+
+ ID = data.ID;
+ }
+
+ ///
+ /// Perform a read operation on this live object.
+ ///
+ /// The action to perform.
+ public void PerformRead(Action perform)
+ {
+ if (originalDataValid)
+ {
+ perform(data);
+ return;
+ }
+
+ using (var realm = Realm.GetInstance(data.Realm.Config))
+ perform(realm.Find(ID));
+ }
+
+ ///
+ /// Perform a read operation on this live object.
+ ///
+ /// The action to perform.
+ public TReturn PerformRead(Func perform)
+ {
+ if (typeof(RealmObjectBase).IsAssignableFrom(typeof(TReturn)))
+ throw new InvalidOperationException($"Realm live objects should not exit the scope of {nameof(PerformRead)}.");
+
+ if (originalDataValid)
+ return perform(data);
+
+ using (var realm = Realm.GetInstance(data.Realm.Config))
+ return perform(realm.Find(ID));
+ }
+
+ ///
+ /// Perform a write operation on this live object.
+ ///
+ /// The action to perform.
+ public void PerformWrite(Action perform) =>
+ PerformRead(t =>
+ {
+ var transaction = t.Realm.BeginWrite();
+ perform(t);
+ transaction.Commit();
+ });
+
+ public T Value
+ {
+ get
+ {
+ if (originalDataValid)
+ return data;
+
+ T retrieved;
+
+ using (var realm = Realm.GetInstance(data.Realm.Config))
+ retrieved = realm.Find(ID);
+
+ if (!retrieved.IsValid)
+ throw new InvalidOperationException("Attempted to access value without an open context");
+
+ return retrieved;
+ }
+ }
+
+ private bool originalDataValid => isCorrectThread && data.IsValid;
+
+ // this matches realm's internal thread validation (see https://github.com/realm/realm-dotnet/blob/903b4d0b304f887e37e2d905384fb572a6496e70/Realm/Realm/Native/SynchronizationContextScheduler.cs#L72)
+ private bool isCorrectThread
+ => (fetchedContext != null && SynchronizationContext.Current == fetchedContext) || fetchedThreadId == Thread.CurrentThread.ManagedThreadId;
+ }
+}
diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs
index c5aa1399a3..18a926fa8c 100644
--- a/osu.Game/Database/RealmObjectExtensions.cs
+++ b/osu.Game/Database/RealmObjectExtensions.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using System.Linq;
using AutoMapper;
using osu.Game.Input.Bindings;
using Realms;
@@ -47,5 +48,17 @@ namespace osu.Game.Database
return mapper.Map(item);
}
+
+ public static List> ToLive(this IEnumerable realmList)
+ where T : RealmObject, IHasGuidPrimaryKey
+ {
+ return realmList.Select(l => new RealmLive(l)).ToList();
+ }
+
+ public static RealmLive ToLive(this T realmObject)
+ where T : RealmObject, IHasGuidPrimaryKey
+ {
+ return new RealmLive(realmObject);
+ }
}
}
diff --git a/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs b/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs
new file mode 100644
index 0000000000..f5709b5158
--- /dev/null
+++ b/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs
@@ -0,0 +1,30 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.IO;
+
+namespace osu.Game.IO.FileAbstraction
+{
+ public class StreamFileAbstraction : TagLib.File.IFileAbstraction
+ {
+ public StreamFileAbstraction(string filename, Stream fileStream)
+ {
+ ReadStream = fileStream;
+ Name = filename;
+ }
+
+ public string Name { get; }
+
+ public Stream ReadStream { get; }
+ public Stream WriteStream => ReadStream;
+
+ public void CloseStream(Stream stream)
+ {
+ if (stream == null)
+ throw new ArgumentNullException(nameof(stream));
+
+ stream.Close();
+ }
+ }
+}
diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs
index 5f71b4be4a..39fc7f1da8 100644
--- a/osu.Game/Online/Rooms/Room.cs
+++ b/osu.Game/Online/Rooms/Room.cs
@@ -130,12 +130,6 @@ namespace osu.Game.Online.Rooms
set => MaxAttempts.Value = value;
}
- ///
- /// The position of this in the list. This is not read from or written to the API.
- ///
- [JsonIgnore]
- public readonly Bindable Position = new Bindable(-1); // Todo: This does not need to exist.
-
public Room()
{
Password.BindValueChanged(p => HasPassword.Value = !string.IsNullOrEmpty(p.NewValue));
@@ -192,8 +186,6 @@ namespace osu.Game.Online.Rooms
RecentParticipants.Clear();
RecentParticipants.AddRange(other.RecentParticipants);
}
-
- Position.Value = other.Position.Value;
}
public void RemoveExpiredPlaylistItems()
diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs
index 81f4808789..6ed91e983a 100644
--- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs
+++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs
@@ -24,6 +24,11 @@ namespace osu.Game.Rulesets.Edit
new CheckAudioQuality(),
new CheckMutedObjects(),
new CheckFewHitsounds(),
+ new CheckTooShortAudioFiles(),
+ new CheckAudioInVideo(),
+
+ // Files
+ new CheckZeroByteFiles(),
// Compose
new CheckUnsnappedObjects(),
diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs
new file mode 100644
index 0000000000..ac2542beb0
--- /dev/null
+++ b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs
@@ -0,0 +1,112 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.IO;
+using osu.Game.IO.FileAbstraction;
+using osu.Game.Rulesets.Edit.Checks.Components;
+using osu.Game.Storyboards;
+using TagLib;
+using File = TagLib.File;
+
+namespace osu.Game.Rulesets.Edit.Checks
+{
+ public class CheckAudioInVideo : ICheck
+ {
+ public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Audio track in video files");
+
+ public IEnumerable PossibleTemplates => new IssueTemplate[]
+ {
+ new IssueTemplateHasAudioTrack(this),
+ new IssueTemplateMissingFile(this),
+ new IssueTemplateFileError(this)
+ };
+
+ public IEnumerable Run(BeatmapVerifierContext context)
+ {
+ var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet;
+ var videoPaths = new List();
+
+ foreach (var layer in context.WorkingBeatmap.Storyboard.Layers)
+ {
+ foreach (var element in layer.Elements)
+ {
+ if (!(element is StoryboardVideo video))
+ continue;
+
+ // Ensures we don't check the same video file multiple times in case of multiple elements using it.
+ if (!videoPaths.Contains(video.Path))
+ videoPaths.Add(video.Path);
+ }
+ }
+
+ foreach (var filename in videoPaths)
+ {
+ string storagePath = beatmapSet.GetPathForFile(filename);
+
+ if (storagePath == null)
+ {
+ // There's an element in the storyboard that requires this resource, so it being missing is worth warning about.
+ yield return new IssueTemplateMissingFile(this).Create(filename);
+
+ continue;
+ }
+
+ Issue issue;
+
+ try
+ {
+ // We use TagLib here for platform invariance; BASS cannot detect audio presence on Linux.
+ using (Stream data = context.WorkingBeatmap.GetStream(storagePath))
+ using (File tagFile = File.Create(new StreamFileAbstraction(filename, data)))
+ {
+ if (tagFile.Properties.AudioChannels == 0)
+ continue;
+ }
+
+ issue = new IssueTemplateHasAudioTrack(this).Create(filename);
+ }
+ catch (CorruptFileException)
+ {
+ issue = new IssueTemplateFileError(this).Create(filename, "Corrupt file");
+ }
+ catch (UnsupportedFormatException)
+ {
+ issue = new IssueTemplateFileError(this).Create(filename, "Unsupported format");
+ }
+
+ yield return issue;
+ }
+ }
+
+ public class IssueTemplateHasAudioTrack : IssueTemplate
+ {
+ public IssueTemplateHasAudioTrack(ICheck check)
+ : base(check, IssueType.Problem, "\"{0}\" has an audio track.")
+ {
+ }
+
+ public Issue Create(string filename) => new Issue(this, filename);
+ }
+
+ public class IssueTemplateFileError : IssueTemplate
+ {
+ public IssueTemplateFileError(ICheck check)
+ : base(check, IssueType.Error, "Could not check whether \"{0}\" has an audio track ({1}).")
+ {
+ }
+
+ public Issue Create(string filename, string errorReason) => new Issue(this, filename, errorReason);
+ }
+
+ public class IssueTemplateMissingFile : IssueTemplate
+ {
+ public IssueTemplateMissingFile(ICheck check)
+ : base(check, IssueType.Warning, "Could not check whether \"{0}\" has an audio track, because it is missing.")
+ {
+ }
+
+ public Issue Create(string filename) => new Issue(this, filename);
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs
new file mode 100644
index 0000000000..57f7c60916
--- /dev/null
+++ b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs
@@ -0,0 +1,85 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using ManagedBass;
+using osu.Framework.Audio.Callbacks;
+using osu.Game.Rulesets.Edit.Checks.Components;
+
+namespace osu.Game.Rulesets.Edit.Checks
+{
+ public class CheckTooShortAudioFiles : ICheck
+ {
+ private const int ms_threshold = 25;
+ private const int min_bytes_threshold = 100;
+
+ private readonly string[] audioExtensions = { "mp3", "ogg", "wav" };
+
+ public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Too short audio files");
+
+ public IEnumerable PossibleTemplates => new IssueTemplate[]
+ {
+ new IssueTemplateTooShort(this),
+ new IssueTemplateBadFormat(this)
+ };
+
+ public IEnumerable Run(BeatmapVerifierContext context)
+ {
+ var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet;
+
+ foreach (var file in beatmapSet.Files)
+ {
+ using (Stream data = context.WorkingBeatmap.GetStream(file.FileInfo.StoragePath))
+ {
+ if (data == null)
+ continue;
+
+ var fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data));
+ int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode | BassFlags.Prescan, fileCallbacks.Callbacks, fileCallbacks.Handle);
+
+ if (decodeStream == 0)
+ {
+ // If the file is not likely to be properly parsed by Bass, we don't produce Error issues about it.
+ // Image files and audio files devoid of audio data both fail, for example, but neither would be issues in this check.
+ if (hasAudioExtension(file.Filename) && probablyHasAudioData(data))
+ yield return new IssueTemplateBadFormat(this).Create(file.Filename);
+
+ continue;
+ }
+
+ long length = Bass.ChannelGetLength(decodeStream);
+ double ms = Bass.ChannelBytes2Seconds(decodeStream, length) * 1000;
+
+ // Extremely short audio files do not play on some soundcards, resulting in nothing being heard in-game for some users.
+ if (ms > 0 && ms < ms_threshold)
+ yield return new IssueTemplateTooShort(this).Create(file.Filename, ms);
+ }
+ }
+ }
+
+ private bool hasAudioExtension(string filename) => audioExtensions.Any(filename.ToLower().EndsWith);
+ private bool probablyHasAudioData(Stream data) => data.Length > min_bytes_threshold;
+
+ public class IssueTemplateTooShort : IssueTemplate
+ {
+ public IssueTemplateTooShort(ICheck check)
+ : base(check, IssueType.Problem, "\"{0}\" is too short ({1:0} ms), should be at least {2:0} ms.")
+ {
+ }
+
+ public Issue Create(string filename, double ms) => new Issue(this, filename, ms, ms_threshold);
+ }
+
+ public class IssueTemplateBadFormat : IssueTemplate
+ {
+ public IssueTemplateBadFormat(ICheck check)
+ : base(check, IssueType.Error, "Could not check whether \"{0}\" is too short (code \"{1}\").")
+ {
+ }
+
+ public Issue Create(string filename) => new Issue(this, filename, Bass.LastError);
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs
new file mode 100644
index 0000000000..3a994fabfa
--- /dev/null
+++ b/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs
@@ -0,0 +1,43 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.IO;
+using osu.Game.Rulesets.Edit.Checks.Components;
+
+namespace osu.Game.Rulesets.Edit.Checks
+{
+ public class CheckZeroByteFiles : ICheck
+ {
+ public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Files, "Zero-byte files");
+
+ public IEnumerable PossibleTemplates => new IssueTemplate[]
+ {
+ new IssueTemplateZeroBytes(this)
+ };
+
+ public IEnumerable Run(BeatmapVerifierContext context)
+ {
+ var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet;
+
+ foreach (var file in beatmapSet.Files)
+ {
+ using (Stream data = context.WorkingBeatmap.GetStream(file.FileInfo.StoragePath))
+ {
+ if (data?.Length == 0)
+ yield return new IssueTemplateZeroBytes(this).Create(file.Filename);
+ }
+ }
+ }
+
+ public class IssueTemplateZeroBytes : IssueTemplate
+ {
+ public IssueTemplateZeroBytes(ICheck check)
+ : base(check, IssueType.Problem, "\"{0}\" is a 0-byte file.")
+ {
+ }
+
+ public Issue Create(string filename) => new Issue(this, filename);
+ }
+ }
+}
diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
index 381849189d..97377278a6 100644
--- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
+++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs
@@ -116,8 +116,6 @@ namespace osu.Game.Screens.OnlinePlay.Components
if (ignoredRooms.Contains(room.RoomID.Value.Value))
return;
- room.Position.Value = -room.RoomID.Value.Value;
-
try
{
foreach (var pi in room.Playlist)
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs
index 907b7e308a..85efdcef1a 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs
@@ -129,7 +129,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
private void updateSorting()
{
foreach (var room in roomFlow)
- roomFlow.SetLayoutPosition(room, room.Room.Position.Value);
+ roomFlow.SetLayoutPosition(room, -(room.Room.RoomID.Value ?? 0));
}
protected override bool OnClick(ClickEvent e)
diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs
index 94a61a4ef3..cf5bff57cf 100644
--- a/osu.Game/Screens/Play/PlayerLoader.cs
+++ b/osu.Game/Screens/Play/PlayerLoader.cs
@@ -35,6 +35,8 @@ namespace osu.Game.Screens.Play
{
protected const float BACKGROUND_BLUR = 15;
+ private const double content_out_duration = 300;
+
public override bool HideOverlaysOnEnter => hideOverlays;
public override bool DisallowExternalBeatmapRulesetChanges => true;
@@ -135,36 +137,39 @@ namespace osu.Game.Screens.Play
muteWarningShownOnce = sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce);
batteryWarningShownOnce = sessionStatics.GetBindable(Static.LowBatteryNotificationShownOnce);
- InternalChild = (content = new LogoTrackingContainer
+ InternalChildren = new Drawable[]
{
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- }).WithChildren(new Drawable[]
- {
- MetadataInfo = new BeatmapMetadataDisplay(Beatmap.Value, Mods, content.LogoFacade)
+ (content = new LogoTrackingContainer
{
- Alpha = 0,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- },
- PlayerSettings = new FillFlowContainer
+ RelativeSizeAxes = Axes.Both,
+ }).WithChildren(new Drawable[]
{
- Anchor = Anchor.TopRight,
- Origin = Anchor.TopRight,
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Vertical,
- Spacing = new Vector2(0, 20),
- Margin = new MarginPadding(25),
- Children = new PlayerSettingsGroup[]
+ MetadataInfo = new BeatmapMetadataDisplay(Beatmap.Value, Mods, content.LogoFacade)
{
- VisualSettings = new VisualSettings(),
- new InputSettings()
- }
- },
- idleTracker = new IdleTracker(750),
+ Alpha = 0,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ },
+ PlayerSettings = new FillFlowContainer
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 20),
+ Margin = new MarginPadding(25),
+ Children = new PlayerSettingsGroup[]
+ {
+ VisualSettings = new VisualSettings(),
+ new InputSettings()
+ }
+ },
+ idleTracker = new IdleTracker(750),
+ }),
lowPassFilter = new AudioFilter(audio.TrackMixer)
- });
+ };
if (Beatmap.Value.BeatmapInfo.EpilepsyWarning)
{
@@ -195,7 +200,6 @@ namespace osu.Game.Screens.Play
epilepsyWarning.DimmableBackground = b;
});
- lowPassFilter.CutoffTo(500, 100, Easing.OutCubic);
Beatmap.Value.Track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment);
content.ScaleTo(0.7f);
@@ -240,15 +244,15 @@ namespace osu.Game.Screens.Play
public override bool OnExiting(IScreen next)
{
cancelLoad();
+ contentOut();
- content.ScaleTo(0.7f, 150, Easing.InQuint);
- this.FadeOut(150);
+ // Ensure the screen doesn't expire until all the outwards fade operations have completed.
+ this.Delay(content_out_duration).FadeOut();
ApplyToBackground(b => b.IgnoreUserSettings.Value = true);
BackgroundBrightnessReduction = false;
Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment);
- lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic);
return base.OnExiting(next);
}
@@ -344,6 +348,7 @@ namespace osu.Game.Screens.Play
content.FadeInFromZero(400);
content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer);
+ lowPassFilter.CutoffTo(1000, 650, Easing.OutQuint);
ApplyToBackground(b => b?.FadeColour(Color4.White, 800, Easing.OutQuint));
}
@@ -353,8 +358,9 @@ namespace osu.Game.Screens.Play
// Ensure the logo is no longer tracking before we scale the content
content.StopTracking();
- content.ScaleTo(0.7f, 300, Easing.InQuint);
- content.FadeOut(250);
+ content.ScaleTo(0.7f, content_out_duration * 2, Easing.OutQuint);
+ content.FadeOut(content_out_duration, Easing.OutQuint);
+ lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, content_out_duration);
}
private void pushWhenLoaded()
@@ -381,7 +387,7 @@ namespace osu.Game.Screens.Play
contentOut();
- TransformSequence pushSequence = this.Delay(250);
+ TransformSequence pushSequence = this.Delay(content_out_duration);
// only show if the warning was created (i.e. the beatmap needs it)
// and this is not a restart of the map (the warning expires after first load).
@@ -400,6 +406,11 @@ namespace osu.Game.Screens.Play
})
.Delay(EpilepsyWarning.FADE_DURATION);
}
+ else
+ {
+ // This goes hand-in-hand with the restoration of low pass filter in contentOut().
+ this.TransformBindableTo(volumeAdjustment, 0, content_out_duration, Easing.OutCubic);
+ }
pushSequence.Schedule(() =>
{
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 184c9d3f63..9ee6f4cf52 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -36,11 +36,12 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 38b920420b..110de79285 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -93,7 +93,7 @@
-
+