1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 17:47:29 +08:00

Merge branch 'master' into fix-editor-difficulty-name-update

This commit is contained in:
Dean Herbert 2021-10-14 10:17:01 +09:00 committed by GitHub
commit 8d479bbe56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1146 additions and 169 deletions

1
.github/FUNDING.yml vendored
View File

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

View File

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

View File

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

@ -45,8 +45,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
{
new Spinner
{
Duration = 2000,
Position = OsuPlayfield.BASE_SIZE / 2
Duration = 6000,
Position = OsuPlayfield.BASE_SIZE / 2,
}
}
},

View File

@ -30,6 +30,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneSpinnerRotation : TestSceneOsuPlayer
{
private const double spinner_start_time = 100;
private const double spinner_duration = 6000;
[Resolved]
private AudioManager audioManager { get; set; }
@ -77,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Tests
double finalTrackerRotation = 0, trackerRotationTolerance = 0;
double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0;
addSeekStep(5000);
addSeekStep(spinner_start_time + 5000);
AddStep("retrieve disc rotation", () =>
{
finalTrackerRotation = drawableSpinner.RotationTracker.Rotation;
@ -90,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests
});
AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation);
addSeekStep(2500);
addSeekStep(spinner_start_time + 2500);
AddAssert("disc rotation rewound",
// we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in.
// due to the exponential damping applied we're allowing a larger margin of error of about 10%
@ -102,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Tests
// cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error.
() => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100));
addSeekStep(5000);
addSeekStep(spinner_start_time + 5000);
AddAssert("is disc rotation almost same",
() => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance));
AddAssert("is symbol rotation almost same",
@ -140,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[Test]
public void TestSpinnerNormalBonusRewinding()
{
addSeekStep(1000);
addSeekStep(spinner_start_time + 1000);
AddAssert("player score matching expected bonus score", () =>
{
@ -201,24 +204,9 @@ namespace osu.Game.Rulesets.Osu.Tests
AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0));
}
private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay
{
Frames = scoreReplay
.Frames
.Cast<OsuReplayFrame>()
.Select(replayFrame =>
{
var adjustedTime = replayFrame.Time * rate;
return new OsuReplayFrame(adjustedTime, replayFrame.Position, replayFrame.Actions.ToArray());
})
.Cast<ReplayFrame>()
.ToList()
};
private void addSeekStep(double time)
{
AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
@ -241,7 +229,8 @@ namespace osu.Game.Rulesets.Osu.Tests
new Spinner
{
Position = new Vector2(256, 192),
EndTime = 6000,
StartTime = spinner_start_time,
Duration = spinner_duration
},
}
};

View File

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

View File

