1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-15 05:02:55 +08:00

Merge branch 'master' into editor-beatmap-changed-event

This commit is contained in:
Dean Herbert 2019-10-04 17:58:58 +08:00 committed by GitHub
commit 9eab56e2fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 980 additions and 157 deletions

View File

@ -7,6 +7,7 @@ using System.Linq;
using System.Text;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.IO;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public void TestStacking()
{
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(beatmap_data)))
using (var reader = new StreamReader(stream))
using (var reader = new LineBufferedReader(stream))
{
var beatmap = Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
var converted = new TestWorkingBeatmap(beatmap).GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty<Mod>());

View File

@ -13,6 +13,7 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Timing;
using osu.Game.IO;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@ -30,13 +31,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
public void TestDecodeBeatmapVersion()
{
using (var resStream = TestResources.OpenResource("beatmap-version.osu"))
using (var stream = new StreamReader(resStream))
using (var stream = new LineBufferedReader(resStream))
{
var decoder = Decoder.GetDecoder<Beatmap>(stream);
stream.BaseStream.Position = 0;
stream.DiscardBufferedData();
var working = new TestWorkingBeatmap(decoder.Decode(stream));
Assert.AreEqual(6, working.BeatmapInfo.BeatmapVersion);
@ -51,7 +48,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
using (var stream = new StreamReader(resStream))
using (var stream = new LineBufferedReader(resStream))
{
var beatmap = decoder.Decode(stream);
var beatmapInfo = beatmap.BeatmapInfo;
@ -75,7 +72,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder();
using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
using (var stream = new StreamReader(resStream))
using (var stream = new LineBufferedReader(resStream))
{
var beatmapInfo = decoder.Decode(stream).BeatmapInfo;
@ -101,7 +98,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder();
using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
using (var stream = new StreamReader(resStream))
using (var stream = new LineBufferedReader(resStream))
{
var beatmap = decoder.Decode(stream);
var beatmapInfo = beatmap.BeatmapInfo;
@ -126,7 +123,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder();
using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
using (var stream = new StreamReader(resStream))
using (var stream = new LineBufferedReader(resStream))
{
var difficulty = decoder.Decode(stream).BeatmapInfo.BaseDifficulty;
@ -145,7 +142,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
using (var stream = new StreamReader(resStream))
using (var stream = new LineBufferedReader(resStream))
{
var beatmap = decoder.Decode(stream);
var metadata = beatmap.Metadata;
@ -164,7 +161,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
using (var stream = new StreamReader(resStream))
using (var stream = new LineBufferedReader(resStream))
{
var beatmap = decoder.Decode(stream);
var controlPoints = beatmap.ControlPointInfo;
@ -239,7 +236,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("overlapping-control-points.osu"))
using (var stream = new StreamReader(resStream))
using (var stream = new LineBufferedReader(resStream))
{
var controlPoints = decoder.Decode(stream).ControlPointInfo;
@ -271,7 +268,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacySkinDecoder();
using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
using (var stream = new StreamReader(resStream))
using (var stream = new LineBufferedReader(resStream))
{
var comboColors = decoder.Decode(stream).ComboColours;
@ -297,7 +294,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder();
using (var resStream = TestResources.OpenResource("hitobject-combo-offset.osu"))
using (var stream = new StreamReader(resStream))
using (var stream = new LineBufferedReader(resStream))
{
var beatmap = decoder.Decode(stream);
@ -320,7 +317,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder();
using (var resStream = TestResources.OpenResource("hitobject-combo-offset.osu"))
using (var stream = new StreamReader(resStream))
using (var stream = new LineBufferedReader(resStream))
{
var beatmap = decoder.Decode(stream);
@ -343,7 +340,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
using (var stream = new StreamReader(resStream))
using (var stream = new LineBufferedReader(resStream))
{
var hitObjects = decoder.Decode(stream).HitObjects;
@ -371,7 +368,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("controlpoint-custom-samplebank.osu"))
using (var stream = new StreamReader(resStream))
using (var stream = new LineBufferedReader(resStream))
{
var hitObjects = decoder.Decode(stream).HitObjects;
@ -393,7 +390,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("hitobject-custom-samplebank.osu"))
using (var stream = new StreamReader(resStream))
using (var stream = new LineBufferedReader(resStream))
{
var hitObjects = decoder.Decode(stream).HitObjects;
@ -411,7 +408,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("hitobject-file-samples.osu"))
using (var stream = new StreamReader(resStream))
using (var stream = new LineBufferedReader(resStream))
{
var hitObjects = decoder.Decode(stream).HitObjects;
@ -431,7 +428,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("slider-samples.osu"))
using (var stream = new StreamReader(resStream))
using (var stream = new LineBufferedReader(resStream))
{
var hitObjects = decoder.Decode(stream).HitObjects;
@ -475,7 +472,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("hitobject-no-addition-bank.osu"))
using (var stream = new StreamReader(resStream))
using (var stream = new LineBufferedReader(resStream))
{
var hitObjects = decoder.Decode(stream).HitObjects;
@ -489,10 +486,110 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var badResStream = TestResources.OpenResource("invalid-events.osu"))
using (var badStream = new StreamReader(badResStream))
using (var badStream = new LineBufferedReader(badResStream))
{
Assert.DoesNotThrow(() => decoder.Decode(badStream));
}
}
[Test]
public void TestFallbackDecoderForCorruptedHeader()
{
Decoder<Beatmap> decoder = null;
Beatmap beatmap = null;
using (var resStream = TestResources.OpenResource("corrupted-header.osu"))
using (var stream = new LineBufferedReader(resStream))
{
Assert.DoesNotThrow(() => decoder = Decoder.GetDecoder<Beatmap>(stream));
Assert.IsInstanceOf<LegacyBeatmapDecoder>(decoder);
Assert.DoesNotThrow(() => beatmap = decoder.Decode(stream));
Assert.IsNotNull(beatmap);
Assert.AreEqual("Beatmap with corrupted header", beatmap.Metadata.Title);
Assert.AreEqual("Evil Hacker", beatmap.Metadata.AuthorString);
}
}
[Test]
public void TestFallbackDecoderForMissingHeader()
{
Decoder<Beatmap> decoder = null;
Beatmap beatmap = null;
using (var resStream = TestResources.OpenResource("missing-header.osu"))
using (var stream = new LineBufferedReader(resStream))
{
Assert.DoesNotThrow(() => decoder = Decoder.GetDecoder<Beatmap>(stream));
Assert.IsInstanceOf<LegacyBeatmapDecoder>(decoder);
Assert.DoesNotThrow(() => beatmap = decoder.Decode(stream));
Assert.IsNotNull(beatmap);
Assert.AreEqual("Beatmap with no header", beatmap.Metadata.Title);
Assert.AreEqual("Incredibly Evil Hacker", beatmap.Metadata.AuthorString);
}
}
[Test]
public void TestDecodeFileWithEmptyLinesAtStart()
{
Decoder<Beatmap> decoder = null;
Beatmap beatmap = null;
using (var resStream = TestResources.OpenResource("empty-lines-at-start.osu"))
using (var stream = new LineBufferedReader(resStream))
{
Assert.DoesNotThrow(() => decoder = Decoder.GetDecoder<Beatmap>(stream));
Assert.IsInstanceOf<LegacyBeatmapDecoder>(decoder);
Assert.DoesNotThrow(() => beatmap = decoder.Decode(stream));
Assert.IsNotNull(beatmap);
Assert.AreEqual("Empty lines at start", beatmap.Metadata.Title);
Assert.AreEqual("Edge Case Hunter", beatmap.Metadata.AuthorString);
}
}
[Test]
public void TestDecodeFileWithEmptyLinesAndNoHeader()
{
Decoder<Beatmap> decoder = null;
Beatmap beatmap = null;
using (var resStream = TestResources.OpenResource("empty-line-instead-of-header.osu"))
using (var stream = new LineBufferedReader(resStream))
{
Assert.DoesNotThrow(() => decoder = Decoder.GetDecoder<Beatmap>(stream));
Assert.IsInstanceOf<LegacyBeatmapDecoder>(decoder);
Assert.DoesNotThrow(() => beatmap = decoder.Decode(stream));
Assert.IsNotNull(beatmap);
Assert.AreEqual("The dog ate the file header", beatmap.Metadata.Title);
Assert.AreEqual("Why does this keep happening", beatmap.Metadata.AuthorString);
}
}
[Test]
public void TestDecodeFileWithContentImmediatelyAfterHeader()
{
Decoder<Beatmap> decoder = null;
Beatmap beatmap = null;
using (var resStream = TestResources.OpenResource("no-empty-line-after-header.osu"))
using (var stream = new LineBufferedReader(resStream))
{
Assert.DoesNotThrow(() => decoder = Decoder.GetDecoder<Beatmap>(stream));
Assert.IsInstanceOf<LegacyBeatmapDecoder>(decoder);
Assert.DoesNotThrow(() => beatmap = decoder.Decode(stream));
Assert.IsNotNull(beatmap);
Assert.AreEqual("No empty line delimiting header from contents", beatmap.Metadata.Title);
Assert.AreEqual("Edge Case Hunter", beatmap.Metadata.AuthorString);
}
}
[Test]
public void TestDecodeEmptyFile()
{
using (var resStream = new MemoryStream())
using (var stream = new LineBufferedReader(resStream))
{
Assert.Throws<IOException>(() => Decoder.GetDecoder<Beatmap>(stream));
}
}
}
}

View File

@ -2,9 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.IO;
using NUnit.Framework;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Beatmaps.Formats
@ -18,7 +18,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LineLoggingDecoder(14);
using (var resStream = TestResources.OpenResource("comments.osu"))
using (var stream = new StreamReader(resStream))
using (var stream = new LineBufferedReader(resStream))
{
decoder.Decode(stream);

View File

@ -1,12 +1,12 @@
// 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.IO;
using System.Linq;
using NUnit.Framework;
using osuTK;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.Storyboards;
using osu.Game.Tests.Resources;
@ -21,7 +21,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyStoryboardDecoder();
using (var resStream = TestResources.OpenResource("Himeringo - Yotsuya-san ni Yoroshiku (RLC) [Winber1's Extreme].osu"))
using (var stream = new StreamReader(resStream))
using (var stream = new LineBufferedReader(resStream))
{
var storyboard = decoder.Decode(stream);
@ -94,7 +94,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = new LegacyStoryboardDecoder();
using (var resStream = TestResources.OpenResource("variable-with-suffix.osb"))
using (var stream = new StreamReader(resStream))
using (var stream = new LineBufferedReader(resStream))
{
var storyboard = decoder.Decode(stream);

View File

@ -8,6 +8,7 @@ using NUnit.Framework;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.IO.Serialization;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
@ -148,13 +149,13 @@ namespace osu.Game.Tests.Beatmaps.Formats
private Beatmap decode(string filename, out Beatmap jsonDecoded)
{
using (var stream = TestResources.OpenResource(filename))
using (var sr = new StreamReader(stream))
using (var sr = new LineBufferedReader(stream))
{
var legacyDecoded = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr);
using (var ms = new MemoryStream())
using (var sw = new StreamWriter(ms))
using (var sr2 = new StreamReader(ms))
using (var sr2 = new LineBufferedReader(ms))
{
sw.Write(legacyDecoded.Serialize());
sw.Flush();

View File

@ -171,7 +171,7 @@ namespace osu.Game.Tests.Beatmaps.IO
var breakTemp = TestResources.GetTestBeatmapForImport();
MemoryStream brokenOsu = new MemoryStream(new byte[] { 1, 3, 3, 7 });
MemoryStream brokenOsu = new MemoryStream();
MemoryStream brokenOsz = new MemoryStream(File.ReadAllBytes(breakTemp));
File.Delete(breakTemp);

View File

@ -0,0 +1,133 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.IO;
using System.Text;
using NUnit.Framework;
using osu.Game.IO;
namespace osu.Game.Tests.Beatmaps.IO
{
[TestFixture]
public class LineBufferedReaderTest
{
[Test]
public void TestReadLineByLine()
{
const string contents = @"line 1
line 2
line 3";
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(contents)))
using (var bufferedReader = new LineBufferedReader(stream))
{
Assert.AreEqual("line 1", bufferedReader.ReadLine());
Assert.AreEqual("line 2", bufferedReader.ReadLine());
Assert.AreEqual("line 3", bufferedReader.ReadLine());
Assert.IsNull(bufferedReader.ReadLine());
}
}
[Test]
public void TestPeekLineOnce()
{
const string contents = @"line 1
peek this
line 3";
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(contents)))
using (var bufferedReader = new LineBufferedReader(stream))
{
Assert.AreEqual("line 1", bufferedReader.ReadLine());
Assert.AreEqual("peek this", bufferedReader.PeekLine());
Assert.AreEqual("peek this", bufferedReader.ReadLine());
Assert.AreEqual("line 3", bufferedReader.ReadLine());
Assert.IsNull(bufferedReader.ReadLine());
}
}
[Test]
public void TestPeekLineMultipleTimes()
{
const string contents = @"peek this once
line 2
peek this a lot";
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(contents)))
using (var bufferedReader = new LineBufferedReader(stream))
{
Assert.AreEqual("peek this once", bufferedReader.PeekLine());
Assert.AreEqual("peek this once", bufferedReader.ReadLine());
Assert.AreEqual("line 2", bufferedReader.ReadLine());
Assert.AreEqual("peek this a lot", bufferedReader.PeekLine());
Assert.AreEqual("peek this a lot", bufferedReader.PeekLine());
Assert.AreEqual("peek this a lot", bufferedReader.PeekLine());
Assert.AreEqual("peek this a lot", bufferedReader.ReadLine());
Assert.IsNull(bufferedReader.ReadLine());
}
}
[Test]
public void TestPeekLineAtEndOfStream()
{
const string contents = @"first line
second line";
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(contents)))
using (var bufferedReader = new LineBufferedReader(stream))
{
Assert.AreEqual("first line", bufferedReader.ReadLine());
Assert.AreEqual("second line", bufferedReader.ReadLine());
Assert.IsNull(bufferedReader.PeekLine());
Assert.IsNull(bufferedReader.ReadLine());
Assert.IsNull(bufferedReader.PeekLine());
}
}
[Test]
public void TestPeekReadLineOnEmptyStream()
{
using (var stream = new MemoryStream())
using (var bufferedReader = new LineBufferedReader(stream))
{
Assert.IsNull(bufferedReader.PeekLine());
Assert.IsNull(bufferedReader.ReadLine());
Assert.IsNull(bufferedReader.ReadLine());
Assert.IsNull(bufferedReader.PeekLine());
}
}
[Test]
public void TestReadToEndNoPeeks()
{
const string contents = @"first line
second line";
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(contents)))
using (var bufferedReader = new LineBufferedReader(stream))
{
Assert.AreEqual(contents, bufferedReader.ReadToEnd());
}
}
[Test]
public void TestReadToEndAfterReadsAndPeeks()
{
const string contents = @"this line is gone
this one shouldn't be
these ones
definitely not";
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(contents)))
using (var bufferedReader = new LineBufferedReader(stream))
{
Assert.AreEqual("this line is gone", bufferedReader.ReadLine());
Assert.AreEqual("this one shouldn't be", bufferedReader.PeekLine());
const string ending = @"this one shouldn't be
these ones
definitely not";
Assert.AreEqual(ending, bufferedReader.ReadToEnd());
}
}
}
}

View File

@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Tests.Resources;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.IO.Archives;
namespace osu.Game.Tests.Beatmaps.IO
@ -50,7 +51,7 @@ namespace osu.Game.Tests.Beatmaps.IO
Beatmap beatmap;
using (var stream = new StreamReader(reader.GetStream("Soleily - Renatus (Deif) [Platter].osu")))
using (var stream = new LineBufferedReader(reader.GetStream("Soleily - Renatus (Deif) [Platter].osu")))
beatmap = Decoder.GetDecoder<Beatmap>(stream).Decode(stream);
var meta = beatmap.Metadata;

View File

@ -0,0 +1,5 @@
ow computerosu file format v14
[Metadata]
Title: Beatmap with corrupted header
Creator: Evil Hacker

View File

@ -0,0 +1,5 @@

[Metadata]
Title: The dog ate the file header
Creator: Why does this keep happening

View File

@ -0,0 +1,8 @@

osu file format v14
[Metadata]
Title: Empty lines at start
Creator: Edge Case Hunter

View File

@ -0,0 +1,4 @@
[Metadata]
Title: Beatmap with no header
Creator: Incredibly Evil Hacker

View File

@ -0,0 +1,4 @@
osu file format v14
[Metadata]
Title: No empty line delimiting header from contents
Creator: Edge Case Hunter

View File

@ -2,8 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.IO;
using NUnit.Framework;
using osu.Game.IO;
using osu.Game.Skinning;
using osu.Game.Tests.Resources;
using osuTK.Graphics;
@ -20,7 +20,7 @@ namespace osu.Game.Tests.Skins
var decoder = new LegacySkinDecoder();
using (var resStream = TestResources.OpenResource(hasColours ? "skin.ini" : "skin-empty.ini"))
using (var stream = new StreamReader(resStream))
using (var stream = new LineBufferedReader(resStream))
{
var comboColors = decoder.Decode(stream).ComboColours;
@ -48,7 +48,7 @@ namespace osu.Game.Tests.Skins
var decoder = new LegacySkinDecoder();
using (var resStream = TestResources.OpenResource("skin.ini"))
using (var stream = new StreamReader(resStream))
using (var stream = new LineBufferedReader(resStream))
{
var config = decoder.Decode(stream);

View File

@ -127,14 +127,47 @@ namespace osu.Game.Tests.Visual.Gameplay
exitAndConfirm();
}
[Test]
public void TestExitFromFailedGameplay()
{
AddUntilStep("wait for fail", () => Player.HasFailed);
AddStep("exit", () => Player.Exit());
confirmExited();
}
[Test]
public void TestQuickRetryFromFailedGameplay()
{
AddUntilStep("wait for fail", () => Player.HasFailed);
AddStep("quick retry", () => Player.GameplayClockContainer.OfType<HotkeyRetryOverlay>().First().Action?.Invoke());
confirmExited();
}
[Test]
public void TestQuickExitFromFailedGameplay()
{
AddUntilStep("wait for fail", () => Player.HasFailed);
AddStep("quick exit", () => Player.GameplayClockContainer.OfType<HotkeyExitOverlay>().First().Action?.Invoke());
confirmExited();
}
[Test]
public void TestExitFromGameplay()
{
AddStep("exit", () => Player.Exit());
confirmPaused();
confirmExited();
}
exitAndConfirm();
[Test]
public void TestQuickExitFromGameplay()
{
AddStep("quick exit", () => Player.GameplayClockContainer.OfType<HotkeyExitOverlay>().First().Action?.Invoke());
confirmExited();
}
[Test]

View File

@ -7,10 +7,16 @@ using System.Linq;
using System.Threading;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.MathUtils;
using osu.Framework.Screens;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
@ -18,25 +24,49 @@ using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.PlayerSettings;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestScenePlayerLoader : ManualInputManagerTestScene
{
private TestPlayerLoader loader;
private OsuScreenStack stack;
private TestPlayerLoaderContainer container;
private TestPlayer player;
[SetUp]
public void Setup() => Schedule(() =>
[Resolved]
private AudioManager audioManager { get; set; }
[Resolved]
private SessionStatics sessionStatics { get; set; }
/// <summary>
/// Sets the input manager child to a new test player loader container instance.
/// </summary>
/// <param name="interactive">If the test player should behave like the production one.</param>
/// <param name="beforeLoadAction">An action to run before player load but after bindable leases are returned.</param>
/// <param name="afterLoadAction">An action to run after container load.</param>
public void ResetPlayer(bool interactive, Action beforeLoadAction = null, Action afterLoadAction = null)
{
InputManager.Child = stack = new OsuScreenStack { RelativeSizeAxes = Axes.Both };
audioManager.Volume.SetDefault();
InputManager.Clear();
beforeLoadAction?.Invoke();
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
});
InputManager.Child = container = new TestPlayerLoaderContainer(
loader = new TestPlayerLoader(() =>
{
afterLoadAction?.Invoke();
return player = new TestPlayer(interactive, interactive);
}));
}
[Test]
public void TestBlockLoadViaMouseMovement()
{
AddStep("load dummy beatmap", () => stack.Push(loader = new TestPlayerLoader(() => new TestPlayer(false, false))));
AddStep("load dummy beatmap", () => ResetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddRepeatStep("move mouse", () => InputManager.MoveMouseTo(loader.VisualSettings.ScreenSpaceDrawQuad.TopLeft + (loader.VisualSettings.ScreenSpaceDrawQuad.BottomRight - loader.VisualSettings.ScreenSpaceDrawQuad.TopLeft) * RNG.NextSingle()), 20);
AddAssert("loader still active", () => loader.IsCurrentScreen());
@ -46,16 +76,17 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestLoadContinuation()
{
Player player = null;
SlowLoadPlayer slowPlayer = null;
AddStep("load dummy beatmap", () => stack.Push(loader = new TestPlayerLoader(() => player = new TestPlayer(false, false))));
AddStep("load dummy beatmap", () => ResetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre));
AddUntilStep("wait for player to be current", () => player.IsCurrentScreen());
AddStep("load slow dummy beatmap", () =>
{
stack.Push(loader = new TestPlayerLoader(() => slowPlayer = new SlowLoadPlayer(false, false)));
InputManager.Child = container = new TestPlayerLoaderContainer(
loader = new TestPlayerLoader(() => slowPlayer = new SlowLoadPlayer(false, false)));
Scheduler.AddDelayed(() => slowPlayer.AllowLoad.Set(), 5000);
});
@ -65,16 +96,11 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestModReinstantiation()
{
TestPlayer player = null;
TestMod gameMod = null;
TestMod playerMod1 = null;
TestMod playerMod2 = null;
AddStep("load player", () =>
{
Mods.Value = new[] { gameMod = new TestMod() };
stack.Push(loader = new TestPlayerLoader(() => player = new TestPlayer()));
});
AddStep("load player", () => { ResetPlayer(true, () => Mods.Value = new[] { gameMod = new TestMod() }); });
AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen());
AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre));
@ -97,6 +123,75 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("player mods applied", () => playerMod2.Applied);
}
[Test]
public void TestMutedNotificationMasterVolume() => addVolumeSteps("master volume", () => audioManager.Volume.Value = 0, null, () => audioManager.Volume.IsDefault);
[Test]
public void TestMutedNotificationTrackVolume() => addVolumeSteps("music volume", () => audioManager.VolumeTrack.Value = 0, null, () => audioManager.VolumeTrack.IsDefault);
[Test]
public void TestMutedNotificationMuteButton() => addVolumeSteps("mute button", null, () => container.VolumeOverlay.IsMuted.Value = true, () => !container.VolumeOverlay.IsMuted.Value);
/// <remarks>
/// Created for avoiding copy pasting code for the same steps.
/// </remarks>
/// <param name="volumeName">What part of the volume system is checked</param>
/// <param name="beforeLoad">The action to be invoked to set the volume before loading</param>
/// <param name="afterLoad">The action to be invoked to set the volume after loading</param>
/// <param name="assert">The function to be invoked and checked</param>
private void addVolumeSteps(string volumeName, Action beforeLoad, Action afterLoad, Func<bool> assert)
{
AddStep("reset notification lock", () => sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce).Value = false);
AddStep("load player", () => ResetPlayer(false, beforeLoad, afterLoad));
AddUntilStep("wait for player", () => player.IsLoaded);
AddAssert("check for notification", () => container.NotificationOverlay.UnreadCount.Value == 1);
AddStep("click notification", () =>
{
var scrollContainer = (OsuScrollContainer)container.NotificationOverlay.Children.Last();
var flowContainer = scrollContainer.Children.OfType<FillFlowContainer<NotificationSection>>().First();
var notification = flowContainer.First();
InputManager.MoveMouseTo(notification);
InputManager.Click(MouseButton.Left);
});
AddAssert("check " + volumeName, assert);
}
private class TestPlayerLoaderContainer : Container
{
[Cached]
public readonly NotificationOverlay NotificationOverlay;
[Cached]
public readonly VolumeOverlay VolumeOverlay;
public TestPlayerLoaderContainer(IScreen screen)
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new OsuScreenStack(screen)
{
RelativeSizeAxes = Axes.Both,
},
NotificationOverlay = new NotificationOverlay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
},
VolumeOverlay = new VolumeOverlay
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
}
};
}
}
private class TestPlayerLoader : PlayerLoader
{
public new VisualSettings VisualSettings => base.VisualSettings;

View File

@ -239,6 +239,18 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("Selection is non-null", () => currentSelection != null);
setSelected(1, 3);
}
[Test]
public void TestFilterRange()
{
loadBeatmaps();
// buffer the selection
setSelected(3, 2);
setSelected(1, 3);
AddStep("Apply a range filter", () => carousel.Filter(new FilterCriteria
{
SearchText = "#3",
@ -249,9 +261,9 @@ namespace osu.Game.Tests.Visual.SongSelect
IsLowerInclusive = true
}
}, false));
waitForSelection(3, 2);
AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
// should reselect the buffered selection.
waitForSelection(3, 2);
}
/// <summary>

