1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-22 22:17:46 +08:00

Merge branch 'master' into rework-selection-updates

This commit is contained in:
Dean Herbert 2019-10-06 15:00:56 +08:00 committed by GitHub
commit 179cde917b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
87 changed files with 1433 additions and 410 deletions

View File

@ -27,8 +27,13 @@ namespace osu.Game.Rulesets.Mania.Tests
[Cached(typeof(IReadOnlyList<Mod>))]
private IReadOnlyList<Mod> mods { get; set; } = Array.Empty<Mod>();
[Cached(typeof(IScrollingInfo))]
private IScrollingInfo scrollingInfo;
protected ManiaPlacementBlueprintTestScene()
{
scrollingInfo = ((ScrollingTestContainer)HitObjectContainer).ScrollingInfo;
Add(column = new Column(0)
{
Anchor = Anchor.Centre,
@ -36,15 +41,8 @@ namespace osu.Game.Rulesets.Mania.Tests
AccentColour = Color4.OrangeRed,
Clock = new FramedClock(new StopwatchClock()), // No scroll
});
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.CacheAs(((ScrollingTestContainer)HitObjectContainer).ScrollingInfo);
return dependencies;
AddStep("change direction", () => ((ScrollingTestContainer)HitObjectContainer).Flip());
}
protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both };

View File

@ -3,7 +3,6 @@
using System;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
using osuTK;
@ -49,13 +48,13 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
private double originalStartTime;
protected override bool OnMouseMove(MouseMoveEvent e)
public override void UpdatePosition(Vector2 screenSpacePosition)
{
base.OnMouseMove(e);
base.UpdatePosition(screenSpacePosition);
if (PlacementBegun)
{
var endTime = TimeAt(e.ScreenSpaceMousePosition);
var endTime = TimeAt(screenSpacePosition);
HitObject.StartTime = endTime < originalStartTime ? endTime : originalStartTime;
HitObject.Duration = Math.Abs(endTime - originalStartTime);
@ -65,10 +64,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
headPiece.Width = tailPiece.Width = SnappedWidth;
headPiece.X = tailPiece.X = SnappedMousePosition.X;
originalStartTime = HitObject.StartTime = TimeAt(e.ScreenSpaceMousePosition);
originalStartTime = HitObject.StartTime = TimeAt(screenSpacePosition);
}
return true;
}
}
}

View File

@ -62,19 +62,18 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
return base.OnMouseUp(e);
}
protected override bool OnMouseMove(MouseMoveEvent e)
public override void UpdatePosition(Vector2 screenSpacePosition)
{
if (!PlacementBegun)
Column = ColumnAt(e.ScreenSpaceMousePosition);
Column = ColumnAt(screenSpacePosition);
if (Column == null) return false;
if (Column == null) return;
SnappedWidth = Column.DrawWidth;
// Snap to the column
var parentPos = Parent.ToLocalSpace(Column.ToScreenSpace(new Vector2(Column.DrawWidth / 2, 0)));
SnappedMousePosition = new Vector2(parentPos.X, e.MousePosition.Y);
return true;
SnappedMousePosition = new Vector2(parentPos.X, Parent.ToLocalSpace(screenSpacePosition).Y);
}
protected double TimeAt(Vector2 screenSpacePosition)
@ -86,7 +85,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
// If we're scrolling downwards, a position of 0 is actually further away from the hit target
// so we need to flip the vertical coordinate in the hitobject container's space
var hitObjectPos = Column.HitObjectContainer.ToLocalSpace(applyPositionOffset(screenSpacePosition, false)).Y;
var hitObjectPos = mouseToHitObjectPosition(Column.HitObjectContainer.ToLocalSpace(screenSpacePosition)).Y;
if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
hitObjectPos = hitObjectContainer.DrawHeight - hitObjectPos;
@ -103,16 +102,58 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
scrollingInfo.TimeRange.Value,
Column.HitObjectContainer.DrawHeight);
return applyPositionOffset(Column.HitObjectContainer.ToSpaceOfOtherDrawable(new Vector2(0, pos), Parent), true).Y;
if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
pos = Column.HitObjectContainer.DrawHeight - pos;
return hitObjectToMousePosition(Column.HitObjectContainer.ToSpaceOfOtherDrawable(new Vector2(0, pos), Parent)).Y;
}
protected Column ColumnAt(Vector2 screenSpacePosition)
=> composer.ColumnAt(applyPositionOffset(screenSpacePosition, false));
=> composer.ColumnAt(screenSpacePosition);
private Vector2 applyPositionOffset(Vector2 position, bool reverse)
/// <summary>
/// Converts a mouse position to a hitobject position.
/// </summary>
/// <remarks>
/// Blueprints are centred on the mouse position, such that the hitobject position is anchored at the top or bottom of the blueprint depending on the scroll direction.
/// </remarks>
/// <param name="mousePosition">The mouse position.</param>
/// <returns>The resulting hitobject position, acnhored at the top or bottom of the blueprint depending on the scroll direction.</returns>
private Vector2 mouseToHitObjectPosition(Vector2 mousePosition)
{
position.Y += (scrollingInfo.Direction.Value == ScrollingDirection.Up && !reverse ? -1 : 1) * NotePiece.NOTE_HEIGHT / 2;
return position;
switch (scrollingInfo.Direction.Value)
{
case ScrollingDirection.Up:
mousePosition.Y -= NotePiece.NOTE_HEIGHT / 2;
break;
case ScrollingDirection.Down:
mousePosition.Y += NotePiece.NOTE_HEIGHT / 2;
break;
}
return mousePosition;
}
/// <summary>
/// Converts a hitobject position to a mouse position.
/// </summary>
/// <param name="hitObjectPosition">The hitobject position.</param>
/// <returns>The resulting mouse position, anchored at the centre of the hitobject.</returns>
private Vector2 hitObjectToMousePosition(Vector2 hitObjectPosition)
{
switch (scrollingInfo.Direction.Value)
{
case ScrollingDirection.Up:
hitObjectPosition.Y += NotePiece.NOTE_HEIGHT / 2;
break;
case ScrollingDirection.Down:
hitObjectPosition.Y -= NotePiece.NOTE_HEIGHT / 2;
break;
}
return hitObjectPosition;
}
}
}

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