@ -0,0 +1,213 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Models;
using Realms;
#nullable enable
namespace osu.Game.Tests.Database
{
public class RealmLiveTests : RealmTest
{
[Test]
public void TestLiveCastability()
{
RunTestWithRealm((realmFactory, _) =>
{
RealmLive<RealmBeatmap> beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive();
ILive<IBeatmapInfo> iBeatmap = beatmap;
Assert.AreEqual(0, iBeatmap.Value.Length);
});
}
[Test]
public void TestValueAccessWithOpenContext()
{
RunTestWithRealm((realmFactory, _) =>
{
RealmLive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() =>
{
using (var threadContext = realmFactory.CreateContext())
{
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
liveBeatmap = beatmap.ToLive();
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
Debug.Assert(liveBeatmap != null);
Task.Factory.StartNew(() =>
{
Assert.DoesNotThrow(() =>
{
using (realmFactory.CreateContext())
{
var resolved = liveBeatmap.Value;
Assert.IsTrue(resolved.Realm.IsClosed);
Assert.IsTrue(resolved.IsValid);
// can access properties without a crash.
Assert.IsFalse(resolved.Hidden);
}
});
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
});
}
[Test]
public void TestScopedReadWithoutContext()
{
RunTestWithRealm((realmFactory, _) =>
{
RealmLive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() =>
{
using (var threadContext = realmFactory.CreateContext())
{
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
liveBeatmap = beatmap.ToLive();
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
Debug.Assert(liveBeatmap != null);
Task.Factory.StartNew(() =>
{
liveBeatmap.PerformRead(beatmap =>
{
Assert.IsTrue(beatmap.IsValid);
Assert.IsFalse(beatmap.Hidden);
});
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
});
}
[Test]
public void TestScopedWriteWithoutContext()
{
RunTestWithRealm((realmFactory, _) =>
{
RealmLive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() =>
{
using (var threadContext = realmFactory.CreateContext())
{
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
liveBeatmap = beatmap.ToLive();
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
Debug.Assert(liveBeatmap != null);
Task.Factory.StartNew(() =>
{
liveBeatmap.PerformWrite(beatmap => { beatmap.Hidden = true; });
liveBeatmap.PerformRead(beatmap => { Assert.IsTrue(beatmap.Hidden); });
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
});
}
[Test]
public void TestValueAccessWithoutOpenContextFails()
{
RunTestWithRealm((realmFactory, _) =>
{
RealmLive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() =>
{
using (var threadContext = realmFactory.CreateContext())
{
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
liveBeatmap = beatmap.ToLive();
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
Debug.Assert(liveBeatmap != null);
Task.Factory.StartNew(() =>
{
Assert.Throws<InvalidOperationException>(() =>
{
var unused = liveBeatmap.Value;
});
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
});
}
[Test]
public void TestLiveAssumptions()
{
RunTestWithRealm((realmFactory, _) =>
{
int changesTriggered = 0;
using (var updateThreadContext = realmFactory.CreateContext())
{
updateThreadContext.All<RealmBeatmap>().SubscribeForNotifications(gotChange);
RealmLive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() =>
{
using (var threadContext = realmFactory.CreateContext())
{
var ruleset = CreateRuleset();
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
// add a second beatmap to ensure that a full refresh occurs below.
// not just a refresh from the resolved Live.
threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
liveBeatmap = beatmap.ToLive();
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
Debug.Assert(liveBeatmap != null);
// not yet seen by main context
Assert.AreEqual(0, updateThreadContext.All<RealmBeatmap>().Count());
Assert.AreEqual(0, changesTriggered);
var resolved = liveBeatmap.Value;
// retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
Assert.AreEqual(2, updateThreadContext.All<RealmBeatmap>().Count());
Assert.AreEqual(1, changesTriggered);
// even though the realm that this instance was resolved for was closed, it's still valid.
Assert.IsTrue(resolved.Realm.IsClosed);
Assert.IsTrue(resolved.IsValid);
// can access properties without a crash.
Assert.IsFalse(resolved.Hidden);
updateThreadContext.Write(r =>
{
// can use with the main context.
r.Remove(resolved);
});
}
void gotChange(IRealmCollection<RealmBeatmap> sender, ChangeSet changes, Exception error)
{
changesTriggered++;
}
});
}
}
}

View File

@ -0,0 +1,104 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Moq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Storyboards;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using FileInfo = osu.Game.IO.FileInfo;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckAudioInVideoTest
{
private CheckAudioInVideo check;
private IBeatmap beatmap;
[SetUp]
public void Setup()
{
check = new CheckAudioInVideo();
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
BeatmapSet = new BeatmapSetInfo
{
Files = new List<BeatmapSetFileInfo>(new[]
{
new BeatmapSetFileInfo
{
Filename = "abc123.mp4",
FileInfo = new FileInfo { Hash = "abcdef" }
}
})
}
}
};
}
[Test]
public void TestRegularVideoFile()
{
using (var resourceStream = TestResources.OpenResource("Videos/test-video.mp4"))
Assert.IsEmpty(check.Run(getContext(resourceStream)));
}
[Test]
public void TestVideoFileWithAudio()
{
using (var resourceStream = TestResources.OpenResource("Videos/test-video-with-audio.mp4"))
{
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateHasAudioTrack);
}
}
[Test]
public void TestVideoFileWithTrackButNoAudio()
{
using (var resourceStream = TestResources.OpenResource("Videos/test-video-with-track-but-no-audio.mp4"))
{
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateHasAudioTrack);
}
}
[Test]
public void TestMissingFile()
{
beatmap.BeatmapInfo.BeatmapSet.Files.Clear();
var issues = check.Run(getContext(null)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateMissingFile);
}
private BeatmapVerifierContext getContext(Stream resourceStream)
{
var storyboard = new Storyboard();
var layer = storyboard.GetLayer("Video");
layer.Add(new StoryboardVideo("abc123.mp4", 0));
var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null, null);
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream);
mockWorkingBeatmap.As<IWorkingBeatmap>().SetupGet(w => w.Storyboard).Returns(storyboard);
return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
}
}
}

View File

@ -0,0 +1,128 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ManagedBass;
using Moq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using osuTK.Audio;
using FileInfo = osu.Game.IO.FileInfo;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckTooShortAudioFilesTest
{
private CheckTooShortAudioFiles check;
private IBeatmap beatmap;
[SetUp]
public void Setup()
{
check = new CheckTooShortAudioFiles();
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
BeatmapSet = new BeatmapSetInfo
{
Files = new List<BeatmapSetFileInfo>(new[]
{
new BeatmapSetFileInfo
{
Filename = "abc123.wav",
FileInfo = new FileInfo { Hash = "abcdef" }
}
})
}
}
};
// 0 = No output device. This still allows decoding.
if (!Bass.Init(0) && Bass.LastError != Errors.Already)
throw new AudioException("Could not initialize Bass.");
}
[Test]
public void TestDifferentExtension()
{
beatmap.BeatmapInfo.BeatmapSet.Files.Clear();
beatmap.BeatmapInfo.BeatmapSet.Files.Add(new BeatmapSetFileInfo
{
Filename = "abc123.jpg",
FileInfo = new FileInfo { Hash = "abcdef" }
});
// Should fail to load, but not produce an error due to the extension not being expected to load.
Assert.IsEmpty(check.Run(getContext(null, allowMissing: true)));
}
[Test]
public void TestRegularAudioFile()
{
using (var resourceStream = TestResources.OpenResource("Samples/test-sample.mp3"))
{
Assert.IsEmpty(check.Run(getContext(resourceStream)));
}
}
[Test]
public void TestBlankAudioFile()
{
using (var resourceStream = TestResources.OpenResource("Samples/blank.wav"))
{
// This is a 0 ms duration audio file, commonly used to silence sliderslides/ticks, and so should be fine.
Assert.IsEmpty(check.Run(getContext(resourceStream)));
}
}
[Test]
public void TestTooShortAudioFile()
{
using (var resourceStream = TestResources.OpenResource("Samples/test-sample-cut.mp3"))
{
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckTooShortAudioFiles.IssueTemplateTooShort);
}
}
[Test]
public void TestMissingAudioFile()
{
using (var resourceStream = TestResources.OpenResource("Samples/missing.mp3"))
{
Assert.IsEmpty(check.Run(getContext(resourceStream, allowMissing: true)));
}
}
[Test]
public void TestCorruptAudioFile()
{
using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav"))
{
var issues = check.Run(getContext(resourceStream)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckTooShortAudioFiles.IssueTemplateBadFormat);
}
}
private BeatmapVerifierContext getContext(Stream resourceStream, bool allowMissing = false)
{
var mockWorkingBeatmap = new Mock<TestWorkingBeatmap>(beatmap, null, null);
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(resourceStream);
return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
}
}
}

View File

@ -0,0 +1,86 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Moq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using FileInfo = osu.Game.IO.FileInfo;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckZeroByteFilesTest
{
private CheckZeroByteFiles check;
private IBeatmap beatmap;
[SetUp]
public void Setup()
{
check = new CheckZeroByteFiles();
beatmap = new Beatmap<HitObject>
{
BeatmapInfo = new BeatmapInfo
{
BeatmapSet = new BeatmapSetInfo
{
Files = new List<BeatmapSetFileInfo>(new[]
{
new BeatmapSetFileInfo
{
Filename = "abc123.jpg",
FileInfo = new FileInfo { Hash = "abcdef" }
}
})
}
}
};
}
[Test]
public void TestNonZeroBytes()
{
Assert.IsEmpty(check.Run(getContext(byteLength: 44)));
}
[Test]
public void TestZeroBytes()
{
var issues = check.Run(getContext(byteLength: 0)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckZeroByteFiles.IssueTemplateZeroBytes);
}
[Test]
public void TestMissing()
{
Assert.IsEmpty(check.Run(getContextMissing()));
}
private BeatmapVerifierContext getContext(long byteLength)
{
var mockStream = new Mock<Stream>();
mockStream.Setup(s => s.Length).Returns(byteLength);
var mockWorkingBeatmap = new Mock<IWorkingBeatmap>();
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns(mockStream.Object);
return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
}
private BeatmapVerifierContext getContextMissing()
{
var mockWorkingBeatmap = new Mock<IWorkingBeatmap>();
mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny<string>())).Returns((Stream)null);
return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object);
}
}
}

View File

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

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Binary file not shown.

View File

@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@ -18,16 +19,19 @@ namespace osu.Game.Tests.Visual.Audio
{
public class TestSceneAudioFilter : OsuTestScene
{
private OsuSpriteText lowpassText;
private AudioFilter lowpassFilter;
private OsuSpriteText lowPassText;
private AudioFilter lowPassFilter;
private OsuSpriteText highpassText;
private AudioFilter highpassFilter;
private OsuSpriteText highPassText;
private AudioFilter highPassFilter;
private Track track;
private WaveformTestBeatmap beatmap;
private OsuSliderBar<int> lowPassSlider;
private OsuSliderBar<int> highPassSlider;
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
@ -38,53 +42,89 @@ namespace osu.Game.Tests.Visual.Audio
{
Children = new Drawable[]
{
lowpassFilter = new AudioFilter(audio.TrackMixer),
highpassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
lowpassText = new OsuSpriteText
lowPassFilter = new AudioFilter(audio.TrackMixer),
highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
lowPassText = new OsuSpriteText
{
Padding = new MarginPadding(20),
Text = $"Low Pass: {lowpassFilter.Cutoff.Value}hz",
Text = $"Low Pass: {lowPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40)
},
new OsuSliderBar<int>
lowPassSlider = new OsuSliderBar<int>
{
Width = 500,
Height = 50,
Padding = new MarginPadding(20),
Current = { BindTarget = lowpassFilter.Cutoff }
Current = new BindableInt
{
MinValue = 0,
MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF,
}
},
highpassText = new OsuSpriteText
highPassText = new OsuSpriteText
{
Padding = new MarginPadding(20),
Text = $"High Pass: {highpassFilter.Cutoff.Value}hz",
Text = $"High Pass: {highPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40)
},
new OsuSliderBar<int>
highPassSlider = new OsuSliderBar<int>
{
Width = 500,
Height = 50,
Padding = new MarginPadding(20),
Current = { BindTarget = highpassFilter.Cutoff }
Current = new BindableInt
{
MinValue = 0,
MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF,
}
}
}
});
lowpassFilter.Cutoff.ValueChanged += e => lowpassText.Text = $"Low Pass: {e.NewValue}hz";
highpassFilter.Cutoff.ValueChanged += e => highpassText.Text = $"High Pass: {e.NewValue}hz";
lowPassSlider.Current.ValueChanged += e =>
{
lowPassText.Text = $"Low Pass: {e.NewValue}hz";
lowPassFilter.Cutoff = e.NewValue;
};
highPassSlider.Current.ValueChanged += e =>
{
highPassText.Text = $"High Pass: {e.NewValue}hz";
highPassFilter.Cutoff = e.NewValue;
};
}
#region Overrides of Drawable
protected override void Update()
{
base.Update();
highPassSlider.Current.Value = highPassFilter.Cutoff;
lowPassSlider.Current.Value = lowPassFilter.Cutoff;
}
#endregion
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Play Track", () => track.Start());
AddStep("Reset filters", () =>
{
lowPassFilter.Cutoff = AudioFilter.MAX_LOWPASS_CUTOFF;
highPassFilter.Cutoff = 0;
});
waitTrackPlay();
}
[Test]
public void TestLowPass()
public void TestLowPassSweep()
{
AddStep("Filter Sweep", () =>
{
lowpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
.CutoffTo(0, 2000, Easing.OutCubic);
});
@ -92,7 +132,7 @@ namespace osu.Game.Tests.Visual.Audio
AddStep("Filter Sweep (reverse)", () =>
{
lowpassFilter.CutoffTo(0).Then()
lowPassFilter.CutoffTo(0).Then()
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
});
@ -101,11 +141,11 @@ namespace osu.Game.Tests.Visual.Audio
}
[Test]
public void TestHighPass()
public void TestHighPassSweep()
{
AddStep("Filter Sweep", () =>
{
highpassFilter.CutoffTo(0).Then()
highPassFilter.CutoffTo(0).Then()
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
});
@ -113,7 +153,7 @@ namespace osu.Game.Tests.Visual.Audio
AddStep("Filter Sweep (reverse)", () =>
{
highpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
highPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
.CutoffTo(0, 2000, Easing.OutCubic);
});

View File

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

View File

@ -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;
/// <summary>
/// The current cutoff of this filter.
/// The cutoff frequency of this filter.
/// </summary>
public BindableNumber<int> Cutoff { get; }
public int Cutoff
{
get => cutoff;
set
{
if (value == cutoff)
return;
cutoff = value;
updateFilter(cutoff);
}
}
/// <summary>
/// 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<int>(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<int> 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();
}
}
}

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
@ -12,7 +11,7 @@ namespace osu.Game.Audio.Effects
/// <summary>
/// The filter cutoff.
/// </summary>
BindableNumber<int> Cutoff { get; }
int Cutoff { get; set; }
}
public static class FilterableAudioComponentExtensions
@ -40,7 +39,7 @@ namespace osu.Game.Audio.Effects
public static TransformSequence<T> CutoffTo<T, TEasing>(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);
/// <summary>
/// Smoothly adjusts filter cutoff over time.
@ -49,6 +48,6 @@ namespace osu.Game.Audio.Effects
public static TransformSequence<T> CutoffTo<T, TEasing>(this TransformSequence<T> 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));
}
}