View File

@ -75,7 +75,6 @@ namespace osu.Game.Tests.Visual.SongSelect
testBeatmapLabels(instance);
// TODO: adjust cases once more info is shown for other gamemodes
switch (instance)
{
case OsuRuleset _:
@ -99,8 +98,6 @@ namespace osu.Game.Tests.Visual.SongSelect
break;
}
}
testNullBeatmap();
}
private void testBeatmapLabels(Ruleset ruleset)
@ -117,7 +114,8 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("check info labels count", () => infoWedge.Info.InfoLabelContainer.Children.Count == expectedCount);
}
private void testNullBeatmap()
[Test]
public void TestNullBeatmap()
{
selectBeatmap(null);
AddAssert("check empty version", () => string.IsNullOrEmpty(infoWedge.Info.VersionLabel.Text));
@ -127,6 +125,12 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("check no info labels", () => !infoWedge.Info.InfoLabelContainer.Children.Any());
}
[Test]
public void TestTruncation()
{
selectBeatmap(createLongMetadata());
}
private void selectBeatmap([CanBeNull] IBeatmap b)
{
BeatmapInfoWedge.BufferedWedgeInfo infoBefore = null;
@ -166,6 +170,25 @@ namespace osu.Game.Tests.Visual.SongSelect
};
}
private IBeatmap createLongMetadata()
{
return new Beatmap
{
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata
{
AuthorString = "WWWWWWWWWWWWWWW",
Artist = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Artist",
Source = "Verrrrry long Source",
Title = "Verrrrry long Title"
},
Version = "Verrrrrrrrrrrrrrrrrrrrrrrrrrrrry long Version",
Status = BeatmapSetOnlineStatus.Graveyard,
},
};
}
private class TestBeatmapInfoWedge : BeatmapInfoWedge
{
public new BufferedWedgeInfo Info => base.Info;

View File

@ -0,0 +1,49 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneLabelledSwitchButton : OsuTestScene
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(LabelledSwitchButton),
typeof(SwitchButton)
};
[TestCase(false)]
[TestCase(true)]
public void TestSwitchButton(bool hasDescription) => createSwitchButton(hasDescription);
private void createSwitchButton(bool hasDescription = false)
{
AddStep("create component", () =>
{
LabelledSwitchButton component;
Child = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 500,
AutoSizeAxes = Axes.Y,
Child = component = new LabelledSwitchButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
component.Label = "a sample component";
component.Description = hasDescription ? "this text describes the component" : string.Empty;
});
}
}
}

