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

Merge branch 'master' into editor-timing-screen

This commit is contained in:
Dean Herbert 2019-10-30 18:42:20 +09:00 committed by GitHub
commit 93076ad6a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 465 additions and 224 deletions

View File

@ -62,6 +62,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2019.1023.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2019.1029.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -7,7 +7,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -30,17 +29,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Children = new Drawable[] Children = new Drawable[]
{ {
new CircularContainer new Container
{ {
Masking = true, Masking = true,
Origin = Anchor.Centre,
EdgeEffect = new EdgeEffectParameters EdgeEffect = new EdgeEffectParameters
{ {
Type = EdgeEffectType.Glow, Type = EdgeEffectType.Glow,
Radius = 60, Radius = 60,
Colour = Color4.White.Opacity(0.5f), Colour = Color4.White.Opacity(0.5f),
}, },
Child = new Box()
}, },
number = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText number = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
{ {

View File

@ -262,6 +262,21 @@ namespace osu.Game.Tests.Beatmaps.Formats
} }
} }
[Test]
public void TestTimingPointResetsSpeedMultiplier()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("timingpoint-speedmultiplier-reset.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var controlPoints = decoder.Decode(stream).ControlPointInfo;
Assert.That(controlPoints.DifficultyPointAt(0).SpeedMultiplier, Is.EqualTo(0.5).Within(0.1));
Assert.That(controlPoints.DifficultyPointAt(2000).SpeedMultiplier, Is.EqualTo(1).Within(0.1));
}
}
[Test] [Test]
public void TestDecodeBeatmapColours() public void TestDecodeBeatmapColours()
{ {
@ -362,6 +377,23 @@ namespace osu.Game.Tests.Beatmaps.Formats
} }
} }
[Test]
public void TestDecodeControlPointDifficultyChange()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("controlpoint-difficulty-multiplier.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var controlPointInfo = decoder.Decode(stream).ControlPointInfo;
Assert.That(controlPointInfo.DifficultyPointAt(5).SpeedMultiplier, Is.EqualTo(1));
Assert.That(controlPointInfo.DifficultyPointAt(1000).SpeedMultiplier, Is.EqualTo(10));
Assert.That(controlPointInfo.DifficultyPointAt(2000).SpeedMultiplier, Is.EqualTo(1.8518518518518519d));
Assert.That(controlPointInfo.DifficultyPointAt(3000).SpeedMultiplier, Is.EqualTo(0.5));
}
}
[Test] [Test]
public void TestDecodeControlPointCustomSampleBank() public void TestDecodeControlPointCustomSampleBank()
{ {

View File

@ -0,0 +1,8 @@
osu file format v7
[TimingPoints]
0,100,4,2,0,100,1,0
12,500,4,2,0,100,1,0
1000,-10,4,2,0,100,0,0
2000,-54,4,2,0,100,0,0
3000,-200,4,2,0,100,0,0

View File

@ -0,0 +1,5 @@
osu file format v14
[TimingPoints]
0,-200,4,1,0,100,0,0
2000,100,1,1,0,100,1,0

View File

@ -3,6 +3,7 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Play.PlayerSettings;
@ -20,6 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
State = { Value = Visibility.Visible }
}); });
Add(container = new ExampleContainer()); Add(container = new ExampleContainer());

View File

@ -33,23 +33,15 @@ namespace osu.Game.Tests.Visual.Menus
[Test] [Test]
public void TestInstantLoad() public void TestInstantLoad()
{ {
bool logoVisible = false; // visual only, very impossible to test this using asserts.
AddStep("begin loading", () => AddStep("load immediately", () =>
{ {
loader = new TestLoader(); loader = new TestLoader();
loader.AllowLoad.Set(); loader.AllowLoad.Set();
LoadScreen(loader); LoadScreen(loader);
}); });
AddUntilStep("loaded", () =>
{
logoVisible = loader.Logo?.Alpha > 0;
return loader.Logo != null && loader.ScreenLoaded;
});
AddAssert("logo was not visible", () => !logoVisible);
} }
[Test] [Test]
@ -58,7 +50,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("begin loading", () => LoadScreen(loader = new TestLoader())); AddStep("begin loading", () => LoadScreen(loader = new TestLoader()));
AddUntilStep("wait for logo visible", () => loader.Logo?.Alpha > 0); AddUntilStep("wait for logo visible", () => loader.Logo?.Alpha > 0);
AddStep("finish loading", () => loader.AllowLoad.Set()); AddStep("finish loading", () => loader.AllowLoad.Set());
AddAssert("loaded", () => loader.Logo != null && loader.ScreenLoaded); AddUntilStep("loaded", () => loader.Logo != null && loader.ScreenLoaded);
AddUntilStep("logo gone", () => loader.Logo?.Alpha == 0); AddUntilStep("logo gone", () => loader.Logo?.Alpha == 0);
} }