View File

@ -0,0 +1,111 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Threading;
using Realms;
#nullable enable
namespace osu.Game.Database
{
/// <summary>
/// Provides a method of working with realm objects over longer application lifetimes.
/// </summary>
/// <typeparam name="T">The underlying object type.</typeparam>
public class RealmLive<T> : ILive<T> where T : RealmObject, IHasGuidPrimaryKey
{
public Guid ID { get; }
private readonly SynchronizationContext? fetchedContext;
private readonly int fetchedThreadId;
/// <summary>
/// The original live data used to create this instance.
/// </summary>
private readonly T data;
/// <summary>
/// Construct a new instance of live realm data.
/// </summary>
/// <param name="data">The realm data.</param>
public RealmLive(T data)
{
this.data = data;
fetchedContext = SynchronizationContext.Current;
fetchedThreadId = Thread.CurrentThread.ManagedThreadId;
ID = data.ID;
}
/// <summary>
/// Perform a read operation on this live object.
/// </summary>
/// <param name="perform">The action to perform.</param>
public void PerformRead(Action<T> perform)
{
if (originalDataValid)
{
perform(data);
return;
}
using (var realm = Realm.GetInstance(data.Realm.Config))
perform(realm.Find<T>(ID));
}
/// <summary>
/// Perform a read operation on this live object.
/// </summary>
/// <param name="perform">The action to perform.</param>
public TReturn PerformRead<TReturn>(Func<T, TReturn> 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<T>(ID));
}
/// <summary>
/// Perform a write operation on this live object.
/// </summary>
/// <param name="perform">The action to perform.</param>
public void PerformWrite(Action<T> 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<T>(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;
}
}

View File

@ -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<T>(item);
}
public static List<RealmLive<T>> ToLive<T>(this IEnumerable<T> realmList)
where T : RealmObject, IHasGuidPrimaryKey
{
return realmList.Select(l => new RealmLive<T>(l)).ToList();
}
public static RealmLive<T> ToLive<T>(this T realmObject)
where T : RealmObject, IHasGuidPrimaryKey
{
return new RealmLive<T>(realmObject);
}
}
}