@ -21,21 +21,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
InternalChild = circlePiece = new HitCirclePiece();
}
protected override void LoadComplete()
{
base.LoadComplete();
// Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame
HitObject.Position = Parent?.ToLocalSpace(GetContainingInputManager().CurrentState.Mouse.Position) ?? Vector2.Zero;
}
protected override void Update()
{
base.Update();
circlePiece.UpdateFrom(HitObject);
}
protected override bool OnClick(ClickEvent e)
{
HitObject.StartTime = EditorClock.CurrentTime;
@ -43,10 +28,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
return true;
}
protected override bool OnMouseMove(MouseMoveEvent e)
public override void UpdatePosition(Vector2 screenSpacePosition)
{
HitObject.Position = e.MousePosition;
return true;
HitObject.Position = ToLocalSpace(screenSpacePosition);
}
}
}

View File

@ -52,28 +52,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
setState(PlacementState.Initial);
}
protected override void LoadComplete()
{
base.LoadComplete();
// Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame
HitObject.Position = Parent?.ToLocalSpace(GetContainingInputManager().CurrentState.Mouse.Position) ?? Vector2.Zero;
}
protected override bool OnMouseMove(MouseMoveEvent e)
public override void UpdatePosition(Vector2 screenSpacePosition)
{
switch (state)
{
case PlacementState.Initial:
HitObject.Position = e.MousePosition;
return true;
HitObject.Position = ToLocalSpace(screenSpacePosition);
break;
case PlacementState.Body:
cursor = e.MousePosition - HitObject.Position;
return true;
cursor = ToLocalSpace(screenSpacePosition) - HitObject.Position;
break;
}
return false;
}
protected override bool OnClick(ClickEvent e)

View File

@ -7,6 +7,7 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
{
@ -50,5 +51,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
return true;
}
public override void UpdatePosition(Vector2 screenSpacePosition)
{
}
}
}

View File

@ -0,0 +1,154 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using Microsoft.EntityFrameworkCore.Internal;
using NUnit.Framework;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
namespace osu.Game.Tests.Beatmaps
{
[TestFixture]
public class EditorBeatmapTest
{
/// <summary>
/// Tests that the addition event is correctly invoked after a hitobject is added.
/// </summary>
[Test]
public void TestHitObjectAddEvent()
{
var editorBeatmap = new EditorBeatmap<OsuHitObject>(new OsuBeatmap());
HitObject addedObject = null;
editorBeatmap.HitObjectAdded += h => addedObject = h;
var hitCircle = new HitCircle();
editorBeatmap.Add(hitCircle);
Assert.That(addedObject, Is.EqualTo(hitCircle));
}
/// <summary>
/// Tests that the removal event is correctly invoked after a hitobject is removed.
/// </summary>
[Test]
public void HitObjectRemoveEvent()
{
var hitCircle = new HitCircle();
var editorBeatmap = new EditorBeatmap<OsuHitObject>(new OsuBeatmap { HitObjects = { hitCircle } });
HitObject removedObject = null;
editorBeatmap.HitObjectRemoved += h => removedObject = h;
editorBeatmap.Remove(hitCircle);
Assert.That(removedObject, Is.EqualTo(hitCircle));
}
/// <summary>
/// Tests that the changed event is correctly invoked after the start time of a hitobject is changed.
/// This tests for hitobjects which were already present before the editor beatmap was constructed.
/// </summary>
[Test]
public void TestInitialHitObjectStartTimeChangeEvent()
{
var hitCircle = new HitCircle();
var editorBeatmap = new EditorBeatmap<OsuHitObject>(new OsuBeatmap { HitObjects = { hitCircle } });
HitObject changedObject = null;
editorBeatmap.StartTimeChanged += h => changedObject = h;
hitCircle.StartTime = 1000;
Assert.That(changedObject, Is.EqualTo(hitCircle));
}
/// <summary>
/// Tests that the changed event is correctly invoked after the start time of a hitobject is changed.
/// This tests for hitobjects which were added to an existing editor beatmap.
/// </summary>
[Test]
public void TestAddedHitObjectStartTimeChangeEvent()
{
var editorBeatmap = new EditorBeatmap<OsuHitObject>(new OsuBeatmap());
HitObject changedObject = null;
editorBeatmap.StartTimeChanged += h => changedObject = h;
var hitCircle = new HitCircle();
editorBeatmap.Add(hitCircle);
Assert.That(changedObject, Is.Null);
hitCircle.StartTime = 1000;
Assert.That(changedObject, Is.EqualTo(hitCircle));
}
/// <summary>
/// Tests that the channged event is not invoked after a hitobject is removed from the beatmap/
/// </summary>
[Test]
public void TestRemovedHitObjectStartTimeChangeEvent()
{
var hitCircle = new HitCircle();
var editorBeatmap = new EditorBeatmap<OsuHitObject>(new OsuBeatmap { HitObjects = { hitCircle } });
HitObject changedObject = null;
editorBeatmap.StartTimeChanged += h => changedObject = h;
editorBeatmap.Remove(hitCircle);
Assert.That(changedObject, Is.Null);
hitCircle.StartTime = 1000;
Assert.That(changedObject, Is.Null);
}
/// <summary>
/// Tests that an added hitobject is correctly inserted to preserve the sorting order of the beatmap.
/// </summary>
[Test]
public void TestAddHitObjectInMiddle()
{
var editorBeatmap = new EditorBeatmap<OsuHitObject>(new OsuBeatmap
{
HitObjects =
{
new HitCircle(),
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 2000 },
}
});
var hitCircle = new HitCircle { StartTime = 1000 };
editorBeatmap.Add(hitCircle);
Assert.That(editorBeatmap.HitObjects.Count(h => h == hitCircle), Is.EqualTo(1));
Assert.That(editorBeatmap.HitObjects.IndexOf(hitCircle), Is.EqualTo(3));
}
/// <summary>
/// Tests that the beatmap remains correctly sorted after the start time of a hitobject is changed.
/// </summary>
[Test]
public void TestResortWhenStartTimeChanged()
{
var hitCircle = new HitCircle { StartTime = 1000 };
var editorBeatmap = new EditorBeatmap<OsuHitObject>(new OsuBeatmap
{
HitObjects =
{
new HitCircle(),
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 1000 },
hitCircle,
new HitCircle { StartTime = 2000 },
}
});
hitCircle.StartTime = 0;
Assert.That(editorBeatmap.HitObjects.Count(h => h == hitCircle), Is.EqualTo(1));
Assert.That(editorBeatmap.HitObjects.IndexOf(hitCircle), Is.EqualTo(1));
}
}
}

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,132 @@ 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));
}
}
[Test]
public void TestAllowFallbackDecoderOverwrite()
{
Decoder<Beatmap> decoder = 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(LegacyDifficultyCalculatorBeatmapDecoder.Register);
using (var resStream = TestResources.OpenResource("corrupted-header.osu"))
using (var stream = new LineBufferedReader(resStream))
{
Assert.DoesNotThrow(() => decoder = Decoder.GetDecoder<Beatmap>(stream));
Assert.IsInstanceOf<LegacyDifficultyCalculatorBeatmapDecoder>(decoder);
}
}
}
}

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