View File

@ -7,6 +7,10 @@ using osu.Game.Online.Chat;
using osu.Game.Users; using osu.Game.Users;
using osuTK; using osuTK;
using System; using System;
using System.Linq;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays.Chat;
namespace osu.Game.Tests.Visual.Online namespace osu.Game.Tests.Visual.Online
{ {
@ -42,14 +46,14 @@ namespace osu.Game.Tests.Visual.Online
[Cached] [Cached]
private ChannelManager channelManager = new ChannelManager(); private ChannelManager channelManager = new ChannelManager();
private readonly StandAloneChatDisplay chatDisplay; private readonly TestStandAloneChatDisplay chatDisplay;
private readonly StandAloneChatDisplay chatDisplay2; private readonly TestStandAloneChatDisplay chatDisplay2;
public TestSceneStandAloneChatDisplay() public TestSceneStandAloneChatDisplay()
{ {
Add(channelManager); Add(channelManager);
Add(chatDisplay = new StandAloneChatDisplay Add(chatDisplay = new TestStandAloneChatDisplay
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
@ -57,7 +61,7 @@ namespace osu.Game.Tests.Visual.Online
Size = new Vector2(400, 80) Size = new Vector2(400, 80)
}); });
Add(chatDisplay2 = new StandAloneChatDisplay(true) Add(chatDisplay2 = new TestStandAloneChatDisplay(true)
{ {
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
@ -119,6 +123,49 @@ namespace osu.Game.Tests.Visual.Online
Content = "Message from the future!", Content = "Message from the future!",
Timestamp = DateTimeOffset.Now Timestamp = DateTimeOffset.Now
})); }));
AddUntilStep("ensure still scrolled to bottom", () => chatDisplay.ScrolledToBottom);
const int messages_per_call = 10;
AddRepeatStep("add many messages", () =>
{
for (int i = 0; i < messages_per_call; i++)
testChannel.AddNewMessages(new Message(sequence++)
{
Sender = longUsernameUser,
Content = "Many messages! " + Guid.NewGuid(),
Timestamp = DateTimeOffset.Now
});
}, Channel.MAX_HISTORY / messages_per_call + 5);
AddAssert("Ensure no adjacent day separators", () =>
{
var indices = chatDisplay.FillFlow.OfType<DrawableChannel.DaySeparator>().Select(ds => chatDisplay.FillFlow.IndexOf(ds));
foreach (var i in indices)
if (i < chatDisplay.FillFlow.Count && chatDisplay.FillFlow[i + 1] is DrawableChannel.DaySeparator)
return false;
return true;
});
AddUntilStep("ensure still scrolled to bottom", () => chatDisplay.ScrolledToBottom);
}
private class TestStandAloneChatDisplay : StandAloneChatDisplay
{
public TestStandAloneChatDisplay(bool textbox = false)
: base(textbox)
{
}
protected DrawableChannel DrawableChannel => InternalChildren.OfType<DrawableChannel>().First();
protected OsuScrollContainer ScrollContainer => (OsuScrollContainer)((Container)DrawableChannel.Child).Child;
public FillFlowContainer FillFlow => (FillFlowContainer)ScrollContainer.Child;
public bool ScrolledToBottom => ScrollContainer.IsScrolledToEnd(1);
} }
} }
} }

View File