View File

@ -0,0 +1,30 @@
// 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.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();
}
}
}

View File

@ -130,12 +130,6 @@ namespace osu.Game.Online.Rooms
set => MaxAttempts.Value = value;
}
/// <summary>
/// The position of this <see cref="Room"/> in the list. This is not read from or written to the API.
/// </summary>
[JsonIgnore]
public readonly Bindable<long> Position = new Bindable<long>(-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()

View File

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

View File

@ -0,0 +1,112 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.IO;
using 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<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateHasAudioTrack(this),
new IssueTemplateMissingFile(this),
new IssueTemplateFileError(this)
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet;
var videoPaths = new List<string>();
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);
}
}
}

View File

@ -0,0 +1,85 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ManagedBass;
using 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<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateTooShort(this),
new IssueTemplateBadFormat(this)
};
public IEnumerable<Issue> 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);
}
}
}

View File

@ -0,0 +1,43 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.IO;
using 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<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateZeroBytes(this)
};
public IEnumerable<Issue> 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);
}
}
}

View File

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

View File

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

View File

@ -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<bool>(Static.MutedAudioNotificationShownOnce);
batteryWarningShownOnce = sessionStatics.GetBindable<bool>(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<PlayerSettingsGroup>
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<PlayerSettingsGroup>
{
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<PlayerLoader> pushSequence = this.Delay(250);
TransformSequence<PlayerLoader> 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(() =>
{

View File

@ -36,11 +36,12 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="10.6.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.1012.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.1013.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1004.0" />
<PackageReference Include="Sentry" Version="3.9.4" />
<PackageReference Include="SharpCompress" Version="0.29.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="TagLibSharp" Version="2.2.0" />
</ItemGroup>
</Project>

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.1012.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.1013.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1004.0" />
</ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@ -93,7 +93,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2021.1012.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.1013.0" />
<PackageReference Include="SharpCompress" Version="0.28.3" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="SharpRaven" Version="2.4.0" />