@ -32,6 +32,12 @@ namespace osu.Game.Tests.Visual.Online
Id = 4,
};
private readonly User longUsernameUser = new User
{
Username = "Very Long Long Username",
Id = 5,
};
[Cached]
private ChannelManager channelManager = new ChannelManager();
@ -99,6 +105,12 @@ namespace osu.Game.Tests.Visual.Online
Sender = admin,
Content = "Okay okay, calm down guys. Let's do this!"
}));
AddStep("message from long username", () => testChannel.AddNewMessages(new Message(sequence++)
{
Sender = longUsernameUser,
Content = "Hi guys, my new username is lit!"
}));
}
}
}

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

@ -18,8 +18,8 @@ namespace osu.Game.Tests.Visual.SongSelect
overlay.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, Color4.Purple, null, Key.Number1);
overlay.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, Color4.Purple, null, Key.Number2);
overlay.AddButton(@"Edit", @"Beatmap", FontAwesome.Solid.PencilAlt, Color4.Yellow, null, Key.Number3);
overlay.AddButton(@"Delete", @"Beatmap", FontAwesome.Solid.Trash, Color4.Pink, null, Key.Number4, float.MaxValue);
overlay.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, Color4.Pink, null, Key.Number3);
overlay.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, Color4.Yellow, null, Key.Number4);
Add(overlay);

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

@ -1,42 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using osu.Framework.Bindables;
namespace osu.Game.Beatmaps
{
/// <summary>
/// A <see cref="Bindable{T}"/> for the <see cref="OsuGame"/> beatmap.
/// This should be used sparingly in-favour of <see cref="IBindable{WorkingBeatmap}"/>.
/// </summary>
public abstract class BindableBeatmap : NonNullableBindable<WorkingBeatmap>
{
private WorkingBeatmap lastBeatmap;
protected BindableBeatmap(WorkingBeatmap defaultValue)
: base(defaultValue)
{
BindValueChanged(b => updateAudioTrack(b.NewValue), true);
}
private void updateAudioTrack(WorkingBeatmap beatmap)
{
var trackLoaded = lastBeatmap?.TrackLoaded ?? false;
// compare to last beatmap as sometimes the two may share a track representation (optimisation, see WorkingBeatmap.TransferTo)
if (!trackLoaded || lastBeatmap?.Track != beatmap.Track)
{
if (trackLoaded)
{
Debug.Assert(lastBeatmap != null);
Debug.Assert(lastBeatmap.Track != null);
lastBeatmap.RecycleTrack();
}
}
lastBeatmap = beatmap;
}
}
}

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,17 @@ 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.
/// Calling this method will overwrite any existing global fallback registration for type <see cref="T"/> - use with caution.
/// </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)
{
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

@ -114,7 +114,7 @@ namespace osu.Game.Configuration
Set(OsuSetting.UIScale, 1f, 0.8f, 1.6f, 0.01f);
Set(OsuSetting.UIHoldActivationDelay, 200, 0, 500);
Set(OsuSetting.UIHoldActivationDelay, 200f, 0f, 500f, 50f);
Set(OsuSetting.IntroSequence, IntroSequence.Triangles);
}

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