@ -245,6 +245,28 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert($"Check #{set_count} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Title.EndsWith($"#{set_count}!")); AddAssert($"Check #{set_count} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Title.EndsWith($"#{set_count}!"));
} }
[Test]
public void TestSortingStability()
{
var sets = new List<BeatmapSetInfo>();
for (int i = 0; i < 20; i++)
{
var set = createTestBeatmapSet(i);
set.Metadata.Artist = "same artist";
set.Metadata.Title = "same title";
sets.Add(set);
}
loadBeatmaps(sets);
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
AddAssert("Items remain in original order", () => carousel.BeatmapSets.Select((set, index) => set.ID == index).All(b => b));
AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
AddAssert("Items remain in original order", () => carousel.BeatmapSets.Select((set, index) => set.ID == index).All(b => b));
}
[Test] [Test]
public void TestSortingWithFiltered() public void TestSortingWithFiltered()
{ {

View File

@ -3,7 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Internal; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
@ -153,7 +153,7 @@ namespace osu.Game.Tests.Visual.UserInterface
}; };
} }
private IReadOnlyList<TimingControlPoint> timingPoints => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints; private List<TimingControlPoint> timingPoints => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ToList();
private TimingControlPoint getNextTimingPoint(TimingControlPoint current) private TimingControlPoint getNextTimingPoint(TimingControlPoint current)
{ {

View File

@ -11,7 +11,7 @@ using osuTK.Graphics;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
{ {
public class TestSceneLabelledComponent : OsuTestScene public class TestSceneLabelledDrawable : OsuTestScene
{ {
[TestCase(false)] [TestCase(false)]
[TestCase(true)] [TestCase(true)]
@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
AddStep("create component", () => AddStep("create component", () =>
{ {
LabelledComponent<Drawable> component; LabelledDrawable<Drawable> component;
Child = new Container Child = new Container
{ {
@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Origin = Anchor.Centre, Origin = Anchor.Centre,
Width = 500, Width = 500,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Child = component = padded ? (LabelledComponent<Drawable>)new PaddedLabelledComponent() : new NonPaddedLabelledComponent(), Child = component = padded ? (LabelledDrawable<Drawable>)new PaddedLabelledDrawable() : new NonPaddedLabelledDrawable(),
}; };
component.Label = "a sample component"; component.Label = "a sample component";
@ -41,9 +41,9 @@ namespace osu.Game.Tests.Visual.UserInterface
}); });
} }
private class PaddedLabelledComponent : LabelledComponent<Drawable> private class PaddedLabelledDrawable : LabelledDrawable<Drawable>
{ {
public PaddedLabelledComponent() public PaddedLabelledDrawable()
: base(true) : base(true)
{ {
} }
@ -57,9 +57,9 @@ namespace osu.Game.Tests.Visual.UserInterface
}; };
} }
private class NonPaddedLabelledComponent : LabelledComponent<Drawable> private class NonPaddedLabelledDrawable : LabelledDrawable<Drawable>
{ {
public NonPaddedLabelledComponent() public NonPaddedLabelledDrawable()
: base(false) : base(false)
{ {
} }

View File

@ -7,7 +7,6 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
@ -28,7 +27,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
AddStep("create component", () => AddStep("create component", () =>
{ {
LabelledComponent<OsuTextBox> component; LabelledTextBox component;
Child = new Container Child = new Container
{ {

View File

@ -89,7 +89,7 @@ namespace osu.Game.Tournament.Screens
}; };
} }
private class ActionableInfo : LabelledComponent<Drawable> private class ActionableInfo : LabelledDrawable<Drawable>
{ {
private OsuButton button; private OsuButton button;

View File

@ -1,24 +1,33 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osuTK; using osu.Framework.Bindables;
namespace osu.Game.Beatmaps.ControlPoints namespace osu.Game.Beatmaps.ControlPoints
{ {
public class DifficultyControlPoint : ControlPoint public class DifficultyControlPoint : ControlPoint
{ {
/// <summary>
/// The speed multiplier at this control point.
/// </summary>
public readonly BindableDouble SpeedMultiplierBindable = new BindableDouble(1)
{
Precision = 0.1,
Default = 1,
MinValue = 0.1,
MaxValue = 10
};
/// <summary> /// <summary>
/// The speed multiplier at this control point. /// The speed multiplier at this control point.
/// </summary> /// </summary>
public double SpeedMultiplier public double SpeedMultiplier
{ {
get => speedMultiplier; get => SpeedMultiplierBindable.Value;
set => speedMultiplier = MathHelper.Clamp(value, 0.1, 10); set => SpeedMultiplierBindable.Value = value;
} }
private double speedMultiplier = 1;
public override bool EquivalentTo(ControlPoint other) => public override bool EquivalentTo(ControlPoint other) =>
other is DifficultyControlPoint otherTyped && otherTyped.SpeedMultiplier.Equals(speedMultiplier); other is DifficultyControlPoint otherTyped && otherTyped.SpeedMultiplier.Equals(SpeedMultiplier);
} }
} }

View File

@ -1,19 +1,39 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
namespace osu.Game.Beatmaps.ControlPoints namespace osu.Game.Beatmaps.ControlPoints
{ {
public class EffectControlPoint : ControlPoint public class EffectControlPoint : ControlPoint
{ {
/// <summary> /// <summary>
/// Whether this control point enables Kiai mode. /// Whether the first bar line of this control point is ignored.
/// </summary> /// </summary>
public bool KiaiMode; public readonly BindableBool OmitFirstBarLineBindable = new BindableBool();
/// <summary> /// <summary>
/// Whether the first bar line of this control point is ignored. /// Whether the first bar line of this control point is ignored.
/// </summary> /// </summary>
public bool OmitFirstBarLine; public bool OmitFirstBarLine
{
get => OmitFirstBarLineBindable.Value;
set => OmitFirstBarLineBindable.Value = value;
}
/// <summary>
/// Whether this control point enables Kiai mode.
/// </summary>
public readonly BindableBool KiaiModeBindable = new BindableBool();
/// <summary>
/// Whether this control point enables Kiai mode.
/// </summary>
public bool KiaiMode
{
get => KiaiModeBindable.Value;
set => KiaiModeBindable.Value = value;
}
public override bool EquivalentTo(ControlPoint other) => public override bool EquivalentTo(ControlPoint other) =>
other is EffectControlPoint otherTyped && other is EffectControlPoint otherTyped &&

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Game.Audio; using osu.Game.Audio;
namespace osu.Game.Beatmaps.ControlPoints namespace osu.Game.Beatmaps.ControlPoints
@ -12,12 +13,35 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <summary> /// <summary>
/// The default sample bank at this control point. /// The default sample bank at this control point.
/// </summary> /// </summary>
public string SampleBank = DEFAULT_BANK; public readonly Bindable<string> SampleBankBindable = new Bindable<string>(DEFAULT_BANK) { Default = DEFAULT_BANK };
/// <summary>
/// The speed multiplier at this control point.
/// </summary>
public string SampleBank
{
get => SampleBankBindable.Value;
set => SampleBankBindable.Value = value;
}
/// <summary>
/// The default sample bank at this control point.
/// </summary>
public readonly BindableInt SampleVolumeBindable = new BindableInt(100)
{
MinValue = 0,
MaxValue = 100,
Default = 100
};
/// <summary> /// <summary>
/// The default sample volume at this control point. /// The default sample volume at this control point.
/// </summary> /// </summary>
public int SampleVolume = 100; public int SampleVolume
{
get => SampleVolumeBindable.Value;
set => SampleVolumeBindable.Value = value;
}
/// <summary> /// <summary>
/// Create a SampleInfo based on the sample settings in this control point. /// Create a SampleInfo based on the sample settings in this control point.

View File

@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osuTK; using osu.Framework.Bindables;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
namespace osu.Game.Beatmaps.ControlPoints namespace osu.Game.Beatmaps.ControlPoints
@ -11,17 +11,35 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <summary> /// <summary>
/// The time signature at this control point. /// The time signature at this control point.
/// </summary> /// </summary>
public TimeSignatures TimeSignature = TimeSignatures.SimpleQuadruple; public readonly Bindable<TimeSignatures> TimeSignatureBindable = new Bindable<TimeSignatures>(TimeSignatures.SimpleQuadruple) { Default = TimeSignatures.SimpleQuadruple };
/// <summary>
/// The time signature at this control point.
/// </summary>
public TimeSignatures TimeSignature
{
get => TimeSignatureBindable.Value;
set => TimeSignatureBindable.Value = value;
}
public const double DEFAULT_BEAT_LENGTH = 1000; public const double DEFAULT_BEAT_LENGTH = 1000;
/// <summary> /// <summary>
/// The beat length at this control point. /// The beat length at this control point.
/// </summary> /// </summary>
public virtual double BeatLength public readonly BindableDouble BeatLengthBindable = new BindableDouble(DEFAULT_BEAT_LENGTH)
{ {
get => beatLength; Default = DEFAULT_BEAT_LENGTH,
set => beatLength = MathHelper.Clamp(value, 6, 60000); MinValue = 6,
MaxValue = 60000
};
/// The beat length at this control point.
/// </summary>
public double BeatLength
{
get => BeatLengthBindable.Value;
set => BeatLengthBindable.Value = value;
} }
/// <summary> /// <summary>
@ -29,10 +47,8 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary> /// </summary>
public double BPM => 60000 / BeatLength; public double BPM => 60000 / BeatLength;
private double beatLength = DEFAULT_BEAT_LENGTH;
public override bool EquivalentTo(ControlPoint other) => public override bool EquivalentTo(ControlPoint other) =>
other is TimingControlPoint otherTyped other is TimingControlPoint otherTyped
&& TimeSignature == otherTyped.TimeSignature && beatLength.Equals(otherTyped.beatLength); && TimeSignature == otherTyped.TimeSignature && BeatLength.Equals(otherTyped.BeatLength);
} }
} }

View File

@ -379,7 +379,7 @@ namespace osu.Game.Beatmaps.Formats
addControlPoint(time, controlPoint, true); addControlPoint(time, controlPoint, true);
} }
addControlPoint(time, new DifficultyControlPoint addControlPoint(time, new LegacyDifficultyControlPoint
{ {
SpeedMultiplier = speedMultiplier, SpeedMultiplier = speedMultiplier,
}, timingChange); }, timingChange);
@ -411,15 +411,15 @@ namespace osu.Game.Beatmaps.Formats
private void addControlPoint(double time, ControlPoint point, bool timingChange) private void addControlPoint(double time, ControlPoint point, bool timingChange)
{ {
if (time != pendingControlPointsTime)
flushPendingPoints();
if (timingChange) if (timingChange)
{ {
beatmap.ControlPointInfo.Add(time, point); beatmap.ControlPointInfo.Add(time, point);
return; return;
} }
if (time != pendingControlPointsTime)
flushPendingPoints();
pendingControlPoints.Add(point); pendingControlPoints.Add(point);
pendingControlPointsTime = time; pendingControlPointsTime = time;
} }

View File

@ -189,6 +189,14 @@ namespace osu.Game.Beatmaps.Formats
Foreground = 3 Foreground = 3
} }
internal class LegacyDifficultyControlPoint : DifficultyControlPoint
{
public LegacyDifficultyControlPoint()
{
SpeedMultiplierBindable.Precision = double.Epsilon;
}
}
internal class LegacySampleControlPoint : SampleControlPoint internal class LegacySampleControlPoint : SampleControlPoint
{ {
public int CustomSampleBank; public int CustomSampleBank;

View File

@ -28,11 +28,15 @@ namespace osu.Game.Beatmaps.Formats
} }
protected override TimingControlPoint CreateTimingControlPoint() protected override TimingControlPoint CreateTimingControlPoint()
=> new LegacyDifficultyCalculatorControlPoint(); => new LegacyDifficultyCalculatorTimingControlPoint();
private class LegacyDifficultyCalculatorControlPoint : TimingControlPoint private class LegacyDifficultyCalculatorTimingControlPoint : TimingControlPoint
{ {
public override double BeatLength { get; set; } = DEFAULT_BEAT_LENGTH; public LegacyDifficultyCalculatorTimingControlPoint()
{
BeatLengthBindable.MinValue = double.MinValue;
BeatLengthBindable.MaxValue = double.MaxValue;
}
} }
} }
} }

