1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-07 12:27:25 +08:00

Merge branch 'master' into gameplay/argon-key-counter_le-retour

This commit is contained in:
Dean Herbert 2023-04-06 23:39:57 +09:00 committed by GitHub
commit 0c71fa1bbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 206 additions and 86 deletions

View File

@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
@ -161,6 +160,21 @@ namespace osu.Game.Tests.Beatmaps.Formats
} }
} }
[Test]
public void TestDecodeImageSpecifiedAsVideo()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("image-specified-as-video.osb"))
using (var stream = new LineBufferedReader(resStream))
{
var beatmap = decoder.Decode(stream);
var metadata = beatmap.Metadata;
Assert.AreEqual("BG.jpg", metadata.BackgroundFile);
}
}
[Test] [Test]
public void TestDecodeBeatmapTimingPoints() public void TestDecodeBeatmapTimingPoints()
{ {
@ -320,6 +334,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
{ {
var comboColors = decoder.Decode(stream).ComboColours; var comboColors = decoder.Decode(stream).ComboColours;
Debug.Assert(comboColors != null);
Color4[] expectedColors = Color4[] expectedColors =
{ {
new Color4(142, 199, 255, 255), new Color4(142, 199, 255, 255),
@ -330,7 +346,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
new Color4(255, 177, 140, 255), new Color4(255, 177, 140, 255),
new Color4(100, 100, 100, 255), // alpha is specified as 100, but should be ignored. new Color4(100, 100, 100, 255), // alpha is specified as 100, but should be ignored.
}; };
Assert.AreEqual(expectedColors.Length, comboColors?.Count); Assert.AreEqual(expectedColors.Length, comboColors.Count);
for (int i = 0; i < expectedColors.Length; i++) for (int i = 0; i < expectedColors.Length; i++)
Assert.AreEqual(expectedColors[i], comboColors[i]); Assert.AreEqual(expectedColors[i], comboColors[i]);
} }
@ -415,14 +431,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsNotNull(positionData); Assert.IsNotNull(positionData);
Assert.IsNotNull(curveData); Assert.IsNotNull(curveData);
Assert.AreEqual(new Vector2(192, 168), positionData.Position); Assert.AreEqual(new Vector2(192, 168), positionData!.Position);
Assert.AreEqual(956, hitObjects[0].StartTime); Assert.AreEqual(956, hitObjects[0].StartTime);
Assert.IsTrue(hitObjects[0].Samples.Any(s => s.Name == HitSampleInfo.HIT_NORMAL)); Assert.IsTrue(hitObjects[0].Samples.Any(s => s.Name == HitSampleInfo.HIT_NORMAL));
positionData = hitObjects[1] as IHasPosition; positionData = hitObjects[1] as IHasPosition;
Assert.IsNotNull(positionData); Assert.IsNotNull(positionData);
Assert.AreEqual(new Vector2(304, 56), positionData.Position); Assert.AreEqual(new Vector2(304, 56), positionData!.Position);
Assert.AreEqual(1285, hitObjects[1].StartTime); Assert.AreEqual(1285, hitObjects[1].StartTime);
Assert.IsTrue(hitObjects[1].Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP)); Assert.IsTrue(hitObjects[1].Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP));
} }
@ -578,8 +594,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test] [Test]
public void TestFallbackDecoderForCorruptedHeader() public void TestFallbackDecoderForCorruptedHeader()
{ {
Decoder<Beatmap> decoder = null; Decoder<Beatmap> decoder = null!;
Beatmap beatmap = null; Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("corrupted-header.osu")) using (var resStream = TestResources.OpenResource("corrupted-header.osu"))
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))
@ -596,8 +612,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test] [Test]
public void TestFallbackDecoderForMissingHeader() public void TestFallbackDecoderForMissingHeader()
{ {
Decoder<Beatmap> decoder = null; Decoder<Beatmap> decoder = null!;
Beatmap beatmap = null; Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("missing-header.osu")) using (var resStream = TestResources.OpenResource("missing-header.osu"))
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))
@ -614,8 +630,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test] [Test]
public void TestDecodeFileWithEmptyLinesAtStart() public void TestDecodeFileWithEmptyLinesAtStart()
{ {
Decoder<Beatmap> decoder = null; Decoder<Beatmap> decoder = null!;
Beatmap beatmap = null; Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("empty-lines-at-start.osu")) using (var resStream = TestResources.OpenResource("empty-lines-at-start.osu"))
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))
@ -632,8 +648,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test] [Test]
public void TestDecodeFileWithEmptyLinesAndNoHeader() public void TestDecodeFileWithEmptyLinesAndNoHeader()
{ {
Decoder<Beatmap> decoder = null; Decoder<Beatmap> decoder = null!;
Beatmap beatmap = null; Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("empty-line-instead-of-header.osu")) using (var resStream = TestResources.OpenResource("empty-line-instead-of-header.osu"))
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))
@ -650,8 +666,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test] [Test]
public void TestDecodeFileWithContentImmediatelyAfterHeader() public void TestDecodeFileWithContentImmediatelyAfterHeader()
{ {
Decoder<Beatmap> decoder = null; Decoder<Beatmap> decoder = null!;
Beatmap beatmap = null; Beatmap beatmap = null!;
using (var resStream = TestResources.OpenResource("no-empty-line-after-header.osu")) using (var resStream = TestResources.OpenResource("no-empty-line-after-header.osu"))
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))
@ -678,7 +694,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
[Test] [Test]
public void TestAllowFallbackDecoderOverwrite() public void TestAllowFallbackDecoderOverwrite()
{ {
Decoder<Beatmap> decoder = null; Decoder<Beatmap> decoder = null!;
using (var resStream = TestResources.OpenResource("corrupted-header.osu")) using (var resStream = TestResources.OpenResource("corrupted-header.osu"))
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osuTK; using osuTK;
@ -30,35 +28,35 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsTrue(storyboard.HasDrawable); Assert.IsTrue(storyboard.HasDrawable);
Assert.AreEqual(6, storyboard.Layers.Count()); Assert.AreEqual(6, storyboard.Layers.Count());
StoryboardLayer background = storyboard.Layers.FirstOrDefault(l => l.Depth == 3); StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3);
Assert.IsNotNull(background); Assert.IsNotNull(background);
Assert.AreEqual(16, background.Elements.Count); Assert.AreEqual(16, background.Elements.Count);
Assert.IsTrue(background.VisibleWhenFailing); Assert.IsTrue(background.VisibleWhenFailing);
Assert.IsTrue(background.VisibleWhenPassing); Assert.IsTrue(background.VisibleWhenPassing);
Assert.AreEqual("Background", background.Name); Assert.AreEqual("Background", background.Name);
StoryboardLayer fail = storyboard.Layers.FirstOrDefault(l => l.Depth == 2); StoryboardLayer fail = storyboard.Layers.Single(l => l.Depth == 2);
Assert.IsNotNull(fail); Assert.IsNotNull(fail);
Assert.AreEqual(0, fail.Elements.Count); Assert.AreEqual(0, fail.Elements.Count);
Assert.IsTrue(fail.VisibleWhenFailing); Assert.IsTrue(fail.VisibleWhenFailing);
Assert.IsFalse(fail.VisibleWhenPassing); Assert.IsFalse(fail.VisibleWhenPassing);
Assert.AreEqual("Fail", fail.Name); Assert.AreEqual("Fail", fail.Name);
StoryboardLayer pass = storyboard.Layers.FirstOrDefault(l => l.Depth == 1); StoryboardLayer pass = storyboard.Layers.Single(l => l.Depth == 1);
Assert.IsNotNull(pass); Assert.IsNotNull(pass);
Assert.AreEqual(0, pass.Elements.Count); Assert.AreEqual(0, pass.Elements.Count);
Assert.IsFalse(pass.VisibleWhenFailing); Assert.IsFalse(pass.VisibleWhenFailing);
Assert.IsTrue(pass.VisibleWhenPassing); Assert.IsTrue(pass.VisibleWhenPassing);
Assert.AreEqual("Pass", pass.Name); Assert.AreEqual("Pass", pass.Name);
StoryboardLayer foreground = storyboard.Layers.FirstOrDefault(l => l.Depth == 0); StoryboardLayer foreground = storyboard.Layers.Single(l => l.Depth == 0);
Assert.IsNotNull(foreground); Assert.IsNotNull(foreground);
Assert.AreEqual(151, foreground.Elements.Count); Assert.AreEqual(151, foreground.Elements.Count);
Assert.IsTrue(foreground.VisibleWhenFailing); Assert.IsTrue(foreground.VisibleWhenFailing);
Assert.IsTrue(foreground.VisibleWhenPassing); Assert.IsTrue(foreground.VisibleWhenPassing);
Assert.AreEqual("Foreground", foreground.Name); Assert.AreEqual("Foreground", foreground.Name);
StoryboardLayer overlay = storyboard.Layers.FirstOrDefault(l => l.Depth == int.MinValue); StoryboardLayer overlay = storyboard.Layers.Single(l => l.Depth == int.MinValue);
Assert.IsNotNull(overlay); Assert.IsNotNull(overlay);
Assert.IsEmpty(overlay.Elements); Assert.IsEmpty(overlay.Elements);
Assert.IsTrue(overlay.VisibleWhenFailing); Assert.IsTrue(overlay.VisibleWhenFailing);
@ -76,7 +74,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var sprite = background.Elements.ElementAt(0) as StoryboardSprite; var sprite = background.Elements.ElementAt(0) as StoryboardSprite;
Assert.NotNull(sprite); Assert.NotNull(sprite);
Assert.IsTrue(sprite.HasCommands); Assert.IsTrue(sprite!.HasCommands);
Assert.AreEqual(new Vector2(320, 240), sprite.InitialPosition); Assert.AreEqual(new Vector2(320, 240), sprite.InitialPosition);
Assert.IsTrue(sprite.IsDrawable); Assert.IsTrue(sprite.IsDrawable);
Assert.AreEqual(Anchor.Centre, sprite.Origin); Assert.AreEqual(Anchor.Centre, sprite.Origin);
@ -171,6 +169,21 @@ namespace osu.Game.Tests.Beatmaps.Formats
} }
} }
[Test]
public void TestDecodeImageSpecifiedAsVideo()
{
var decoder = new LegacyStoryboardDecoder();
using (var resStream = TestResources.OpenResource("image-specified-as-video.osb"))
using (var stream = new LineBufferedReader(resStream))
{
var storyboard = decoder.Decode(stream);
StoryboardLayer foreground = storyboard.Layers.Single(l => l.Name == "Video");
Assert.That(foreground.Elements.Count, Is.Zero);
}
}
[Test] [Test]
public void TestDecodeOutOfRangeLoopAnimationType() public void TestDecodeOutOfRangeLoopAnimationType()
{ {

View File

@ -0,0 +1,4 @@
osu file format v14
[Events]
Video,0,"BG.jpg",0,0

View File

@ -45,6 +45,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved] [Resolved]
private SessionStatics sessionStatics { get; set; } private SessionStatics sessionStatics { get; set; }
[Resolved]
private OsuConfigManager config { get; set; }
[Cached(typeof(INotificationOverlay))] [Cached(typeof(INotificationOverlay))]
private readonly NotificationOverlay notificationOverlay; private readonly NotificationOverlay notificationOverlay;
@ -317,6 +320,7 @@ namespace osu.Game.Tests.Visual.Gameplay
saveVolumes(); saveVolumes();
setFullVolume(); setFullVolume();
AddStep("enable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, true));
AddStep("change epilepsy warning", () => epilepsyWarning = warning); AddStep("change epilepsy warning", () => epilepsyWarning = warning);
AddStep("load dummy beatmap", () => resetPlayer(false)); AddStep("load dummy beatmap", () => resetPlayer(false));
@ -333,12 +337,30 @@ namespace osu.Game.Tests.Visual.Gameplay
restoreVolumes(); restoreVolumes();
} }
[Test]
public void TestEpilepsyWarningWithDisabledStoryboard()
{
saveVolumes();
setFullVolume();
AddStep("disable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, false));
AddStep("change epilepsy warning", () => epilepsyWarning = true);
AddStep("load dummy beatmap", () => resetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddUntilStep("epilepsy warning absent", () => getWarning() == null);
restoreVolumes();
}
[Test] [Test]
public void TestEpilepsyWarningEarlyExit() public void TestEpilepsyWarningEarlyExit()
{ {
saveVolumes(); saveVolumes();
setFullVolume(); setFullVolume();
AddStep("enable storyboards", () => config.SetValue(OsuSetting.ShowStoryboard, true));
AddStep("set epilepsy warning", () => epilepsyWarning = true); AddStep("set epilepsy warning", () => epilepsyWarning = true);
AddStep("load dummy beatmap", () => resetPlayer(false)); AddStep("load dummy beatmap", () => resetPlayer(false));
@ -449,7 +471,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("click notification", () => notification.TriggerClick()); AddStep("click notification", () => notification.TriggerClick());
} }
private EpilepsyWarning getWarning() => loader.ChildrenOfType<EpilepsyWarning>().SingleOrDefault(); private EpilepsyWarning getWarning() => loader.ChildrenOfType<EpilepsyWarning>().SingleOrDefault(w => w.IsAlive);
private partial class TestPlayerLoader : PlayerLoader private partial class TestPlayerLoader : PlayerLoader
{ {

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -22,8 +20,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public partial class TestSceneSkinnableSound : OsuTestScene public partial class TestSceneSkinnableSound : OsuTestScene
{ {
private TestSkinSourceContainer skinSource; private TestSkinSourceContainer skinSource = null!;
private PausableSkinnableSound skinnableSound; private PausableSkinnableSound skinnableSound = null!;
private const string sample_lookup = "Gameplay/normal-sliderslide";
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}; };
// has to be added after the hierarchy above else the `ISkinSource` dependency won't be cached. // has to be added after the hierarchy above else the `ISkinSource` dependency won't be cached.
skinSource.Add(skinnableSound = new PausableSkinnableSound(new SampleInfo("Gameplay/normal-sliderslide"))); skinSource.Add(skinnableSound = new PausableSkinnableSound(new SampleInfo(sample_lookup)));
}); });
} }
@ -99,10 +99,28 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("sample not playing", () => !skinnableSound.IsPlaying); AddAssert("sample not playing", () => !skinnableSound.IsPlaying);
} }
[Test]
public void TestSampleUpdatedBeforePlaybackWhenNotPresent()
{
AddStep("make sample non-present", () => skinnableSound.Hide());
AddUntilStep("ensure not present", () => skinnableSound.IsPresent, () => Is.False);
AddUntilStep("ensure sample loaded", () => skinnableSound.ChildrenOfType<DrawableSample>().Single().Name, () => Is.EqualTo(sample_lookup));
AddStep("change source", () =>
{
skinSource.OverridingSample = new SampleVirtual("new skin");
skinSource.TriggerSourceChanged();
});
AddStep("start sample", () => skinnableSound.Play());
AddUntilStep("sample updated", () => skinnableSound.ChildrenOfType<DrawableSample>().Single().Name, () => Is.EqualTo("new skin"));
}
[Test] [Test]
public void TestSkinChangeDoesntPlayOnPause() public void TestSkinChangeDoesntPlayOnPause()
{ {
DrawableSample sample = null; DrawableSample? sample = null;
AddStep("start sample", () => AddStep("start sample", () =>
{ {
skinnableSound.Play(); skinnableSound.Play();
@ -118,7 +136,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("retrieve and ensure current sample is different", () => AddAssert("retrieve and ensure current sample is different", () =>
{ {
DrawableSample oldSample = sample; DrawableSample? oldSample = sample;
sample = skinnableSound.ChildrenOfType<DrawableSample>().Single(); sample = skinnableSound.ChildrenOfType<DrawableSample>().Single();
return sample != oldSample; return sample != oldSample;
}); });
@ -134,20 +152,29 @@ namespace osu.Game.Tests.Visual.Gameplay
private partial class TestSkinSourceContainer : Container, ISkinSource, ISamplePlaybackDisabler private partial class TestSkinSourceContainer : Container, ISkinSource, ISamplePlaybackDisabler
{ {
[Resolved] [Resolved]
private ISkinSource source { get; set; } private ISkinSource source { get; set; } = null!;
public event Action SourceChanged; public event Action? SourceChanged;
public Bindable<bool> SamplePlaybackDisabled { get; } = new Bindable<bool>(); public Bindable<bool> SamplePlaybackDisabled { get; } = new Bindable<bool>();
public ISample? OverridingSample;
IBindable<bool> ISamplePlaybackDisabler.SamplePlaybackDisabled => SamplePlaybackDisabled; IBindable<bool> ISamplePlaybackDisabler.SamplePlaybackDisabled => SamplePlaybackDisabled;
public Drawable GetDrawableComponent(ISkinComponentLookup lookup) => source?.GetDrawableComponent(lookup); public Drawable? GetDrawableComponent(ISkinComponentLookup lookup) => source.GetDrawableComponent(lookup);
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT); public Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source.GetTexture(componentName, wrapModeS, wrapModeT);
public ISample GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo); public ISample? GetSample(ISampleInfo sampleInfo) => OverridingSample ?? source.GetSample(sampleInfo);
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => source?.GetConfig<TLookup, TValue>(lookup);
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => lookupFunction(this) ? this : source?.FindProvider(lookupFunction); public IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
public IEnumerable<ISkin> AllSources => new[] { this }.Concat(source?.AllSources ?? Enumerable.Empty<ISkin>()); where TLookup : notnull
where TValue : notnull
{
return source.GetConfig<TLookup, TValue>(lookup);
}
public ISkin? FindProvider(Func<ISkin, bool> lookupFunction) => lookupFunction(this) ? this : source.FindProvider(lookupFunction);
public IEnumerable<ISkin> AllSources => new[] { this }.Concat(source.AllSources);
public void TriggerSourceChanged() public void TriggerSourceChanged()
{ {

View File

@ -363,6 +363,19 @@ namespace osu.Game.Beatmaps.Formats
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[3]); beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[3]);
break; break;
case LegacyEventType.Video:
string filename = CleanFilename(split[2]);
// Some very old beatmaps had incorrect type specifications for their backgrounds (ie. using 1 for VIDEO
// instead of 0 for BACKGROUND). To handle this gracefully, check the file extension against known supported
// video extensions and handle similar to a background if it doesn't match.
if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename)))
{
beatmap.BeatmapInfo.Metadata.BackgroundFile = filename;
}
break;
case LegacyEventType.Background: case LegacyEventType.Background:
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]); beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]);
break; break;