@ -400,20 +400,17 @@ namespace osu.Game.Database
int i = 0;
using (ContextFactory.GetForWrite())
foreach (var b in items)
{
foreach (var b in items)
{
if (notification.State == ProgressNotificationState.Cancelled)
// user requested abort
return;
if (notification.State == ProgressNotificationState.Cancelled)
// user requested abort
return;
notification.Text = $"Deleting {HumanisedModelName}s ({++i} of {items.Count})";
notification.Text = $"Deleting {HumanisedModelName}s ({++i} of {items.Count})";
Delete(b);
Delete(b);
notification.Progress = (float)i / items.Count;
}
notification.Progress = (float)i / items.Count;
}
notification.State = ProgressNotificationState.Completed;
@ -439,20 +436,17 @@ namespace osu.Game.Database
int i = 0;
using (ContextFactory.GetForWrite())
foreach (var item in items)
{
foreach (var item in items)
{
if (notification.State == ProgressNotificationState.Cancelled)
// user requested abort
return;
if (notification.State == ProgressNotificationState.Cancelled)
// user requested abort
return;
notification.Text = $"Restoring ({++i} of {items.Count})";
notification.Text = $"Restoring ({++i} of {items.Count})";
Undelete(item);
Undelete(item);
notification.Progress = (float)i / items.Count;
}
notification.Progress = (float)i / items.Count;
}
notification.State = ProgressNotificationState.Completed;

View File

@ -30,12 +30,12 @@ namespace osu.Game.Graphics.Containers
public Bindable<double> Progress = new BindableDouble();
private Bindable<int> holdActivationDelay;
private Bindable<float> holdActivationDelay;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
holdActivationDelay = config.GetBindable<int>(OsuSetting.UIHoldActivationDelay);
holdActivationDelay = config.GetBindable<float>(OsuSetting.UIHoldActivationDelay);
}
protected void BeginConfirm()

View File

@ -18,7 +18,7 @@ namespace osu.Game.Graphics.UserInterface
public BackButton(Receptor receptor)
{
receptor.OnBackPressed = () => Action?.Invoke();
receptor.OnBackPressed = () => button.Click();
Size = TwoLayerButton.SIZE_EXTENDED;

View File

@ -2,22 +2,20 @@
// See the LICENCE file in the repository root for full licence text.
using osuTK.Graphics;
using System;
using osu.Framework.Allocation;
using osu.Framework.Input.Events;
using osu.Framework.Platform;
using osu.Game.Input.Bindings;
using osuTK.Input;
using osu.Framework.Input.Bindings;
namespace osu.Game.Graphics.UserInterface
{
/// <summary>
/// A textbox which holds focus eagerly.
/// </summary>
public class FocusedTextBox : OsuTextBox
public class FocusedTextBox : OsuTextBox, IKeyBindingHandler<GlobalAction>
{
public Action Exit;
private bool focus;
private bool allowImmediateFocus => host?.OnScreenKeyboardOverlapsGameWindow != true;
@ -63,12 +61,12 @@ namespace osu.Game.Graphics.UserInterface
if (!HasFocus) return false;
if (e.Key == Key.Escape)
return false; // disable the framework-level handling of escape key for confority (we use GlobalAction.Back).
return false; // disable the framework-level handling of escape key for conformity (we use GlobalAction.Back).
return base.OnKeyDown(e);
}
public override bool OnPressed(GlobalAction action)
public bool OnPressed(GlobalAction action)
{
if (action == GlobalAction.Back)
{
@ -79,14 +77,10 @@ namespace osu.Game.Graphics.UserInterface
}
}
return base.OnPressed(action);
return false;
}
protected override void KillFocus()
{
base.KillFocus();
Exit?.Invoke();
}
public bool OnReleased(GlobalAction action) => false;
public override bool RequestsFocus => HoldFocus;
}

View File

@ -8,13 +8,11 @@ using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.Sprites;
using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Input.Bindings;
namespace osu.Game.Graphics.UserInterface
{
public class OsuTextBox : TextBox, IKeyBindingHandler<GlobalAction>
public class OsuTextBox : TextBox
{
protected override float LeftRightPadding => 10;
@ -57,18 +55,5 @@ namespace osu.Game.Graphics.UserInterface
}
protected override Drawable GetDrawableCharacter(char c) => new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: CalculatedTextSize) };
public virtual bool OnPressed(GlobalAction action)
{
if (action == GlobalAction.Back)
{
KillFocus();
return true;
}
return false;
}
public bool OnReleased(GlobalAction action) => false;
}
}

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

@ -21,8 +21,6 @@ namespace osu.Game.Online.Chat
{
public readonly Bindable<Channel> Channel = new Bindable<Channel>();
public Action Exit;
private readonly FocusedTextBox textbox;
protected ChannelManager ChannelManager;
@ -66,8 +64,6 @@ namespace osu.Game.Online.Chat
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
});
textbox.Exit += () => Exit?.Invoke();
}
Channel.BindValueChanged(channelChanged);
@ -146,6 +142,7 @@ namespace osu.Game.Online.Chat
protected override float HorizontalPadding => 10;
protected override float MessagePadding => 120;
protected override float TimestampPadding => 50;
public StandAloneMessage(Message message)
: base(message)

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