View File

@ -0,0 +1,44 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneSwitchButton : ManualInputManagerTestScene
{
private SwitchButton switchButton;
[SetUp]
public void Setup() => Schedule(() =>
{
Child = switchButton = new SwitchButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
});
[Test]
public void TestChangeThroughInput()
{
AddStep("move to switch button", () => InputManager.MoveMouseTo(switchButton));
AddStep("click on", () => InputManager.Click(MouseButton.Left));
AddStep("click off", () => InputManager.Click(MouseButton.Left));
}
[Test]
public void TestChangeThroughBindable()
{
BindableBool bindable = null;
AddStep("bind bindable", () => switchButton.Current.BindTo(bindable = new BindableBool()));
AddStep("toggle bindable", () => bindable.Toggle());
AddStep("toggle bindable", () => bindable.Toggle());
}
}
}

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.Graphics.Video;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.Tests.Resources;
@ -56,7 +57,7 @@ namespace osu.Game.Tests
private Beatmap createTestBeatmap()
{
using (var beatmapStream = getBeatmapStream())
using (var beatmapReader = new StreamReader(beatmapStream))
using (var beatmapReader = new LineBufferedReader(beatmapStream))
return Decoder.GetDecoder<Beatmap>(beatmapReader).Decode(beatmapReader);
}
}