View File

@ -108,7 +108,7 @@ namespace osu.Game.Database
return Import(notification, paths); return Import(notification, paths);
} }
protected async Task Import(ProgressNotification notification, params string[] paths) protected async Task<IEnumerable<TModel>> Import(ProgressNotification notification, params string[] paths)
{ {
notification.Progress = 0; notification.Progress = 0;
notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import is initialising..."; notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import is initialising...";
@ -168,6 +168,8 @@ namespace osu.Game.Database
notification.State = ProgressNotificationState.Completed; notification.State = ProgressNotificationState.Completed;
} }
return imported;
} }
/// <summary> /// <summary>

View File

@ -76,7 +76,12 @@ namespace osu.Game.Database
Task.Factory.StartNew(async () => Task.Factory.StartNew(async () =>
{ {
// This gets scheduled back to the update thread, but we want the import to run in the background. // This gets scheduled back to the update thread, but we want the import to run in the background.
await Import(notification, filename); var imported = await Import(notification, filename);
// for now a failed import will be marked as a failed download for simplicity.
if (!imported.Any())
DownloadFailed?.Invoke(request);
currentDownloads.Remove(request); currentDownloads.Remove(request);
}, TaskCreationOptions.LongRunning); }, TaskCreationOptions.LongRunning);
}; };