@ -202,7 +202,13 @@ namespace osu.Game
// this adds a global reduction of track volume for the time being.
Audio.Tracks.AddAdjustment(AdjustableProperty.Volume, new BindableDouble(0.8));
beatmap = new OsuBindableBeatmap(defaultBeatmap);
beatmap = new NonNullableBindable<WorkingBeatmap>(defaultBeatmap);
beatmap.BindValueChanged(b => ScheduleAfterChildren(() =>
{
// compare to last beatmap as sometimes the two may share a track representation (optimisation, see WorkingBeatmap.TransferTo)
if (b.OldValue?.TrackLoaded == true && b.OldValue?.Track != b.NewValue?.Track)
b.OldValue.RecycleTrack();
}));
dependencies.CacheAs<IBindable<WorkingBeatmap>>(beatmap);
dependencies.CacheAs(beatmap);
@ -292,14 +298,6 @@ namespace osu.Game
public string[] HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions).ToArray();
private class OsuBindableBeatmap : BindableBeatmap
{
public OsuBindableBeatmap(WorkingBeatmap defaultValue)
: base(defaultValue)
{
}
}
private class OsuUserInputManager : UserInputManager
{
protected override MouseButtonEventManager CreateButtonManagerFor(MouseButton button)

View File

@ -31,7 +31,9 @@ namespace osu.Game.Overlays.Chat
protected virtual float MessagePadding => default_message_padding;
private const float timestamp_padding = 65;
private const float default_timestamp_padding = 65;
protected virtual float TimestampPadding => default_timestamp_padding;
private const float default_horizontal_padding = 15;
@ -94,7 +96,7 @@ namespace osu.Game.Overlays.Chat
Font = OsuFont.GetFont(size: TextSize, weight: FontWeight.Bold, italics: true),
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
MaxWidth = default_message_padding - timestamp_padding
MaxWidth = MessagePadding - TimestampPadding
};
if (hasBackground)
@ -149,7 +151,6 @@ namespace osu.Game.Overlays.Chat
new MessageSender(message.Sender)
{
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = timestamp_padding },
Origin = Anchor.TopRight,
Anchor = Anchor.TopRight,
Child = effectedUsername,

View File

@ -119,7 +119,6 @@ namespace osu.Game.Overlays.Chat.Selection
{
RelativeSizeAxes = Axes.X,
PlaceholderText = @"Search",
Exit = Hide,
},
},
},

View File

@ -138,7 +138,6 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both,
Height = 1,
PlaceholderText = "type your message",
Exit = Hide,
OnCommit = postMessage,
ReleaseFocusOnCommit = false,
HoldFocus = true,

View File

@ -31,7 +31,6 @@ namespace osu.Game.Overlays.Music
{
RelativeSizeAxes = Axes.X,
Height = 40,
Exit = () => ExitRequested?.Invoke(),
},
new CollectionsDropdown<PlaylistCollection>
{
@ -47,8 +46,6 @@ namespace osu.Game.Overlays.Music
private void current_ValueChanged(ValueChangedEvent<string> e) => FilterChanged?.Invoke(e.NewValue);
public Action ExitRequested;
public Action<string> FilterChanged;
public class FilterTextBox : SearchTextBox

View File

@ -63,7 +63,6 @@ namespace osu.Game.Overlays.Music
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
ExitRequested = Hide,
FilterChanged = search => list.Filter(search),
Padding = new MarginPadding(10),
},

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

@ -88,8 +88,6 @@ namespace osu.Game.Overlays.SearchableList
},
},
};
Filter.Search.Exit = Hide;
}
protected override void Update()

View File

@ -27,16 +27,16 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
LabelText = "Parallax",
Bindable = config.GetBindable<bool>(OsuSetting.MenuParallax)
},
new SettingsSlider<int, TimeSlider>
new SettingsSlider<float, TimeSlider>
{
LabelText = "Hold-to-confirm activation time",
Bindable = config.GetBindable<int>(OsuSetting.UIHoldActivationDelay),
Bindable = config.GetBindable<float>(OsuSetting.UIHoldActivationDelay),
KeyboardStep = 50
},
};
}
private class TimeSlider : OsuSliderBar<int>
private class TimeSlider : OsuSliderBar<float>
{
public override string TooltipText => Current.Value.ToString("N0") + "ms";
}

View File

@ -91,7 +91,6 @@ namespace osu.Game.Overlays
Top = 20,
Bottom = 20
},
Exit = Hide,
},
Footer = CreateFooter()
},

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

@ -132,6 +132,7 @@ namespace osu.Game.Rulesets.Edit
editorBeatmap = new EditorBeatmap<TObject>(playableBeatmap);
editorBeatmap.HitObjectAdded += addHitObject;
editorBeatmap.HitObjectRemoved += removeHitObject;
editorBeatmap.StartTimeChanged += updateHitObject;
var dependencies = new DependencyContainer(parent);
dependencies.CacheAs<IEditorBeatmap>(editorBeatmap);
@ -162,12 +163,7 @@ namespace osu.Game.Rulesets.Edit
});
}
private void addHitObject(HitObject hitObject)
{
beatmapProcessor?.PreProcess();
hitObject.ApplyDefaults(playableBeatmap.ControlPointInfo, playableBeatmap.BeatmapInfo.BaseDifficulty);
beatmapProcessor?.PostProcess();
}
private void addHitObject(HitObject hitObject) => updateHitObject(hitObject);
private void removeHitObject(HitObject hitObject)
{
@ -175,6 +171,13 @@ namespace osu.Game.Rulesets.Edit
beatmapProcessor?.PostProcess();
}
private void updateHitObject(HitObject hitObject)
{
beatmapProcessor?.PreProcess();
hitObject.ApplyDefaults(playableBeatmap.ControlPointInfo, playableBeatmap.BeatmapInfo.BaseDifficulty);
beatmapProcessor?.PostProcess();
}
public override IEnumerable<DrawableHitObject> HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects;
public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position);