View File

@ -20,6 +20,7 @@ using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.Beatmaps.Formats;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
@ -264,7 +265,7 @@ namespace osu.Game.Beatmaps
}
Beatmap beatmap;
using (var stream = new StreamReader(reader.GetStream(mapName)))
using (var stream = new LineBufferedReader(reader.GetStream(mapName)))
beatmap = Decoder.GetDecoder<Beatmap>(stream).Decode(stream);
return new BeatmapSetInfo
@ -287,7 +288,7 @@ namespace osu.Game.Beatmaps
{
using (var raw = Files.Store.GetStream(file.FileInfo.StoragePath))
using (var ms = new MemoryStream()) //we need a memory stream so we can seek
using (var sr = new StreamReader(ms))
using (var sr = new LineBufferedReader(ms))
{
raw.CopyTo(ms);
ms.Position = 0;

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.IO;
using System.Linq;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
@ -11,6 +10,7 @@ using osu.Framework.Graphics.Video;
using osu.Framework.IO.Stores;
using osu.Framework.Logging;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.Skinning;
using osu.Game.Storyboards;
@ -33,7 +33,7 @@ namespace osu.Game.Beatmaps
{
try
{
using (var stream = new StreamReader(store.GetStream(getPathForFile(BeatmapInfo.Path))))
using (var stream = new LineBufferedReader(store.GetStream(getPathForFile(BeatmapInfo.Path))))
return Decoder.GetDecoder<Beatmap>(stream).Decode(stream);
}
catch
@ -127,7 +127,7 @@ namespace osu.Game.Beatmaps
try
{
using (var stream = new StreamReader(store.GetStream(getPathForFile(BeatmapInfo.Path))))
using (var stream = new LineBufferedReader(store.GetStream(getPathForFile(BeatmapInfo.Path))))
{
var decoder = Decoder.GetDecoder<Storyboard>(stream);
@ -136,7 +136,7 @@ namespace osu.Game.Beatmaps
storyboard = decoder.Decode(stream);
else
{
using (var secondaryStream = new StreamReader(store.GetStream(getPathForFile(BeatmapSetInfo.StoryboardFile))))
using (var secondaryStream = new LineBufferedReader(store.GetStream(getPathForFile(BeatmapSetInfo.StoryboardFile))))
storyboard = decoder.Decode(stream, secondaryStream);
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using osu.Game.IO;
namespace osu.Game.Beatmaps.Formats
{
@ -13,20 +14,21 @@ namespace osu.Game.Beatmaps.Formats
{
protected virtual TOutput CreateTemplateObject() => new TOutput();
public TOutput Decode(StreamReader primaryStream, params StreamReader[] otherStreams)
public TOutput Decode(LineBufferedReader primaryStream, params LineBufferedReader[] otherStreams)
{
var output = CreateTemplateObject();
foreach (StreamReader stream in otherStreams.Prepend(primaryStream))
foreach (LineBufferedReader stream in otherStreams.Prepend(primaryStream))
ParseStreamInto(stream, output);
return output;
}
protected abstract void ParseStreamInto(StreamReader stream, TOutput output);
protected abstract void ParseStreamInto(LineBufferedReader stream, TOutput output);
}
public abstract class Decoder
{
private static readonly Dictionary<Type, Dictionary<string, Func<string, Decoder>>> decoders = new Dictionary<Type, Dictionary<string, Func<string, Decoder>>>();
private static readonly Dictionary<Type, Func<Decoder>> fallback_decoders = new Dictionary<Type, Func<Decoder>>();
static Decoder()
{
@ -39,7 +41,7 @@ namespace osu.Game.Beatmaps.Formats
/// Retrieves a <see cref="Decoder"/> to parse a <see cref="Beatmap"/>.
/// </summary>
/// <param name="stream">A stream pointing to the <see cref="Beatmap"/>.</param>
public static Decoder<T> GetDecoder<T>(StreamReader stream)
public static Decoder<T> GetDecoder<T>(LineBufferedReader stream)
where T : new()
{
if (stream == null)
@ -48,21 +50,31 @@ namespace osu.Game.Beatmaps.Formats
if (!decoders.TryGetValue(typeof(T), out var typedDecoders))
throw new IOException(@"Unknown decoder type");
string line;
// start off with the first line of the file
string line = stream.PeekLine()?.Trim();
do
while (line != null && line.Length == 0)
{
line = stream.ReadLine()?.Trim();
} while (line != null && line.Length == 0);
// consume the previously peeked empty line and advance to the next one
stream.ReadLine();
line = stream.PeekLine()?.Trim();
}
if (line == null)
throw new IOException(@"Unknown file format (null)");
throw new IOException("Unknown file format (null)");
var decoder = typedDecoders.Select(d => line.StartsWith(d.Key, StringComparison.InvariantCulture) ? d.Value : null).FirstOrDefault();
if (decoder == null)
throw new IOException($@"Unknown file format ({line})");
return (Decoder<T>)decoder.Invoke(line);
// it's important the magic does NOT get consumed here, since sometimes it's part of the structure
// (see JsonBeatmapDecoder - the magic string is the opening brace)
// decoder implementations should therefore not die on receiving their own magic
if (decoder != null)
return (Decoder<T>)decoder.Invoke(line);
if (!fallback_decoders.TryGetValue(typeof(T), out var fallbackDecoder))
throw new IOException($"Unknown file format ({line})");
return (Decoder<T>)fallbackDecoder.Invoke();
}
/// <summary>
@ -77,5 +89,19 @@ namespace osu.Game.Beatmaps.Formats
typedDecoders[magic] = constructor;
}
/// <summary>
/// Registers a fallback decoder instantiation function.
/// The fallback will be returned if the first non-empty line of the decoded stream does not match any known magic.
/// </summary>
/// <typeparam name="T">Type of object being decoded.</typeparam>
/// <param name="constructor">A function that constructs the fallback<see cref="Decoder"/>.</param>
protected static void SetFallbackDecoder<T>(Func<Decoder> constructor)
{
if (fallback_decoders.ContainsKey(typeof(T)))
throw new InvalidOperationException($"A fallback decoder was already added for type {typeof(T)}.");
fallback_decoders[typeof(T)] = constructor;
}
}
}

View File

@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.IO;
using osu.Game.IO;
using osu.Game.IO.Serialization;
namespace osu.Game.Beatmaps.Formats
@ -13,11 +13,8 @@ namespace osu.Game.Beatmaps.Formats
AddDecoder<Beatmap>("{", m => new JsonBeatmapDecoder());
}
protected override void ParseStreamInto(StreamReader stream, Beatmap output)
protected override void ParseStreamInto(LineBufferedReader stream, Beatmap output)
{
stream.BaseStream.Position = 0;
stream.DiscardBufferedData();
stream.ReadToEnd().DeserializeInto(output);
foreach (var hitObject in output.HitObjects)

View File

@ -8,6 +8,7 @@ using osu.Framework.IO.File;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.IO;
namespace osu.Game.Beatmaps.Formats
{
@ -25,6 +26,7 @@ namespace osu.Game.Beatmaps.Formats
public static void Register()
{
AddDecoder<Beatmap>(@"osu file format v", m => new LegacyBeatmapDecoder(Parsing.ParseInt(m.Split('v').Last())));
SetFallbackDecoder<Beatmap>(() => new LegacyBeatmapDecoder());
}
/// <summary>
@ -41,7 +43,7 @@ namespace osu.Game.Beatmaps.Formats
offset = FormatVersion < 5 ? 24 : 0;
}
protected override void ParseStreamInto(StreamReader stream, Beatmap beatmap)
protected override void ParseStreamInto(LineBufferedReader stream, Beatmap beatmap)
{
this.beatmap = beatmap;
this.beatmap.BeatmapInfo.BeatmapVersion = FormatVersion;

View File

@ -3,10 +3,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using osu.Framework.Logging;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.IO;
using osuTK.Graphics;
namespace osu.Game.Beatmaps.Formats
@ -21,7 +21,7 @@ namespace osu.Game.Beatmaps.Formats
FormatVersion = version;
}
protected override void ParseStreamInto(StreamReader stream, T output)
protected override void ParseStreamInto(LineBufferedReader stream, T output)
{
Section section = Section.None;

View File

@ -24,6 +24,7 @@ namespace osu.Game.Beatmaps.Formats
public new static void Register()
{
AddDecoder<Beatmap>(@"osu file format v", m => new LegacyDifficultyCalculatorBeatmapDecoder(int.Parse(m.Split('v').Last())));
SetFallbackDecoder<Beatmap>(() => new LegacyDifficultyCalculatorBeatmapDecoder());
}
protected override TimingControlPoint CreateTimingControlPoint()

View File

@ -10,6 +10,7 @@ using osuTK;
using osuTK.Graphics;
using osu.Framework.Graphics;
using osu.Framework.IO.File;
using osu.Game.IO;
using osu.Game.Storyboards;
namespace osu.Game.Beatmaps.Formats
@ -33,9 +34,10 @@ namespace osu.Game.Beatmaps.Formats
// note that this isn't completely correct
AddDecoder<Storyboard>(@"osu file format v", m => new LegacyStoryboardDecoder());
AddDecoder<Storyboard>(@"[Events]", m => new LegacyStoryboardDecoder());
SetFallbackDecoder<Storyboard>(() => new LegacyStoryboardDecoder());
}
protected override void ParseStreamInto(StreamReader stream, Storyboard storyboard)
protected override void ParseStreamInto(LineBufferedReader stream, Storyboard storyboard)
{
this.storyboard = storyboard;
base.ParseStreamInto(stream, storyboard);

View File

@ -11,11 +11,13 @@ namespace osu.Game.Configuration
protected override void InitialiseDefaults()
{
Set(Static.LoginOverlayDisplayed, false);
Set(Static.MutedAudioNotificationShownOnce, false);
}
}
public enum Static
{
LoginOverlayDisplayed,
MutedAudioNotificationShownOnce
}
}

View File

@ -0,0 +1,15 @@
// 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.
namespace osu.Game.Graphics.UserInterfaceV2
{
public class LabelledSwitchButton : LabelledComponent<SwitchButton>
{
public LabelledSwitchButton()
: base(true)
{
}
protected override SwitchButton CreateComponent() => new SwitchButton();
}
}

View File

@ -0,0 +1,118 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterfaceV2
{
public class SwitchButton : Checkbox
{
private const float border_thickness = 4.5f;
private const float padding = 1.25f;
private readonly Box fill;
private readonly Container switchContainer;
private readonly Drawable switchCircle;
private readonly CircularBorderContainer circularContainer;
private Color4 enabledColour;
private Color4 disabledColour;
public SwitchButton()
{
Size = new Vector2(45, 20);
InternalChild = circularContainer = new CircularBorderContainer
{
RelativeSizeAxes = Axes.Both,
BorderColour = Color4.White,
BorderThickness = border_thickness,
Masking = true,
Children = new Drawable[]
{
fill = new Box
{
RelativeSizeAxes = Axes.Both,
AlwaysPresent = true,
Alpha = 0
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(border_thickness + padding),
Child = switchContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Child = switchCircle = new CircularContainer
{
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Masking = true,
Child = new Box { RelativeSizeAxes = Axes.Both }
}
}
}
}
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
enabledColour = colours.BlueDark;
disabledColour = colours.Gray3;
switchContainer.Colour = enabledColour;
fill.Colour = disabledColour;
}
protected override void LoadComplete()
{
base.LoadComplete();
Current.BindValueChanged(updateState, true);
FinishTransforms(true);
}
private void updateState(ValueChangedEvent<bool> state)
{
switchCircle.MoveToX(state.NewValue ? switchContainer.DrawWidth - switchCircle.DrawWidth : 0, 200, Easing.OutQuint);
fill.FadeTo(state.NewValue ? 1 : 0, 250, Easing.OutQuint);
updateBorder();
}
protected override bool OnHover(HoverEvent e)
{
updateBorder();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateBorder();
base.OnHoverLost(e);
}
private void updateBorder()
{
circularContainer.TransformBorderTo((Current.Value ? enabledColour : disabledColour).Lighten(IsHovered ? 0.3f : 0));
}
private class CircularBorderContainer : CircularContainer
{
public void TransformBorderTo(SRGBColour colour)
=> this.TransformTo(nameof(BorderColour), colour, 250, Easing.OutQuint);
}
}
}

View File

@ -0,0 +1,72 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace osu.Game.IO
{
/// <summary>
/// A <see cref="StreamReader"/>-like decorator (with more limited API) for <see cref="Stream"/>s
/// that allows lines to be peeked without consuming.
/// </summary>
public class LineBufferedReader : IDisposable
{
private readonly StreamReader streamReader;
private readonly Queue<string> lineBuffer;
public LineBufferedReader(Stream stream)
{
streamReader = new StreamReader(stream);
lineBuffer = new Queue<string>();
}
/// <summary>
/// Reads the next line from the stream without consuming it.
/// Subsequent calls to <see cref="PeekLine"/> without a <see cref="ReadLine"/> will return the same string.
/// </summary>
public string PeekLine()
{
if (lineBuffer.Count > 0)
return lineBuffer.Peek();
var line = streamReader.ReadLine();
if (line != null)
lineBuffer.Enqueue(line);
return line;
}
/// <summary>
/// Reads the next line from the stream and consumes it.
/// If a line was peeked, that same line will then be consumed and returned.
/// </summary>
public string ReadLine() => lineBuffer.Count > 0 ? lineBuffer.Dequeue() : streamReader.ReadLine();
/// <summary>
/// Reads the stream to its end and returns the text read.
/// This includes any peeked but unconsumed lines.
/// </summary>
public string ReadToEnd()
{
var remainingText = streamReader.ReadToEnd();
if (lineBuffer.Count == 0)
return remainingText;
var builder = new StringBuilder();
// this might not be completely correct due to varying platform line endings
while (lineBuffer.Count > 0)
builder.AppendLine(lineBuffer.Dequeue());
builder.Append(remainingText);
return builder.ToString();
}
public void Dispose()
{
streamReader?.Dispose();
}
}
}

View File

@ -488,7 +488,8 @@ namespace osu.Game
toolbarElements.Add(d);
});
loadComponentSingleFile(volume = new VolumeOverlay(), leftFloatingOverlayContent.Add);
loadComponentSingleFile(volume = new VolumeOverlay(), leftFloatingOverlayContent.Add, true);
loadComponentSingleFile(new OnScreenDisplay(), Add, true);
loadComponentSingleFile(musicController = new MusicController(), Add, true);

View File

@ -75,7 +75,7 @@ namespace osu.Game.Overlays
/// <summary>
/// Returns whether the current beatmap track is playing.
/// </summary>
public bool IsPlaying => beatmap.Value?.Track.IsRunning ?? false;
public bool IsPlaying => current?.Track.IsRunning ?? false;
private void handleBeatmapAdded(BeatmapSetInfo set) =>
Schedule(() => beatmapSets.Add(set));

View File

@ -32,6 +32,9 @@ namespace osu.Game.Overlays
private readonly BindableDouble muteAdjustment = new BindableDouble();
private readonly Bindable<bool> isMuted = new Bindable<bool>();
public Bindable<bool> IsMuted => isMuted;
[BackgroundDependencyLoader]
private void load(AudioManager audio, OsuColour colours)
{
@ -64,7 +67,8 @@ namespace osu.Game.Overlays
volumeMeterMusic = new VolumeMeter("MUSIC", 125, colours.BlueDarker),
muteButton = new MuteButton
{
Margin = new MarginPadding { Top = 100 }
Margin = new MarginPadding { Top = 100 },
Current = { BindTarget = isMuted }
}
}
},
@ -74,13 +78,13 @@ namespace osu.Game.Overlays
volumeMeterEffect.Bindable.BindTo(audio.VolumeSample);
volumeMeterMusic.Bindable.BindTo(audio.VolumeTrack);
muteButton.Current.ValueChanged += muted =>
isMuted.BindValueChanged(muted =>
{
if (muted.NewValue)
audio.AddAdjustment(AdjustableProperty.Volume, muteAdjustment);
else
audio.RemoveAdjustment(AdjustableProperty.Volume, muteAdjustment);
};
});
}
protected override void LoadComplete()