View File

@ -109,6 +109,14 @@ namespace osu.Game.Beatmaps.Formats
int offset = Parsing.ParseInt(split[1]); int offset = Parsing.ParseInt(split[1]);
string path = CleanFilename(split[2]); string path = CleanFilename(split[2]);
// See handling in LegacyBeatmapDecoder for the special case where a video type is used but
// the file extension is not a valid video.
//
// This avoids potential weird crashes when ffmpeg attempts to parse an image file as a video
// (see https://github.com/ppy/osu/issues/22829#issuecomment-1465552451).
if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path)))
break;
storyboard.GetLayer("Video").Add(new StoryboardVideo(path, offset)); storyboard.GetLayer("Video").Add(new StoryboardVideo(path, offset));
break; break;
} }
@ -276,7 +284,8 @@ namespace osu.Game.Beatmaps.Formats
switch (type) switch (type)
{ {
case "A": case "A":
timelineGroup?.BlendingParameters.Add(easing, startTime, endTime, BlendingParameters.Additive, startTime == endTime ? BlendingParameters.Additive : BlendingParameters.Inherit); timelineGroup?.BlendingParameters.Add(easing, startTime, endTime, BlendingParameters.Additive,
startTime == endTime ? BlendingParameters.Additive : BlendingParameters.Inherit);
break; break;
case "H": case "H":

View File

@ -71,7 +71,7 @@ namespace osu.Game
[Cached(typeof(OsuGameBase))] [Cached(typeof(OsuGameBase))]
public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider
{ {
public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv" }; public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv", ".mpg", ".wmv", ".m4v" };
public const string OSU_PROTOCOL = "osu://"; public const string OSU_PROTOCOL = "osu://";

View File

@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.UI
isDisposed = true; isDisposed = true;
if (ShaderManager.IsNotNull()) SampleStore.Dispose(); if (SampleStore.IsNotNull()) SampleStore.Dispose();
if (TextureStore.IsNotNull()) TextureStore.Dispose(); if (TextureStore.IsNotNull()) TextureStore.Dispose();
if (ShaderManager.IsNotNull()) ShaderManager.Dispose(); if (ShaderManager.IsNotNull()) ShaderManager.Dispose();
} }

View File

@ -1,15 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Game.Rulesets.UI;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using ManagedBass.Fx; using ManagedBass.Fx;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Sample; using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -21,6 +19,7 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -50,8 +49,7 @@ namespace osu.Game.Screens.Play
private const float duration = 2500; private const float duration = 2500;
private ISample? failSample; private SkinnableSound failSample = null!;
private SampleChannel? failSampleChannel;
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } = null!; private OsuConfigManager config { get; set; } = null!;
@ -76,10 +74,10 @@ namespace osu.Game.Screens.Play
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio, ISkinSource skin, IBindable<WorkingBeatmap> beatmap) private void load(AudioManager audio, IBindable<WorkingBeatmap> beatmap)
{ {
track = beatmap.Value.Track; track = beatmap.Value.Track;
failSample = skin.GetSample(new SampleInfo(@"Gameplay/failsound")); AddInternal(failSample = new SkinnableSound(new SampleInfo("Gameplay/failsound")));
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
{ {
@ -126,7 +124,7 @@ namespace osu.Game.Screens.Play
failHighPassFilter.CutoffTo(300); failHighPassFilter.CutoffTo(300);
failLowPassFilter.CutoffTo(300, duration, Easing.OutCubic); failLowPassFilter.CutoffTo(300, duration, Easing.OutCubic);
failSampleChannel = failSample?.Play(); failSample.Play();
track.AddAdjustment(AdjustableProperty.Frequency, trackFreq); track.AddAdjustment(AdjustableProperty.Frequency, trackFreq);
track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment); track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment);
@ -159,7 +157,7 @@ namespace osu.Game.Screens.Play
/// </summary> /// </summary>
public void Stop() public void Stop()
{ {
failSampleChannel?.Stop(); failSample.Stop();
removeFilters(); removeFilters();
} }

View File

@ -67,6 +67,8 @@ namespace osu.Game.Screens.Play
private OsuScrollContainer settingsScroll = null!; private OsuScrollContainer settingsScroll = null!;
private Bindable<bool> showStoryboards = null!;
private bool backgroundBrightnessReduction; private bool backgroundBrightnessReduction;
private readonly BindableDouble volumeAdjustment = new BindableDouble(1); private readonly BindableDouble volumeAdjustment = new BindableDouble(1);
@ -149,10 +151,11 @@ namespace osu.Game.Screens.Play
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(SessionStatics sessionStatics, AudioManager audio) private void load(SessionStatics sessionStatics, AudioManager audio, OsuConfigManager config)
{ {
muteWarningShownOnce = sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce); muteWarningShownOnce = sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce);
batteryWarningShownOnce = sessionStatics.GetBindable<bool>(Static.LowBatteryNotificationShownOnce); batteryWarningShownOnce = sessionStatics.GetBindable<bool>(Static.LowBatteryNotificationShownOnce);
showStoryboards = config.GetBindable<bool>(OsuSetting.ShowStoryboard);
const float padding = 25; const float padding = 25;
@ -463,7 +466,10 @@ namespace osu.Game.Screens.Play
// only show if the warning was created (i.e. the beatmap needs it) // 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). // and this is not a restart of the map (the warning expires after first load).
if (epilepsyWarning?.IsAlive == true) //
// note the late check of storyboard enable as the user may have just changed it
// from the settings on the loader screen.
if (epilepsyWarning?.IsAlive == true && showStoryboards.Value)
{ {
const double epilepsy_display_length = 3000; const double epilepsy_display_length = 3000;
@ -483,6 +489,7 @@ namespace osu.Game.Screens.Play
{ {
// This goes hand-in-hand with the restoration of low pass filter in contentOut(). // This goes hand-in-hand with the restoration of low pass filter in contentOut().
this.TransformBindableTo(volumeAdjustment, 0, CONTENT_OUT_DURATION, Easing.OutCubic); this.TransformBindableTo(volumeAdjustment, 0, CONTENT_OUT_DURATION, Easing.OutCubic);
epilepsyWarning?.Expire();
} }
pushSequence.Schedule(() => pushSequence.Schedule(() =>

View File

@ -9,7 +9,6 @@ using JetBrains.Annotations;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -70,20 +69,6 @@ namespace osu.Game.Skinning
updateSample(); updateSample();
} }
protected override void LoadComplete()
{
base.LoadComplete();
CurrentSkin.SourceChanged += skinChangedImmediate;
}
private void skinChangedImmediate()
{
// Clean up the previous sample immediately on a source change.
// This avoids a potential call to Play() of an already disposed sample (samples are disposed along with the skin, but SkinChanged is scheduled).
clearPreviousSamples();
}
protected override void SkinChanged(ISkinSource skin) protected override void SkinChanged(ISkinSource skin)
{ {
base.SkinChanged(skin); base.SkinChanged(skin);
@ -109,6 +94,8 @@ namespace osu.Game.Skinning
private void updateSample() private void updateSample()
{ {
clearPreviousSamples();
if (sampleInfo == null) if (sampleInfo == null)
return; return;
@ -129,6 +116,8 @@ namespace osu.Game.Skinning
/// </summary> /// </summary>
public void Play() public void Play()
{ {
FlushPendingSkinChanges();
if (Sample == null) if (Sample == null)
return; return;
@ -172,14 +161,6 @@ namespace osu.Game.Skinning
} }
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (CurrentSkin.IsNotNull())
CurrentSkin.SourceChanged -= skinChangedImmediate;
}
#region Re-expose AudioContainer #region Re-expose AudioContainer
public BindableNumber<double> Volume => sampleContainer.Volume; public BindableNumber<double> Volume => sampleContainer.Volume;

View File

@ -5,6 +5,7 @@ using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Pooling;
using osu.Framework.Threading;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
@ -14,6 +15,8 @@ namespace osu.Game.Skinning
/// </summary> /// </summary>
public abstract partial class SkinReloadableDrawable : PoolableDrawable public abstract partial class SkinReloadableDrawable : PoolableDrawable
{ {
private ScheduledDelegate? pendingSkinChange;
/// <summary> /// <summary>
/// Invoked when <see cref="CurrentSkin"/> has changed. /// Invoked when <see cref="CurrentSkin"/> has changed.
/// </summary> /// </summary>
@ -31,21 +34,30 @@ namespace osu.Game.Skinning
CurrentSkin.SourceChanged += onChange; CurrentSkin.SourceChanged += onChange;
} }
private void onChange() =>
// schedule required to avoid calls after disposed.
// note that this has the side-effect of components only performing a skin change when they are alive.
Scheduler.AddOnce(skinChanged);
protected override void LoadAsyncComplete() protected override void LoadAsyncComplete()
{ {
base.LoadAsyncComplete(); base.LoadAsyncComplete();
skinChanged(); skinChanged();
} }
private void skinChanged() /// <summary>
/// Force any pending <see cref="SkinChanged"/> calls to be performed immediately.
/// </summary>
/// <remarks>
/// When a skin change occurs, the handling provided by this class is scheduled.
/// In some cases, such a sample playback, this can result in the sample being played
/// just before it is updated to a potentially different sample.
///
/// Calling this method will ensure any pending update operations are run immediately.
/// It is recommended to call this before consuming the result of skin changes for anything non-drawable.
/// </remarks>
protected void FlushPendingSkinChanges()
{ {
SkinChanged(CurrentSkin); if (pendingSkinChange == null)
OnSkinChanged?.Invoke(); return;
pendingSkinChange.RunTask();
pendingSkinChange = null;
} }
/// <summary> /// <summary>
@ -56,6 +68,22 @@ namespace osu.Game.Skinning
{ {
} }
private void onChange()
{
// schedule required to avoid calls after disposed.
// note that this has the side-effect of components only performing a skin change when they are alive.
pendingSkinChange?.Cancel();
pendingSkinChange = Scheduler.Add(skinChanged);
}
private void skinChanged()
{
SkinChanged(CurrentSkin);
OnSkinChanged?.Invoke();
pendingSkinChange = null;
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);

View File

@ -115,6 +115,8 @@ namespace osu.Game.Skinning
/// </summary> /// </summary>
public virtual void Play() public virtual void Play()
{ {
FlushPendingSkinChanges();
samplesContainer.ForEach(c => samplesContainer.ForEach(c =>
{ {
if (PlayWhenZeroVolume || c.AggregateVolume.Value > 0) if (PlayWhenZeroVolume || c.AggregateVolume.Value > 0)