View File

@ -108,6 +108,12 @@ namespace osu.Game.Rulesets.Edit
placementHandler.EndPlacement(HitObject);
}
/// <summary>
/// Updates the position of this <see cref="PlacementBlueprint"/> to a new screen-space position.
/// </summary>
/// <param name="screenSpacePosition">The screen-space position.</param>
public abstract void UpdatePosition(Vector2 screenSpacePosition);
/// <summary>
/// Invokes <see cref="Objects.HitObject.ApplyDefaults(ControlPointInfo,BeatmapDifficulty)"/>,
/// refreshing <see cref="Objects.HitObject.NestedHitObjects"/> and parameters for the <see cref="HitObject"/>.
@ -125,7 +131,7 @@ namespace osu.Game.Rulesets.Edit
case ScrollEvent _:
return false;
case MouseEvent _:
case MouseButtonEvent _:
return true;
default:

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@ -27,10 +28,16 @@ namespace osu.Game.Rulesets.Objects
/// </summary>
private const double control_point_leniency = 1;
public readonly Bindable<double> StartTimeBindable = new Bindable<double>();
/// <summary>
/// The time at which the HitObject starts.
/// </summary>
public virtual double StartTime { get; set; }
public virtual double StartTime
{
get => StartTimeBindable.Value;
set => StartTimeBindable.Value = value;
}
private List<HitSampleInfo> samples;
@ -67,6 +74,17 @@ namespace osu.Game.Rulesets.Objects
[JsonIgnore]
public IReadOnlyList<HitObject> NestedHitObjects => nestedHitObjects;
public HitObject()
{
StartTimeBindable.ValueChanged += time =>
{
double offset = time.NewValue - time.OldValue;
foreach (var nested in NestedHitObjects)
nested.StartTime += offset;
};
}
/// <summary>
/// Applies default values to this HitObject.
/// </summary>

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

@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Input.States;
using osu.Game.Rulesets.Edit;
@ -22,8 +23,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
private Container<PlacementBlueprint> placementBlueprintContainer;
private PlacementBlueprint currentPlacement;
private SelectionHandler selectionHandler;
private InputManager inputManager;
private IEnumerable<SelectionBlueprint> selections => selectionBlueprints.Children.Where(c => c.IsAlive);
@ -66,6 +67,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
beatmap.HitObjectAdded += addBlueprintFor;
beatmap.HitObjectRemoved += removeBlueprintFor;
inputManager = GetContainingInputManager();
}
private HitObjectCompositionTool currentTool;
@ -136,6 +139,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
return true;
}
protected override bool OnMouseMove(MouseMoveEvent e)
{
if (currentPlacement != null)
{
currentPlacement.UpdatePosition(e.ScreenSpaceMousePosition);
return true;
}
return base.OnMouseMove(e);
}
protected override void Update()
{
base.Update();
@ -158,8 +172,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
currentPlacement = null;
var blueprint = CurrentTool?.CreatePlacementBlueprint();
if (blueprint != null)
{
placementBlueprintContainer.Child = currentPlacement = blueprint;
// Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame
blueprint.UpdatePosition(inputManager.CurrentState.Mouse.Position);
}
}
/// <summary>

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
@ -13,14 +14,30 @@ namespace osu.Game.Screens.Edit
public class EditorBeatmap<T> : IEditorBeatmap<T>
where T : HitObject
{
/// <summary>
/// Invoked when a <see cref="HitObject"/> is added to this <see cref="EditorBeatmap{T}"/>.
/// </summary>
public event Action<HitObject> HitObjectAdded;
/// <summary>
/// Invoked when a <see cref="HitObject"/> is removed from this <see cref="EditorBeatmap{T}"/>.
/// </summary>
public event Action<HitObject> HitObjectRemoved;
/// <summary>
/// Invoked when the start time of a <see cref="HitObject"/> in this <see cref="EditorBeatmap{T}"/> was changed.
/// </summary>
public event Action<HitObject> StartTimeChanged;
private readonly Dictionary<T, Bindable<double>> startTimeBindables = new Dictionary<T, Bindable<double>>();
private readonly Beatmap<T> beatmap;
public EditorBeatmap(Beatmap<T> beatmap)
{
this.beatmap = beatmap;
foreach (var obj in HitObjects)
trackStartTime(obj);
}
public BeatmapInfo BeatmapInfo
@ -37,7 +54,7 @@ namespace osu.Game.Screens.Edit
public double TotalBreakTime => beatmap.TotalBreakTime;
IReadOnlyList<T> IBeatmap<T>.HitObjects => beatmap.HitObjects;
public IReadOnlyList<T> HitObjects => beatmap.HitObjects;
IReadOnlyList<HitObject> IBeatmap.HitObjects => beatmap.HitObjects;
@ -51,6 +68,8 @@ namespace osu.Game.Screens.Edit
/// <param name="hitObject">The <see cref="HitObject"/> to add.</param>
public void Add(T hitObject)
{
trackStartTime(hitObject);
// Preserve existing sorting order in the beatmap
var insertionIndex = beatmap.HitObjects.FindLastIndex(h => h.StartTime <= hitObject.StartTime);
beatmap.HitObjects.Insert(insertionIndex + 1, hitObject);
@ -65,7 +84,28 @@ namespace osu.Game.Screens.Edit
public void Remove(T hitObject)
{
if (beatmap.HitObjects.Remove(hitObject))
{
var bindable = startTimeBindables[hitObject];
bindable.UnbindAll();
startTimeBindables.Remove(hitObject);
HitObjectRemoved?.Invoke(hitObject);
}
}
private void trackStartTime(T hitObject)
{
startTimeBindables[hitObject] = hitObject.StartTimeBindable.GetBoundCopy();
startTimeBindables[hitObject].ValueChanged += _ =>
{
// For now we'll remove and re-add the hitobject. This is not optimal and can be improved if required.
beatmap.HitObjects.Remove(hitObject);
var insertionIndex = beatmap.HitObjects.FindLastIndex(h => h.StartTime <= hitObject.StartTime);
beatmap.HitObjects.Insert(insertionIndex + 1, hitObject);
StartTimeChanged?.Invoke(hitObject);
};
}
/// <summary>

View File

@ -62,7 +62,7 @@ namespace osu.Game.Screens.Menu
protected override BackgroundScreen CreateBackground() => background;
private Bindable<int> holdDelay;
private Bindable<float> holdDelay;
private Bindable<bool> loginDisplayed;
private ExitConfirmOverlay exitConfirmOverlay;
@ -70,7 +70,7 @@ namespace osu.Game.Screens.Menu
[BackgroundDependencyLoader(true)]
private void load(DirectOverlay direct, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics)
{
holdDelay = config.GetBindable<int>(OsuSetting.UIHoldActivationDelay);
holdDelay = config.GetBindable<float>(OsuSetting.UIHoldActivationDelay);
loginDisplayed = statics.GetBindable<bool>(Static.LoginOverlayDisplayed);
if (host.CanExit)

View File

@ -69,8 +69,6 @@ namespace osu.Game.Screens.Multi.Lounge
},
},
};
Filter.Search.Exit += this.Exit;
}
protected override void UpdateAfterChildren()