View File

@ -47,6 +47,11 @@ namespace osu.Game.Rulesets.UI
private IFrameBasedClock parentGameplayClock;
/// <summary>
/// The current direction of playback to be exposed to frame stable children.
/// </summary>
private int direction;
[BackgroundDependencyLoader(true)]
private void load(GameplayClock clock)
{
@ -110,27 +115,22 @@ namespace osu.Game.Rulesets.UI
setClock(); // LoadComplete may not be run yet, but we still want the clock.
validState = true;
manualClock.Rate = parentGameplayClock.Rate;
manualClock.IsRunning = parentGameplayClock.IsRunning;
requireMoreUpdateLoops = false;
var newProposedTime = parentGameplayClock.CurrentTime;
try
{
if (!FrameStablePlayback)
{
manualClock.CurrentTime = newProposedTime;
requireMoreUpdateLoops = false;
return;
}
else if (firstConsumption)
if (firstConsumption)
{
// On the first update, frame-stability seeking would result in unexpected/unwanted behaviour.
// Instead we perform an initial seek to the proposed time.
manualClock.CurrentTime = newProposedTime;
// do a second process to clear out ElapsedTime
// process frame (in addition to finally clause) to clear out ElapsedTime
manualClock.CurrentTime = newProposedTime;
framedClock.ProcessFrame();
firstConsumption = false;
@ -144,11 +144,7 @@ namespace osu.Game.Rulesets.UI
: Math.Max(newProposedTime, manualClock.CurrentTime - sixty_frame_time);
}
if (!isAttached)
{
manualClock.CurrentTime = newProposedTime;
}
else
if (isAttached)
{
double? newTime = ReplayInputHandler.SetFrameFromTime(newProposedTime);
@ -156,19 +152,24 @@ namespace osu.Game.Rulesets.UI
{
// we shouldn't execute for this time value. probably waiting on more replay data.
validState = false;
requireMoreUpdateLoops = true;
manualClock.CurrentTime = newProposedTime;
return;
}
manualClock.CurrentTime = newTime.Value;
newProposedTime = newTime.Value;
}
requireMoreUpdateLoops = manualClock.CurrentTime != parentGameplayClock.CurrentTime;
}
finally
{
if (newProposedTime != manualClock.CurrentTime)
direction = newProposedTime > manualClock.CurrentTime ? 1 : -1;
manualClock.CurrentTime = newProposedTime;
manualClock.Rate = Math.Abs(parentGameplayClock.Rate) * direction;
manualClock.IsRunning = parentGameplayClock.IsRunning;
requireMoreUpdateLoops |= manualClock.CurrentTime != parentGameplayClock.CurrentTime;
// The manual clock time has changed in the above code. The framed clock now needs to be updated
// to ensure that the its time is valid for our children before input is processed
framedClock.ProcessFrame();

View File

@ -137,9 +137,9 @@ namespace osu.Game.Rulesets.UI
{
}
public bool OnPressed(T action) => Target.Children.OfType<KeyCounterAction<T>>().Any(c => c.OnPressed(action, Clock.ElapsedFrameTime > 0));
public bool OnPressed(T action) => Target.Children.OfType<KeyCounterAction<T>>().Any(c => c.OnPressed(action, Clock.Rate >= 0));
public bool OnReleased(T action) => Target.Children.OfType<KeyCounterAction<T>>().Any(c => c.OnReleased(action, Clock.ElapsedFrameTime > 0));
public bool OnReleased(T action) => Target.Children.OfType<KeyCounterAction<T>>().Any(c => c.OnReleased(action, Clock.Rate >= 0));
}
#endregion

View File

@ -299,7 +299,16 @@ namespace osu.Game.Screens.Play
{
if (!this.IsCurrentScreen()) return;
this.Exit();
if (ValidForResume && HasFailed && !FailOverlay.IsPresent)
{
failAnimation.FinishTransforms(true);
return;
}
if (canPause)
Pause();
else
this.Exit();
}
public void Restart()
@ -508,24 +517,12 @@ namespace osu.Game.Screens.Play
return true;
}
if (canPause)
{
Pause();
return true;
}
// ValidForResume is false when restarting
if (ValidForResume)
{
if (pauseCooldownActive && !GameplayClockContainer.IsPaused.Value)
// still want to block if we are within the cooldown period and not already paused.
return true;
if (HasFailed && !FailOverlay.IsPresent)
{
failAnimation.FinishTransforms(true);
return true;
}
}
GameplayClockContainer.ResetLocalAdjustments();