View File

@ -166,19 +166,6 @@ namespace osu.Game.Database
// no-op. called by tooling. // no-op. called by tooling.
} }
private class OsuDbLoggerProvider : ILoggerProvider
{
#region Disposal
public void Dispose()
{
}
#endregion
public ILogger CreateLogger(string categoryName) => new OsuDbLogger();
}
private class OsuDbLogger : ILogger private class OsuDbLogger : ILogger
{ {
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)

View File

@ -59,7 +59,7 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
hover.FadeIn(200); hover.FadeIn(200);
return base.OnHover(e); return true;
} }
protected override void OnHoverLost(HoverLostEvent e) protected override void OnHoverLost(HoverLostEvent e)

View File

@ -1,132 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers;
using osuTK;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
{ {
public abstract class LabelledComponent<T> : CompositeDrawable public abstract class LabelledComponent<T, U> : LabelledDrawable<T>, IHasCurrentValue<U>
where T : Drawable where T : Drawable, IHasCurrentValue<U>
{ {
protected const float CONTENT_PADDING_VERTICAL = 10;
protected const float CONTENT_PADDING_HORIZONTAL = 15;
protected const float CORNER_RADIUS = 15;
/// <summary>
/// The component that is being displayed.
/// </summary>
protected readonly T Component;
private readonly OsuTextFlowContainer labelText;
private readonly OsuTextFlowContainer descriptionText;
/// <summary>
/// Creates a new <see cref="LabelledComponent{T}"/>.
/// </summary>
/// <param name="padded">Whether the component should be padded or should be expanded to the bounds of this <see cref="LabelledComponent{T}"/>.</param>
protected LabelledComponent(bool padded) protected LabelledComponent(bool padded)
: base(padded)
{ {
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
CornerRadius = CORNER_RADIUS;
Masking = true;
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.FromHex("1c2125"),
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Padding = padded
? new MarginPadding { Horizontal = CONTENT_PADDING_HORIZONTAL, Vertical = CONTENT_PADDING_VERTICAL }
: new MarginPadding { Left = CONTENT_PADDING_HORIZONTAL },
Spacing = new Vector2(0, 12),
Children = new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Content = new[]
{
new Drawable[]
{
labelText = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Bold))
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = 20 }
},
new Container
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = Component = CreateComponent().With(d =>
{
d.Anchor = Anchor.CentreRight;
d.Origin = Anchor.CentreRight;
})
}
},
},
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }
},
descriptionText = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold, italics: true))
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Bottom = padded ? 0 : CONTENT_PADDING_VERTICAL },
Alpha = 0,
}
}
}
};
} }
[BackgroundDependencyLoader] public Bindable<U> Current
private void load(OsuColour osuColour)
{ {
descriptionText.Colour = osuColour.Yellow; get => Component.Current;
set => Component.Current = value;
} }
public string Label
{
set => labelText.Text = value;
}
public string Description
{
set
{
descriptionText.Text = value;
if (!string.IsNullOrEmpty(value))
descriptionText.Show();
else
descriptionText.Hide();
}
}
/// <summary>
/// Creates the component that should be displayed.
/// </summary>
/// <returns>The component.</returns>
protected abstract T CreateComponent();
} }
} }