View File

@ -62,7 +62,6 @@ namespace osu.Game.Screens.Multi.Match
[BackgroundDependencyLoader]
private void load()
{
MatchChatDisplay chat;
Components.Header header;
Info info;
GridContainer bottomRow;
@ -122,7 +121,7 @@ namespace osu.Game.Screens.Multi.Match
Vertical = 10,
},
RelativeSizeAxes = Axes.Both,
Child = chat = new MatchChatDisplay
Child = new MatchChatDisplay
{
RelativeSizeAxes = Axes.Both
}
@ -159,12 +158,6 @@ namespace osu.Game.Screens.Multi.Match
bottomRow.FadeTo(settingsDisplayed ? 0 : 1, fade_duration, Easing.OutQuint);
}, true);
chat.Exit += () =>
{
if (this.IsCurrentScreen())
this.Exit();
};
beatmapManager.ItemAdded += beatmapAdded;
}

View File

@ -63,11 +63,11 @@ namespace osu.Game.Screens.Play.HUD
[Resolved]
private OsuConfigManager config { get; set; }
private Bindable<int> activationDelay;
private Bindable<float> activationDelay;
protected override void LoadComplete()
{
activationDelay = config.GetBindable<int>(OsuSetting.UIHoldActivationDelay);
activationDelay = config.GetBindable<float>(OsuSetting.UIHoldActivationDelay);
activationDelay.BindValueChanged(v =>
{
text.Text = v.NewValue > 0

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

@ -49,8 +49,6 @@ namespace osu.Game.Screens.Select
return criteria;
}
public Action Exit;
private readonly SearchTextBox searchTextBox;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
@ -75,11 +73,7 @@ namespace osu.Game.Screens.Select
Origin = Anchor.TopRight,
Children = new Drawable[]
{
searchTextBox = new SearchTextBox
{
RelativeSizeAxes = Axes.X,
Exit = () => Exit?.Invoke(),
},
searchTextBox = new SearchTextBox { RelativeSizeAxes = Axes.X },
new Box
{
RelativeSizeAxes = Axes.X,

View File

@ -89,11 +89,7 @@ namespace osu.Game.Screens.Select.Options
/// <param name="icon">Icon of the button.</param>
/// <param name="hotkey">Hotkey of the button.</param>
/// <param name="action">Binding the button does.</param>
/// <param name="depth">
/// <para>Lower depth to be put on the left, and higher to be put on the right.</para>
/// <para>Notice this is different to <see cref="Footer"/>!</para>
/// </param>
public void AddButton(string firstLine, string secondLine, IconUsage icon, Color4 colour, Action action, Key? hotkey = null, float depth = 0)
public void AddButton(string firstLine, string secondLine, IconUsage icon, Color4 colour, Action action, Key? hotkey = null)
{
var button = new BeatmapOptionsButton
{
@ -101,7 +97,6 @@ namespace osu.Game.Screens.Select.Options
SecondLineText = secondLine,
Icon = icon,
ButtonColour = colour,
Depth = depth,
Action = () =>
{
Hide();
@ -110,7 +105,7 @@ namespace osu.Game.Screens.Select.Options
HotKey = hotkey
};
buttonsContainer.Insert((int)depth, button);
buttonsContainer.Add(button);
}
}
}

View File

@ -28,7 +28,7 @@ namespace osu.Game.Screens.Select
{
ValidForResume = false;
Edit();
}, Key.Number3);
}, Key.Number4);
}
public override void OnResuming(IScreen last)

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,
@ -171,18 +171,13 @@ namespace osu.Game.Screens.Select
Height = FilterControl.HEIGHT,
FilterChanged = c => Carousel.Filter(c),
Background = { Width = 2 },
Exit = () =>
{
if (this.IsCurrentScreen())
this.Exit();
},
},
}
},
},
beatmapInfoWedge = new BeatmapInfoWedge
{
Size = wedged_container_size,
Size = WEDGED_CONTAINER_SIZE,
RelativeSizeAxes = Axes.X,
Margin = new MarginPadding
{
@ -235,9 +230,9 @@ namespace osu.Game.Screens.Select
Footer.AddButton(new FooterButtonRandom { Action = triggerRandom });
Footer.AddButton(new FooterButtonOptions(), BeatmapOptions);
BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo), Key.Number4, float.MaxValue);
BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null, Key.Number1);
BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => clearScores(Beatmap.Value.BeatmapInfo), Key.Number2);
BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo), Key.Number3);
}
if (this.beatmaps == null)
@ -418,7 +413,7 @@ namespace osu.Game.Screens.Select
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, previous);
if (this.IsCurrentScreen() && Beatmap.Value?.Track != previous?.Track)
ensurePlayingSelected();
ensurePlayingSelected(true);
if (beatmap != null)
{
@ -590,18 +585,14 @@ namespace osu.Game.Screens.Select
{
Track track = Beatmap.Value.Track;
if (!track.IsRunning || restart)
if (!track.IsRunning)
{
track.RestartPoint = Beatmap.Value.Metadata.PreviewTime;
if (music != null)
{
// use the global music controller (when available) to cancel a potential local user paused state.
music.SeekTo(track.RestartPoint);
music.Play();
}
else
if (restart)
track.Restart();
else
track.Start();
}
}

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);
}