View File

@ -6,6 +6,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@ -14,11 +16,14 @@ using osu.Framework.Localisation;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play.HUD;
@ -53,9 +58,19 @@ namespace osu.Game.Screens.Play
private Task loadTask;
private InputManager inputManager;
private IdleTracker idleTracker;
[Resolved(CanBeNull = true)]
private NotificationOverlay notificationOverlay { get; set; }
[Resolved(CanBeNull = true)]
private VolumeOverlay volumeOverlay { get; set; }
[Resolved]
private AudioManager audioManager { get; set; }
private Bindable<bool> muteWarningShownOnce;
public PlayerLoader(Func<Player> createPlayer)
{
this.createPlayer = createPlayer;
@ -68,8 +83,10 @@ namespace osu.Game.Screens.Play
}
[BackgroundDependencyLoader]
private void load()
private void load(SessionStatics sessionStatics)
{
muteWarningShownOnce = sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce);
InternalChild = (content = new LogoTrackingContainer
{
Anchor = Anchor.Centre,
@ -103,7 +120,22 @@ namespace osu.Game.Screens.Play
loadNewPlayer();
}
private void playerLoaded(Player player) => info.Loading = false;
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
if (!muteWarningShownOnce.Value)
{
//Checks if the notification has not been shown yet and also if master volume is muted, track/music volume is muted or if the whole game is muted.
if (volumeOverlay?.IsMuted.Value == true || audioManager.Volume.Value <= audioManager.Volume.MinValue || audioManager.VolumeTrack.Value <= audioManager.VolumeTrack.MinValue)
{
notificationOverlay?.Post(new MutedNotification());
muteWarningShownOnce.Value = true;
}
}
}
public override void OnResuming(IScreen last)
{
@ -127,7 +159,7 @@ namespace osu.Game.Screens.Play
player.RestartCount = restartCount;
player.RestartRequested = restartRequested;
loadTask = LoadComponentAsync(player, playerLoaded);
loadTask = LoadComponentAsync(player, _ => info.Loading = false);
}
private void contentIn()
@ -185,12 +217,6 @@ namespace osu.Game.Screens.Play
content.StopTracking();
}
protected override void LoadComplete()
{
inputManager = GetContainingInputManager();
base.LoadComplete();
}
private ScheduledDelegate pushDebounce;
protected VisualSettings VisualSettings;
@ -473,5 +499,33 @@ namespace osu.Game.Screens.Play
Loading = true;
}
}
private class MutedNotification : SimpleNotification
{
public MutedNotification()
{
Text = "Your music volume is set to 0%! Click here to restore it.";
}
public override bool IsImportant => true;
[BackgroundDependencyLoader]
private void load(OsuColour colours, AudioManager audioManager, NotificationOverlay notificationOverlay, VolumeOverlay volumeOverlay)
{
Icon = FontAwesome.Solid.VolumeMute;
IconBackgound.Colour = colours.RedDark;
Activated = delegate
{
notificationOverlay.Hide();
volumeOverlay.IsMuted.Value = false;
audioManager.Volume.SetDefault();
audioManager.VolumeTrack.SetDefault();
return true;
};
}
}
}
}