View File

@ -0,0 +1,132 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers;
using osuTK;
namespace osu.Game.Graphics.UserInterfaceV2
{
public abstract class LabelledDrawable<T> : CompositeDrawable
where T : Drawable
{
protected const float CONTENT_PADDING_VERTICAL = 10;
protected const float CONTENT_PADDING_HORIZONTAL = 15;
protected const float CORNER_RADIUS = 15;
/// <summary>
/// The component that is being displayed.
/// </summary>
protected readonly T Component;
private readonly OsuTextFlowContainer labelText;
private readonly OsuTextFlowContainer descriptionText;
/// <summary>
/// Creates a new <see cref="LabelledComponent{T, U}"/>.
/// </summary>
/// <param name="padded">Whether the component should be padded or should be expanded to the bounds of this <see cref="LabelledComponent{T, U}"/>.</param>
protected LabelledDrawable(bool padded)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
CornerRadius = CORNER_RADIUS;
Masking = true;
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.FromHex("1c2125"),
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Padding = padded
? new MarginPadding { Horizontal = CONTENT_PADDING_HORIZONTAL, Vertical = CONTENT_PADDING_VERTICAL }
: new MarginPadding { Left = CONTENT_PADDING_HORIZONTAL },
Spacing = new Vector2(0, 12),
Children = new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Content = new[]
{
new Drawable[]
{
labelText = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(weight: FontWeight.Bold))
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = 20 }
},
new Container
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = Component = CreateComponent().With(d =>
{
d.Anchor = Anchor.CentreRight;
d.Origin = Anchor.CentreRight;
})
}
},
},
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }
},
descriptionText = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold, italics: true))
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Bottom = padded ? 0 : CONTENT_PADDING_VERTICAL },
Alpha = 0,
}
}
}
};
}
[BackgroundDependencyLoader]
private void load(OsuColour osuColour)
{
descriptionText.Colour = osuColour.Yellow;
}
public string Label
{
set => labelText.Text = value;
}
public string Description
{
set
{
descriptionText.Text = value;
if (!string.IsNullOrEmpty(value))
descriptionText.Show();
else
descriptionText.Hide();
}
}
/// <summary>
/// Creates the component that should be displayed.
/// </summary>
/// <returns>The component.</returns>
protected abstract T CreateComponent();
}
}

View File