View File

@ -28,9 +28,9 @@ namespace osu.Game.Tests.Visual
{
[Cached(typeof(Bindable<WorkingBeatmap>))]
[Cached(typeof(IBindable<WorkingBeatmap>))]
private OsuTestBeatmap beatmap;
private NonNullableBindable<WorkingBeatmap> beatmap;
protected BindableBeatmap Beatmap => beatmap;
protected Bindable<WorkingBeatmap> Beatmap => beatmap;
[Cached]
[Cached(typeof(IBindable<RulesetInfo>))]
@ -73,10 +73,13 @@ namespace osu.Game.Tests.Visual
// This is the earliest we can get OsuGameBase, which is used by the dummy working beatmap to find textures
var working = new DummyWorkingBeatmap(parent.Get<AudioManager>(), parent.Get<TextureStore>());
beatmap = new OsuTestBeatmap(working)
beatmap = new NonNullableBindable<WorkingBeatmap>(working) { Default = working };
beatmap.BindValueChanged(b => ScheduleAfterChildren(() =>
{
Default = working
};
// compare to last beatmap as sometimes the two may share a track representation (optimisation, see WorkingBeatmap.TransferTo)
if (b.OldValue?.TrackLoaded == true && b.OldValue?.Track != b.NewValue?.Track)
b.OldValue.RecycleTrack();
}));
Dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
@ -317,13 +320,5 @@ namespace osu.Game.Tests.Visual
public void RunTestBlocking(TestScene test) => runner.RunTestBlocking(test);
}
private class OsuTestBeatmap : BindableBeatmap
{
public OsuTestBeatmap(WorkingBeatmap defaultValue)
: base(defaultValue)
{
}
}
}
}

View File

@ -4,6 +4,8 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
@ -18,16 +20,17 @@ namespace osu.Game.Tests.Visual
protected Container HitObjectContainer;
private PlacementBlueprint currentBlueprint;
private InputManager inputManager;
protected PlacementBlueprintTestScene()
{
Add(HitObjectContainer = CreateHitObjectContainer());
Add(HitObjectContainer = CreateHitObjectContainer().With(c => c.Clock = new FramedClock(new StopwatchClock())));
}
[BackgroundDependencyLoader]
private void load()
{
Beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize = 2;
Add(currentBlueprint = CreateBlueprint());
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@ -38,6 +41,14 @@ namespace osu.Game.Tests.Visual
return dependencies;
}
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
Add(currentBlueprint = CreateBlueprint());
}
public void BeginPlacement(HitObject hitObject)
{
}
@ -54,10 +65,27 @@ namespace osu.Game.Tests.Visual
{
}
protected virtual Container CreateHitObjectContainer() => new Container { RelativeSizeAxes = Axes.Both };
protected override bool OnMouseMove(MouseMoveEvent e)
{
currentBlueprint.UpdatePosition(e.ScreenSpaceMousePosition);
return true;
}
public override void Add(Drawable drawable)
{
base.Add(drawable);
if (drawable is PlacementBlueprint blueprint)
{
blueprint.Show();
blueprint.UpdatePosition(inputManager.CurrentState.Mouse.Position);
}
}
protected virtual void AddHitObject(DrawableHitObject hitObject) => HitObjectContainer.Add(hitObject);
protected virtual Container CreateHitObjectContainer() => new Container { RelativeSizeAxes = Axes.Both };
protected abstract DrawableHitObject CreateHitObject(HitObject hitObject);
protected abstract PlacementBlueprint CreateBlueprint();
}