View File

@ -31,7 +31,9 @@ namespace osu.Game.Screens.Select
{
public class BeatmapInfoWedge : OverlayContainer
{
private static readonly Vector2 wedged_container_shear = new Vector2(0.15f, 0);
private const float shear_width = 36.75f;
private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / SongSelect.WEDGED_CONTAINER_SIZE.Y, 0);
private readonly IBindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>();
@ -200,14 +202,17 @@ namespace osu.Game.Screens.Select
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Direction = FillDirection.Vertical,
Margin = new MarginPadding { Top = 10, Left = 25, Right = 10, Bottom = 20 },
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 10, Left = 25, Right = shear_width * 2.5f },
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Children = new Drawable[]
{
VersionLabel = new OsuSpriteText
{
Text = beatmapInfo.Version,
Font = OsuFont.GetFont(size: 24, italics: true),
RelativeSizeAxes = Axes.X,
Truncate = true,
},
}
},
@ -217,7 +222,7 @@ namespace osu.Game.Screens.Select
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Direction = FillDirection.Vertical,
Margin = new MarginPadding { Top = 14, Left = 10, Right = 18, Bottom = 20 },
Padding = new MarginPadding { Top = 14, Right = shear_width / 2 },
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
@ -234,19 +239,24 @@ namespace osu.Game.Screens.Select
Name = "Centre-aligned metadata",
Anchor = Anchor.CentreLeft,
Origin = Anchor.TopLeft,
Y = -22,
Y = -7,
Direction = FillDirection.Vertical,
Margin = new MarginPadding { Top = 15, Left = 25, Right = 10, Bottom = 20 },
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = 25, Right = shear_width },
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Children = new Drawable[]
{
TitleLabel = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 28, italics: true),
RelativeSizeAxes = Axes.X,
Truncate = true,
},
ArtistLabel = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 17, italics: true),
RelativeSizeAxes = Axes.X,
Truncate = true,
},
MapperContainer = new FillFlowContainer
{

View File

@ -41,7 +41,7 @@ namespace osu.Game.Screens.Select
{
public abstract class SongSelect : OsuScreen, IKeyBindingHandler<GlobalAction>
{
private static readonly Vector2 wedged_container_size = new Vector2(0.5f, 245);
public static readonly Vector2 WEDGED_CONTAINER_SIZE = new Vector2(0.5f, 245);
protected const float BACKGROUND_BLUR = 20;
private const float left_area_padding = 20;
@ -109,7 +109,7 @@ namespace osu.Game.Screens.Select
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = -150 },
Size = new Vector2(wedged_container_size.X, 1),
Size = new Vector2(WEDGED_CONTAINER_SIZE.X, 1),
}
}
},
@ -118,11 +118,11 @@ namespace osu.Game.Screens.Select
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(wedged_container_size.X, 1),
Size = new Vector2(WEDGED_CONTAINER_SIZE.X, 1),
Padding = new MarginPadding
{
Bottom = Footer.HEIGHT,
Top = wedged_container_size.Y + left_area_padding,
Top = WEDGED_CONTAINER_SIZE.Y + left_area_padding,
Left = left_area_padding,
Right = left_area_padding * 2,
},
@ -158,7 +158,7 @@ namespace osu.Game.Screens.Select
Child = Carousel = new BeatmapCarousel
{
RelativeSizeAxes = Axes.Both,
Size = new Vector2(1 - wedged_container_size.X, 1),
Size = new Vector2(1 - WEDGED_CONTAINER_SIZE.X, 1),
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
SelectionChanged = updateSelectedBeatmap,
@ -177,7 +177,7 @@ namespace osu.Game.Screens.Select
},
beatmapInfoWedge = new BeatmapInfoWedge
{
Size = wedged_container_size,
Size = WEDGED_CONTAINER_SIZE,
RelativeSizeAxes = Axes.X,
Margin = new MarginPadding
{

View File

@ -14,10 +14,10 @@ namespace osu.Game.Skinning
{
ComboColours.AddRange(new[]
{
new Color4(17, 136, 170, 255),
new Color4(102, 136, 0, 255),
new Color4(204, 102, 0, 255),
new Color4(121, 9, 13, 255)
new Color4(255, 192, 0, 255),
new Color4(0, 202, 0, 255),
new Color4(18, 124, 255, 255),
new Color4(242, 24, 57, 255),
});
}
}

View File

@ -12,6 +12,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Game.Audio;
using osu.Game.IO;
using osu.Game.Rulesets.Scoring;
using osuTK.Graphics;
@ -35,7 +36,7 @@ namespace osu.Game.Skinning
{
Stream stream = storage?.GetStream(filename);
if (stream != null)
using (StreamReader reader = new StreamReader(stream))
using (LineBufferedReader reader = new LineBufferedReader(stream))
Configuration = new LegacySkinDecoder().Decode(reader);
else
Configuration = new DefaultSkinConfiguration();

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.Graphics.Video;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
@ -142,7 +143,7 @@ namespace osu.Game.Tests.Beatmaps
private IBeatmap getBeatmap(string name)
{
using (var resStream = openResource($"{resource_namespace}.{name}.osu"))
using (var stream = new StreamReader(resStream))
using (var stream = new LineBufferedReader(resStream))
{
var decoder = Decoder.GetDecoder<Beatmap>(stream);
((LegacyBeatmapDecoder)decoder).ApplyOffsets = false;

View File

@ -7,6 +7,7 @@ using System.Reflection;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
@ -26,7 +27,7 @@ namespace osu.Game.Tests.Beatmaps
private WorkingBeatmap getBeatmap(string name)
{
using (var resStream = openResource($"{resource_namespace}.{name}.osu"))
using (var stream = new StreamReader(resStream))
using (var stream = new LineBufferedReader(resStream))
{
var decoder = Decoder.GetDecoder<Beatmap>(stream);
((LegacyBeatmapDecoder)decoder).ApplyOffsets = false;

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.IO;
using System.Text;
using osu.Game.Beatmaps;
using osu.Game.IO;
using osu.Game.Rulesets;
using Decoder = osu.Game.Beatmaps.Formats.Decoder;
@ -39,7 +40,7 @@ namespace osu.Game.Tests.Beatmaps
private static Beatmap createTestBeatmap()
{
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(test_beatmap_data)))
using (var reader = new StreamReader(stream))
using (var reader = new LineBufferedReader(stream))
return Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
}