@ -3,7 +3,7 @@
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
{ {
public class LabelledSwitchButton : LabelledComponent<SwitchButton> public class LabelledSwitchButton : LabelledComponent<SwitchButton, bool>
{ {
public LabelledSwitchButton() public LabelledSwitchButton()
: base(true) : base(true)

View File

@ -8,7 +8,7 @@ using osu.Game.Graphics.UserInterface;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
{ {
public class LabelledTextBox : LabelledComponent<OsuTextBox> public class LabelledTextBox : LabelledComponent<OsuTextBox, string>
{ {
public event TextBox.OnCommitHandler OnCommit; public event TextBox.OnCommitHandler OnCommit;

View File

@ -14,7 +14,7 @@ namespace osu.Game.Online.Chat
{ {
public class Channel public class Channel
{ {
public readonly int MaxHistory = 300; public const int MAX_HISTORY = 300;
/// <summary> /// <summary>
/// Contains every joined user except the current logged in user. Currently only returned for PM channels. /// Contains every joined user except the current logged in user. Currently only returned for PM channels.
@ -80,8 +80,6 @@ namespace osu.Game.Online.Chat
/// </summary> /// </summary>
public Bindable<bool> Joined = new Bindable<bool>(); public Bindable<bool> Joined = new Bindable<bool>();
public const int MAX_HISTORY = 300;
[JsonConstructor] [JsonConstructor]
public Channel() public Channel()
{ {
@ -162,8 +160,8 @@ namespace osu.Game.Online.Chat
{ {
// never purge local echos // never purge local echos
int messageCount = Messages.Count - pendingMessages.Count; int messageCount = Messages.Count - pendingMessages.Count;
if (messageCount > MaxHistory) if (messageCount > MAX_HISTORY)
Messages.RemoveRange(0, messageCount - MaxHistory); Messages.RemoveRange(0, messageCount - MAX_HISTORY);
} }
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
@ -89,8 +89,10 @@ namespace osu.Game.Overlays.Chat
private void newMessagesArrived(IEnumerable<Message> newMessages) private void newMessagesArrived(IEnumerable<Message> newMessages)
{ {
bool shouldScrollToEnd = scroll.IsScrolledToEnd(10) || !chatLines.Any() || newMessages.Any(m => m is LocalMessage);
// Add up to last Channel.MAX_HISTORY messages // Add up to last Channel.MAX_HISTORY messages
var displayMessages = newMessages.Skip(Math.Max(0, newMessages.Count() - Channel.MaxHistory)); var displayMessages = newMessages.Skip(Math.Max(0, newMessages.Count() - Channel.MAX_HISTORY));
Message lastMessage = chatLines.LastOrDefault()?.Message; Message lastMessage = chatLines.LastOrDefault()?.Message;
@ -103,19 +105,32 @@ namespace osu.Game.Overlays.Chat
lastMessage = message; lastMessage = message;
} }
if (scroll.IsScrolledToEnd(10) || !chatLines.Any() || newMessages.Any(m => m is LocalMessage))
scrollToEnd();
var staleMessages = chatLines.Where(c => c.LifetimeEnd == double.MaxValue).ToArray(); var staleMessages = chatLines.Where(c => c.LifetimeEnd == double.MaxValue).ToArray();
int count = staleMessages.Length - Channel.MaxHistory; int count = staleMessages.Length - Channel.MAX_HISTORY;
for (int i = 0; i < count; i++) if (count > 0)
{ {
var d = staleMessages[i]; void expireAndAdjustScroll(Drawable d)
if (!scroll.IsScrolledToEnd(10)) {
scroll.OffsetScrollPosition(-d.DrawHeight); scroll.OffsetScrollPosition(-d.DrawHeight);
d.Expire(); d.Expire();
}
for (int i = 0; i < count; i++)
expireAndAdjustScroll(staleMessages[i]);
// remove all adjacent day separators after stale message removal
for (int i = 0; i < ChatLineFlow.Count - 1; i++)
{
if (!(ChatLineFlow[i] is DaySeparator)) break;
if (!(ChatLineFlow[i + 1] is DaySeparator)) break;
expireAndAdjustScroll(ChatLineFlow[i]);
}
} }
if (shouldScrollToEnd)
scrollToEnd();
} }
private void pendingMessageResolved(Message existing, Message updated) private void pendingMessageResolved(Message existing, Message updated)
@ -141,7 +156,7 @@ namespace osu.Game.Overlays.Chat
private void scrollToEnd() => ScheduleAfterChildren(() => scroll.ScrollToEnd()); private void scrollToEnd() => ScheduleAfterChildren(() => scroll.ScrollToEnd());
protected class DaySeparator : Container public class DaySeparator : Container
{ {
public float TextSize public float TextSize
{ {

View File

@ -200,6 +200,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
{ {
private TextBox username; private TextBox username;
private TextBox password; private TextBox password;
private ShakeContainer shakeSignIn;
private IAPIProvider api; private IAPIProvider api;
public Action RequestHide; public Action RequestHide;
@ -208,6 +209,8 @@ namespace osu.Game.Overlays.Settings.Sections.General
{ {
if (!string.IsNullOrEmpty(username.Text) && !string.IsNullOrEmpty(password.Text)) if (!string.IsNullOrEmpty(username.Text) && !string.IsNullOrEmpty(password.Text))
api.Login(username.Text, password.Text); api.Login(username.Text, password.Text);
else
shakeSignIn.Shake();
} }
[BackgroundDependencyLoader(permitNulls: true)] [BackgroundDependencyLoader(permitNulls: true)]
@ -244,10 +247,23 @@ namespace osu.Game.Overlays.Settings.Sections.General
LabelText = "Stay signed in", LabelText = "Stay signed in",
Bindable = config.GetBindable<bool>(OsuSetting.SavePassword), Bindable = config.GetBindable<bool>(OsuSetting.SavePassword),
}, },
new SettingsButton new Container
{ {
Text = "Sign in", RelativeSizeAxes = Axes.X,
Action = performLogin AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
shakeSignIn = new ShakeContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = new SettingsButton
{
Text = "Sign in",
Action = performLogin
},
}
}
}, },
new SettingsButton new SettingsButton
{ {

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Linq; using System.Linq;
using Microsoft.EntityFrameworkCore.Internal;
using osu.Framework.MathUtils; using osu.Framework.MathUtils;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -88,7 +87,17 @@ namespace osu.Game.Screens.Edit
if (direction < 0 && timingPoint.Time == CurrentTime) if (direction < 0 && timingPoint.Time == CurrentTime)
{ {
// When going backwards and we're at the boundary of two timing points, we compute the seek distance with the timing point which we are seeking into // When going backwards and we're at the boundary of two timing points, we compute the seek distance with the timing point which we are seeking into
int activeIndex = ControlPointInfo.TimingPoints.IndexOf(timingPoint); int activeIndex = -1;
for (int i = 0; i < ControlPointInfo.TimingPoints.Count; i++)
{
if (ControlPointInfo.TimingPoints[i] == timingPoint)
{
activeIndex = i;
break;
}
}
while (activeIndex > 0 && CurrentTime == timingPoint.Time) while (activeIndex > 0 && CurrentTime == timingPoint.Time)
timingPoint = ControlPointInfo.TimingPoints[--activeIndex]; timingPoint = ControlPointInfo.TimingPoints[--activeIndex];
} }

View File

@ -42,6 +42,7 @@ namespace osu.Game.Screens.Menu
public IntroSequence() public IntroSequence()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
Alpha = 0;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]

View File

@ -45,8 +45,6 @@ namespace osu.Game.Screens.Play.HUD
VisualSettings = new VisualSettings { Expanded = false } VisualSettings = new VisualSettings { Expanded = false }
} }
}; };
Show();
} }
protected override void PopIn() => this.FadeIn(fade_duration); protected override void PopIn() => this.FadeIn(fade_duration);

View File

@ -106,6 +106,8 @@ namespace osu.Game.Screens.Play
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete();
Show(); Show();
replayLoaded.ValueChanged += loaded => AllowSeeking = loaded.NewValue; replayLoaded.ValueChanged += loaded => AllowSeeking = loaded.NewValue;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace osu.Game.Screens.Select.Carousel namespace osu.Game.Screens.Select.Carousel
{ {
@ -81,12 +82,10 @@ namespace osu.Game.Screens.Select.Carousel
{ {
base.Filter(criteria); base.Filter(criteria);
var children = new List<CarouselItem>(InternalChildren); InternalChildren.ForEach(c => c.Filter(criteria));
// IEnumerable<T>.OrderBy() is used instead of List<T>.Sort() to ensure sorting stability
children.ForEach(c => c.Filter(criteria)); var criteriaComparer = Comparer<CarouselItem>.Create((x, y) => x.CompareTo(criteria, y));
children.Sort((x, y) => x.CompareTo(criteria, y)); InternalChildren = InternalChildren.OrderBy(c => c, criteriaComparer).ToList();
InternalChildren = children;
} }
protected virtual void ChildItemStateChanged(CarouselItem item, CarouselItemState value) protected virtual void ChildItemStateChanged(CarouselItem item, CarouselItemState value)

View File

@ -26,7 +26,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.1023.0" /> <PackageReference Include="ppy.osu.Framework" Version="2019.1029.0" />
<PackageReference Include="SharpCompress" Version="0.24.0" /> <PackageReference Include="SharpCompress" Version="0.24.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />

View File

@ -118,8 +118,8 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.1010.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.1023.0" /> <PackageReference Include="ppy.osu.Framework" Version="2019.1029.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2019.1023.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2019.1029.0" />
<PackageReference Include="SharpCompress" Version="0.24.0" /> <PackageReference Include="SharpCompress" Version="0.24.0" />
<PackageReference Include="NUnit" Version="3.11.0" /> <PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />