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

Merge branch 'master' into more-timeline-toggles

This commit is contained in:
Bartłomiej Dach 2020-10-04 12:53:10 +02:00 committed by GitHub
commit 7957773d58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 609 additions and 165 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.930.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1001.0" />
</ItemGroup>
</Project>

View File

@ -9,7 +9,7 @@ using osu.Framework.Android;
namespace osu.Android
{
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullSensor, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
public class OsuGameActivity : AndroidGameActivity
{
protected override Framework.Game CreateGame() => new OsuGameAndroid();

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
@ -13,7 +14,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
public class TestSceneHoldNote : ManiaHitObjectTestScene
{
public TestSceneHoldNote()
[Test]
public void TestHoldNote()
{
AddToggleStep("toggle hitting", v =>
{

View File

@ -28,8 +28,15 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestFixture]
public class TestSceneNotes : OsuTestScene
{
[BackgroundDependencyLoader]
private void load()
[Test]
public void TestVariousNotes()
{
DrawableNote note1 = null;
DrawableNote note2 = null;
DrawableHoldNote holdNote1 = null;
DrawableHoldNote holdNote2 = null;
AddStep("create notes", () =>
{
Child = new FillFlowContainer
{
@ -41,12 +48,13 @@ namespace osu.Game.Rulesets.Mania.Tests
Spacing = new Vector2(20),
Children = new[]
{
createNoteDisplay(ScrollingDirection.Down, 1, out var note1),
createNoteDisplay(ScrollingDirection.Up, 2, out var note2),
createHoldNoteDisplay(ScrollingDirection.Down, 1, out var holdNote1),
createHoldNoteDisplay(ScrollingDirection.Up, 2, out var holdNote2),
createNoteDisplay(ScrollingDirection.Down, 1, out note1),
createNoteDisplay(ScrollingDirection.Up, 2, out note2),
createHoldNoteDisplay(ScrollingDirection.Down, 1, out holdNote1),
createHoldNoteDisplay(ScrollingDirection.Up, 2, out holdNote2),
}
};
});
AddAssert("note 1 facing downwards", () => verifyAnchors(note1, Anchor.y2));
AddAssert("note 2 facing upwards", () => verifyAnchors(note2, Anchor.y0));

View File

@ -20,7 +20,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
private int depthIndex;
public TestSceneHitCircle()
[Test]
public void TestVariousHitCircles()
{
AddStep("Miss Big Single", () => SetContents(() => testSingle(2)));
AddStep("Miss Medium Single", () => SetContents(() => testSingle(5)));

View File

@ -27,7 +27,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
private int depthIndex;
public TestSceneSlider()
[Test]
public void TestVariousSliders()
{
AddStep("Big Single", () => SetContents(() => testSimpleBig()));
AddStep("Medium Single", () => SetContents(() => testSimpleMedium()));
@ -164,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
StartTime = Time.Current + 1000,
StartTime = Time.Current + time_offset,
Position = new Vector2(239, 176),
Path = new SliderPath(PathType.PerfectCurve, new[]
{
@ -185,22 +186,26 @@ namespace osu.Game.Rulesets.Osu.Tests
private Drawable testSlowSpeed() => createSlider(speedMultiplier: 0.5);
private Drawable testShortSlowSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 0.5);
private Drawable testShortSlowSpeed(int repeats = 0) => createSlider(distance: max_length / 4, repeats: repeats, speedMultiplier: 0.5);
private Drawable testHighSpeed(int repeats = 0) => createSlider(repeats: repeats, speedMultiplier: 15);
private Drawable testShortHighSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 15);
private Drawable testShortHighSpeed(int repeats = 0) => createSlider(distance: max_length / 4, repeats: repeats, speedMultiplier: 15);
private Drawable createSlider(float circleSize = 2, float distance = 400, int repeats = 0, double speedMultiplier = 2, int stackHeight = 0)
private const double time_offset = 1500;
private const float max_length = 200;
private Drawable createSlider(float circleSize = 2, float distance = max_length, int repeats = 0, double speedMultiplier = 2, int stackHeight = 0)
{
var slider = new Slider
{
StartTime = Time.Current + 1000,
Position = new Vector2(-(distance / 2), 0),
StartTime = Time.Current + time_offset,
Position = new Vector2(0, -(distance / 2)),
Path = new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,
new Vector2(distance, 0),
new Vector2(0, distance),
}, distance),
RepeatCount = repeats,
StackHeight = stackHeight
@ -213,14 +218,14 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
StartTime = Time.Current + 1000,
Position = new Vector2(-200, 0),
StartTime = Time.Current + time_offset,
Position = new Vector2(-max_length / 2, 0),
Path = new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,
new Vector2(200, 200),
new Vector2(400, 0)
}, 600),
new Vector2(max_length / 2, max_length / 2),
new Vector2(max_length, 0)
}, max_length * 1.5f),
RepeatCount = repeats,
};
@ -233,16 +238,16 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
StartTime = Time.Current + 1000,
Position = new Vector2(-200, 0),
StartTime = Time.Current + time_offset,
Position = new Vector2(-max_length / 2, 0),
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(150, 75),
new Vector2(200, 0),
new Vector2(300, -200),
new Vector2(400, 0),
new Vector2(430, 0)
new Vector2(max_length * 0.375f, max_length * 0.18f),
new Vector2(max_length / 2, 0),
new Vector2(max_length * 0.75f, -max_length / 2),
new Vector2(max_length * 0.95f, 0),
new Vector2(max_length, 0)
}),
RepeatCount = repeats,
};
@ -256,15 +261,15 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
StartTime = Time.Current + 1000,
Position = new Vector2(-200, 0),
StartTime = Time.Current + time_offset,
Position = new Vector2(-max_length / 2, 0),
Path = new SliderPath(PathType.Bezier, new[]
{
Vector2.Zero,
new Vector2(150, 75),
new Vector2(200, 100),
new Vector2(300, -200),
new Vector2(430, 0)
new Vector2(max_length * 0.375f, max_length * 0.18f),
new Vector2(max_length / 2, max_length / 4),
new Vector2(max_length * 0.75f, -max_length / 2),
new Vector2(max_length, 0)
}),
RepeatCount = repeats,
};
@ -278,16 +283,16 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
StartTime = Time.Current + 1000,
StartTime = Time.Current + time_offset,
Position = new Vector2(0, 0),
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(-200, 0),
new Vector2(-max_length / 2, 0),
new Vector2(0, 0),
new Vector2(0, -200),
new Vector2(-200, -200),
new Vector2(0, -200)
new Vector2(0, -max_length / 2),
new Vector2(-max_length / 2, -max_length / 2),
new Vector2(0, -max_length / 2)
}),
RepeatCount = repeats,
};
@ -305,14 +310,14 @@ namespace osu.Game.Rulesets.Osu.Tests
var slider = new Slider
{
StartTime = Time.Current + 1000,
Position = new Vector2(-100, 0),
StartTime = Time.Current + time_offset,
Position = new Vector2(-max_length / 4, 0),
Path = new SliderPath(PathType.Catmull, new[]
{
Vector2.Zero,
new Vector2(50, -50),
new Vector2(150, 50),
new Vector2(200, 0)
new Vector2(max_length * 0.125f, max_length * 0.125f),
new Vector2(max_length * 0.375f, max_length * 0.125f),
new Vector2(max_length / 2, 0)
}),
RepeatCount = repeats,
NodeSamples = repeatSamples

View File

@ -110,6 +110,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
}
public override void StopAllSamples()
{
base.StopAllSamples();
slidingSample?.Stop();
}
private void updateSlidingSample(ValueChangedEvent<bool> tracking)
{
if (tracking.NewValue)

View File

@ -124,6 +124,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
}
public override void StopAllSamples()
{
base.StopAllSamples();
spinningSample?.Stop();
}
protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
base.AddNestedHitObject(hitObject);

View File

@ -6,6 +6,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
@ -25,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}
[BackgroundDependencyLoader]
private void load(TextureStore textures)
private void load(TextureStore textures, DrawableHitObject drawableHitObject)
{
InternalChildren = new Drawable[]
{
@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Origin = Anchor.Centre,
Texture = textures.Get(@"Gameplay/osu/disc"),
},
new TrianglesPiece
new TrianglesPiece((int)drawableHitObject.HitObject.StartTime)
{
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,

View File

@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private Spinner spinner;
private const float initial_scale = 1.3f;
private const float idle_alpha = 0.2f;
private const float tracking_alpha = 0.4f;
@ -41,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
// we are slightly bigger than our parent, to clip the top and bottom of the circle
// this should probably be revisited when scaled spinners are a thing.
Scale = new Vector2(1.3f);
Scale = new Vector2(initial_scale);
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
@ -93,6 +94,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
drawableSpinner.RotationTracker.Complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200));
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
}
protected override void Update()
@ -115,8 +118,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
fill.Alpha = (float)Interpolation.Damp(fill.Alpha, drawableSpinner.RotationTracker.Tracking ? tracking_alpha : idle_alpha, 0.98f, (float)Math.Abs(Clock.ElapsedFrameTime));
}
const float initial_scale = 0.2f;
float targetScale = initial_scale + (1 - initial_scale) * drawableSpinner.Progress;
const float initial_fill_scale = 0.2f;
float targetScale = initial_fill_scale + (1 - initial_fill_scale) * drawableSpinner.Progress;
fill.Scale = new Vector2((float)Interpolation.Lerp(fill.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1)));
mainContainer.Rotation = drawableSpinner.RotationTracker.Rotation;
@ -124,14 +127,43 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
centre.ScaleTo(0);
mainContainer.ScaleTo(0);
if (!(drawableHitObject is DrawableSpinner))
return;
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true))
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
{
this.ScaleTo(initial_scale);
this.RotateTo(0);
using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
{
// constant ambient rotation to give the spinner "spinning" character.
this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration);
}
using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset, true))
{
switch (state)
{
case ArmedState.Hit:
this.ScaleTo(initial_scale * 1.2f, 320, Easing.Out);
this.RotateTo(mainContainer.Rotation + 180, 320);
break;
case ArmedState.Miss:
this.ScaleTo(initial_scale * 0.8f, 320, Easing.In);
break;
}
}
}
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
{
centre.ScaleTo(0);
mainContainer.ScaleTo(0);
using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
{
centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint);
mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint);
@ -141,24 +173,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint);
}
}
}
// transforms we have from completing the spinner will be rolled back, so reapply immediately.
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
updateComplete(state == ArmedState.Hit, 0);
using (BeginDelayedSequence(spinner.Duration, true))
{
switch (state)
{
case ArmedState.Hit:
this.ScaleTo(Scale * 1.2f, 320, Easing.Out);
this.RotateTo(mainContainer.Rotation + 180, 320);
break;
case ArmedState.Miss:
this.ScaleTo(Scale * 0.8f, 320, Easing.In);
break;
}
}
}
private void updateComplete(bool complete, double duration)

View File

@ -11,7 +11,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
protected override bool CreateNewTriangles => false;
protected override float SpawnRatio => 0.5f;
public TrianglesPiece()
public TrianglesPiece(int? seed = null)
: base(seed)
{
TriangleScale = 1.2f;
HideAlphaDiscrepancies = false;

View File

@ -70,21 +70,31 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
base.LoadComplete();
this.FadeOut();
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
}
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
if (!(drawableHitObject is DrawableSpinner))
return;
var spinner = (Spinner)drawableSpinner.HitObject;
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
this.FadeOut();
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true))
this.FadeInFromZero(spinner.TimeFadeIn / 2);
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
{
fixedMiddle.FadeColour(Color4.White);
using (BeginAbsoluteSequence(spinner.StartTime, true))
using (BeginDelayedSequence(spinner.TimePreempt, true))
fixedMiddle.FadeColour(Color4.Red, spinner.Duration);
}
}
protected override void Update()
{

View File

@ -24,12 +24,16 @@ namespace osu.Game.Rulesets.Osu.Skinning
private Sprite metreSprite;
private Container metre;
private bool spinnerBlink;
private const float sprite_scale = 1 / 1.6f;
private const float final_metre_height = 692 * sprite_scale;
[BackgroundDependencyLoader]
private void load(ISkinSource source, DrawableHitObject drawableObject)
{
spinnerBlink = source.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.SpinnerNoBlink)?.Value != true;
drawableSpinner = (DrawableSpinner)drawableObject;
RelativeSizeAxes = Axes.Both;
@ -84,14 +88,20 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
base.LoadComplete();
this.FadeOut();
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
}
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
if (!(drawableHitObject is DrawableSpinner))
return;
var spinner = drawableSpinner.HitObject;
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
this.FadeOut();
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true))
this.FadeInFromZero(spinner.TimeFadeIn / 2);
}
@ -116,12 +126,15 @@ namespace osu.Game.Rulesets.Osu.Skinning
private float getMetreHeight(float progress)
{
progress = Math.Min(99, progress * 100);
progress *= 100;
// the spinner should still blink at 100% progress.
if (spinnerBlink)
progress = Math.Min(99, progress);
int barCount = (int)progress / 10;
// todo: add SpinnerNoBlink support
if (RNG.NextBool(((int)progress % 10) / 10f))
if (spinnerBlink && RNG.NextBool(((int)progress % 10) / 10f))
barCount++;
return (float)barCount / total_bars * final_metre_height;

View File

@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
CursorRotate,
HitCircleOverlayAboveNumber,
HitCircleOverlayAboveNumer, // Some old skins will have this typo
SpinnerFrequencyModulate
SpinnerFrequencyModulate,
SpinnerNoBlink
}
}

View File

@ -3,7 +3,6 @@
using System;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
@ -30,8 +29,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
private readonly Random rng = new Random(1337);
[BackgroundDependencyLoader]
private void load()
[Test]
public void TestVariousHits()
{
AddStep("Hit", () => addHitJudgement(false));
AddStep("Strong hit", () => addStrongHitJudgement(false));

View File

@ -12,6 +12,14 @@ namespace osu.Game.Tests.Editing
[TestFixture]
public class EditorChangeHandlerTest
{
private int stateChangedFired;
[SetUp]
public void SetUp()
{
stateChangedFired = 0;
}
[Test]
public void TestSaveRestoreState()
{
@ -23,6 +31,8 @@ namespace osu.Game.Tests.Editing
addArbitraryChange(beatmap);
handler.SaveState();
Assert.That(stateChangedFired, Is.EqualTo(1));
Assert.That(handler.CanUndo.Value, Is.True);
Assert.That(handler.CanRedo.Value, Is.False);
@ -30,6 +40,8 @@ namespace osu.Game.Tests.Editing
Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.True);
Assert.That(stateChangedFired, Is.EqualTo(2));
}
[Test]
@ -45,6 +57,7 @@ namespace osu.Game.Tests.Editing
Assert.That(handler.CanUndo.Value, Is.True);
Assert.That(handler.CanRedo.Value, Is.False);
Assert.That(stateChangedFired, Is.EqualTo(1));
string hash = handler.CurrentStateHash;
@ -52,6 +65,7 @@ namespace osu.Game.Tests.Editing
handler.SaveState();
Assert.That(hash, Is.EqualTo(handler.CurrentStateHash));
Assert.That(stateChangedFired, Is.EqualTo(1));
handler.RestoreState(-1);
@ -60,6 +74,7 @@ namespace osu.Game.Tests.Editing
// we should only be able to restore once even though we saved twice.
Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.True);
Assert.That(stateChangedFired, Is.EqualTo(2));
}
[Test]
@ -71,6 +86,8 @@ namespace osu.Game.Tests.Editing
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
{
Assert.That(stateChangedFired, Is.EqualTo(i));
addArbitraryChange(beatmap);
handler.SaveState();
}
@ -114,7 +131,10 @@ namespace osu.Game.Tests.Editing
{
var beatmap = new EditorBeatmap(new Beatmap());
return (new EditorChangeHandler(beatmap), beatmap);
var changeHandler = new EditorChangeHandler(beatmap);
changeHandler.OnStateChange += () => stateChangedFired++;
return (changeHandler, beatmap);
}
private void addArbitraryChange(EditorBeatmap beatmap)

View File

@ -139,6 +139,22 @@ namespace osu.Game.Tests.NonVisual
Assert.That(cpi.Groups.Count, Is.EqualTo(0));
}
[Test]
public void TestRemoveGroupAlsoRemovedControlPoints()
{
var cpi = new ControlPointInfo();
var group = cpi.GroupAt(1000, true);
group.Add(new SampleControlPoint());
Assert.That(cpi.SamplePoints.Count, Is.EqualTo(1));
cpi.RemoveGroup(group);
Assert.That(cpi.SamplePoints.Count, Is.EqualTo(0));
}
[Test]
public void TestAddControlPointToGroup()
{

View File

@ -0,0 +1,47 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics.Audio;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneEditorSamplePlayback : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
[Test]
public void TestSlidingSampleStopsOnSeek()
{
DrawableSlider slider = null;
DrawableSample[] samples = null;
AddStep("get first slider", () =>
{
slider = Editor.ChildrenOfType<DrawableSlider>().OrderBy(s => s.HitObject.StartTime).First();
samples = slider.ChildrenOfType<DrawableSample>().ToArray();
});
AddStep("start playback", () => EditorClock.Start());
AddUntilStep("wait for slider sliding then seek", () =>
{
if (!slider.Tracking.Value)
return false;
if (!samples.Any(s => s.Playing))
return false;
EditorClock.Seek(20000);
return true;
});
AddAssert("slider samples are not playing", () => samples.Length == 5 && samples.All(s => s.Played && !s.Playing));
}
}
}

View File

@ -28,7 +28,7 @@ namespace osu.Game.Tournament.Components
{
base.Bindable = new Bindable<string>();
((OsuTextBox)Control).OnCommit = (sender, newText) =>
((OsuTextBox)Control).OnCommit += (sender, newText) =>
{
try
{

View File

@ -158,6 +158,9 @@ namespace osu.Game.Beatmaps.ControlPoints
public void RemoveGroup(ControlPointGroup group)
{
foreach (var item in group.ControlPoints.ToArray())
group.Remove(item);
group.ItemAdded -= groupItemAdded;
group.ItemRemoved -= groupItemRemoved;

View File

@ -86,13 +86,24 @@ namespace osu.Game.Graphics.Backgrounds
/// </summary>
public float Velocity = 1;
private readonly Random stableRandom;
private float nextRandom() => (float)(stableRandom?.NextDouble() ?? RNG.NextSingle());
private readonly SortedList<TriangleParticle> parts = new SortedList<TriangleParticle>(Comparer<TriangleParticle>.Default);
private IShader shader;
private readonly Texture texture;
public Triangles()
/// <summary>
/// Construct a new triangle visualisation.
/// </summary>
/// <param name="seed">An optional seed to stabilise random positions / attributes. Note that this does not guarantee stable playback when seeking in time.</param>
public Triangles(int? seed = null)
{
if (seed != null)
stableRandom = new Random(seed.Value);
texture = Texture.WhitePixel;
}
@ -175,8 +186,8 @@ namespace osu.Game.Graphics.Backgrounds
{
TriangleParticle particle = CreateTriangle();
particle.Position = new Vector2(RNG.NextSingle(), randomY ? RNG.NextSingle() : 1);
particle.ColourShade = RNG.NextSingle();
particle.Position = new Vector2(nextRandom(), randomY ? nextRandom() : 1);
particle.ColourShade = nextRandom();
particle.Colour = CreateTriangleShade(particle.ColourShade);
return particle;
@ -191,8 +202,8 @@ namespace osu.Game.Graphics.Backgrounds
const float std_dev = 0.16f;
const float mean = 0.5f;
float u1 = 1 - RNG.NextSingle(); //uniform(0,1] random floats
float u2 = 1 - RNG.NextSingle();
float u1 = 1 - nextRandom(); //uniform(0,1] random floats
float u2 = 1 - nextRandom();
float randStdNormal = (float)(Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2)); // random normal(0,1)
var scale = Math.Max(triangleScale * (mean + std_dev * randStdNormal), 0.1f); // random normal(mean,stdDev^2)

View File

@ -38,6 +38,7 @@ namespace osu.Game.Online.API.Requests.Responses
Rank = Rank,
Ruleset = ruleset,
Mods = mods,
IsLegacyScore = true
};
if (Statistics != null)

View File

@ -59,12 +59,13 @@ namespace osu.Game.Online.Chat
RelativeSizeAxes = Axes.X,
Height = textbox_height,
PlaceholderText = "type your message",
OnCommit = postMessage,
ReleaseFocusOnCommit = false,
HoldFocus = true,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
});
textbox.OnCommit += postMessage;
}
Channel.BindValueChanged(channelChanged);

View File

@ -146,7 +146,6 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both,
Height = 1,
PlaceholderText = "type your message",
OnCommit = postMessage,
ReleaseFocusOnCommit = false,
HoldFocus = true,
}
@ -186,6 +185,8 @@ namespace osu.Game.Overlays
},
};
textbox.OnCommit += postMessage;
ChannelTabControl.Current.ValueChanged += current => channelManager.CurrentChannel.Value = current.NewValue;
ChannelTabControl.ChannelSelectorActive.ValueChanged += active => ChannelSelectionOverlay.State.Value = active.NewValue ? Visibility.Visible : Visibility.Hidden;
ChannelSelectionOverlay.State.ValueChanged += state =>

View File

@ -75,7 +75,7 @@ namespace osu.Game.Overlays.Music
},
};
filter.Search.OnCommit = (sender, newText) =>
filter.Search.OnCommit += (sender, newText) =>
{
BeatmapInfo toSelect = list.FirstVisibleSet?.Beatmaps?.FirstOrDefault();

View File

@ -236,7 +236,6 @@ namespace osu.Game.Overlays.Settings.Sections.General
PlaceholderText = "password",
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this,
OnCommit = (sender, newText) => performLogin()
},
new SettingsCheckbox
{
@ -276,6 +275,8 @@ namespace osu.Game.Overlays.Settings.Sections.General
}
}
};
password.OnCommit += (sender, newText) => performLogin();
}
public override bool AcceptsFocus => true;

View File

@ -40,17 +40,28 @@ namespace osu.Game.Rulesets.Edit
Playfield.DisplayJudgements.Value = false;
}
[Resolved(canBeNull: true)]
private IEditorChangeHandler changeHandler { get; set; }
protected override void LoadComplete()
{
base.LoadComplete();
beatmap.HitObjectAdded += addHitObject;
beatmap.HitObjectUpdated += updateReplay;
beatmap.HitObjectRemoved += removeHitObject;
if (changeHandler != null)
{
// for now only regenerate replay on a finalised state change, not HitObjectUpdated.
changeHandler.OnStateChange += updateReplay;
}
else
{
beatmap.HitObjectUpdated += _ => updateReplay();
}
}
private void updateReplay(HitObject obj = null) =>
drawableRuleset.RegenerateAutoplay();
private void updateReplay() => drawableRuleset.RegenerateAutoplay();
private void addHitObject(HitObject hitObject)
{
@ -58,8 +69,6 @@ namespace osu.Game.Rulesets.Edit
drawableRuleset.Playfield.Add(drawableObject);
drawableRuleset.Playfield.PostProcess();
updateReplay();
}
private void removeHitObject(HitObject hitObject)
@ -68,8 +77,6 @@ namespace osu.Game.Rulesets.Edit
drawableRuleset.Playfield.Remove(drawableObject);
drawableRuleset.Playfield.PostProcess();
drawableRuleset.RegenerateAutoplay();
}
public override bool PropagatePositionalInputSubTree => false;

View File

@ -124,19 +124,19 @@ namespace osu.Game.Rulesets.Judgements
return -DEFAULT_MAX_HEALTH_INCREASE;
case HitResult.Meh:
return -DEFAULT_MAX_HEALTH_INCREASE * 0.05;
return -DEFAULT_MAX_HEALTH_INCREASE * 0.5;
case HitResult.Ok:
return -DEFAULT_MAX_HEALTH_INCREASE * 0.01;
return -DEFAULT_MAX_HEALTH_INCREASE * 0.3;
case HitResult.Good:
return DEFAULT_MAX_HEALTH_INCREASE * 0.5;
return DEFAULT_MAX_HEALTH_INCREASE * 0.1;
case HitResult.Great:
return DEFAULT_MAX_HEALTH_INCREASE;
return DEFAULT_MAX_HEALTH_INCREASE * 0.8;
case HitResult.Perfect:
return DEFAULT_MAX_HEALTH_INCREASE * 1.05;
return DEFAULT_MAX_HEALTH_INCREASE;
case HitResult.SmallBonus:
return DEFAULT_MAX_HEALTH_INCREASE * 0.1;

View File

@ -51,12 +51,12 @@ namespace osu.Game.Rulesets.Objects.Drawables
public override bool PropagateNonPositionalInputSubTree => HandleUserInput;
/// <summary>
/// Invoked when a <see cref="JudgementResult"/> has been applied by this <see cref="DrawableHitObject"/> or a nested <see cref="DrawableHitObject"/>.
/// Invoked by this or a nested <see cref="DrawableHitObject"/> after a <see cref="JudgementResult"/> has been applied.
/// </summary>
public event Action<DrawableHitObject, JudgementResult> OnNewResult;
/// <summary>
/// Invoked when a <see cref="JudgementResult"/> is being reverted by this <see cref="DrawableHitObject"/> or a nested <see cref="DrawableHitObject"/>.
/// Invoked by this or a nested <see cref="DrawableHitObject"/> prior to a <see cref="JudgementResult"/> being reverted.
/// </summary>
public event Action<DrawableHitObject, JudgementResult> OnRevertResult;
@ -236,7 +236,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
#region State / Transform Management
/// <summary>
/// Bind to apply a custom state which can override the default implementation.
/// Invoked by this or a nested <see cref="DrawableHitObject"/> to apply a custom state that can override the default implementation.
/// </summary>
public event Action<DrawableHitObject, ArmedState> ApplyCustomUpdateState;
@ -384,6 +384,11 @@ namespace osu.Game.Rulesets.Objects.Drawables
}
}
/// <summary>
/// Stops playback of all samples. Automatically called when <see cref="DrawableHitObject{TObject}"/>'s lifetime has been exceeded.
/// </summary>
public virtual void StopAllSamples() => Samples?.Stop();
protected override void Update()
{
base.Update();
@ -452,6 +457,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
foreach (var nested in NestedHitObjects)
nested.OnKilled();
StopAllSamples();
UpdateResult(false);
}
@ -462,6 +469,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// <param name="application">The callback that applies changes to the <see cref="JudgementResult"/>.</param>
protected void ApplyResult(Action<JudgementResult> application)
{
if (Result.HasResult)
throw new InvalidOperationException("Cannot apply result on a hitobject that already has a result.");
application?.Invoke(Result);
if (!Result.HasResult)

View File

@ -185,6 +185,34 @@ namespace osu.Game.Scoring
[JsonProperty("position")]
public int? Position { get; set; }
private bool isLegacyScore;
/// <summary>
/// Whether this <see cref="ScoreInfo"/> represents a legacy (osu!stable) score.
/// </summary>
[JsonIgnore]
[NotMapped]
public bool IsLegacyScore
{
get
{
if (isLegacyScore)
return true;
// The above check will catch legacy online scores that have an appropriate UserString + UserId.
// For non-online scores such as those imported in, a heuristic is used based on the following table:
//
// Mode | UserString | UserId
// --------------- | ---------- | ---------
// stable | <username> | 1
// lazer | <username> | <userid>
// lazer (offline) | Guest | 1
return ID > 0 && UserID == 1 && UserString != "Guest";
}
set => isLegacyScore = value;
}
public IEnumerable<(HitResult result, int count, int? maxCount)> GetStatisticsForDisplay()
{
foreach (var key in OrderAttributeUtils.GetValuesInOrder<HitResult>())

View File

@ -10,6 +10,7 @@ using System.Threading;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
@ -149,13 +150,17 @@ namespace osu.Game.Scoring
return;
}
int? beatmapMaxCombo = score.Beatmap.MaxCombo;
int beatmapMaxCombo;
if (beatmapMaxCombo == null)
if (score.IsLegacyScore)
{
// This score is guaranteed to be an osu!stable score.
// The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used.
if (score.Beatmap.MaxCombo == null)
{
if (score.Beatmap.ID == 0 || difficulties == null)
{
// We don't have enough information (max combo) to compute the score, so let's use the provided score.
// We don't have enough information (max combo) to compute the score, so use the provided score.
Value = score.TotalScore;
return;
}
@ -163,9 +168,20 @@ namespace osu.Game.Scoring
// We can compute the max combo locally after the async beatmap difficulty computation.
difficultyBindable = difficulties().GetBindableDifficulty(score.Beatmap, score.Ruleset, score.Mods, (difficultyCancellationSource = new CancellationTokenSource()).Token);
difficultyBindable.BindValueChanged(d => updateScore(d.NewValue.MaxCombo), true);
return;
}
beatmapMaxCombo = score.Beatmap.MaxCombo.Value;
}
else
updateScore(beatmapMaxCombo.Value);
{
// This score is guaranteed to be an osu!lazer score.
// The combo must be determined through the score's statistics, as both the beatmap's max combo and the difficulty calculator will provide osu!stable combo values.
beatmapMaxCombo = Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetOrDefault(r)).Sum();
}
updateScore(beatmapMaxCombo);
}
private void updateScore(int beatmapMaxCombo)

View File

@ -21,6 +21,8 @@ namespace osu.Game.Screens.Edit
public readonly Bindable<bool> CanUndo = new Bindable<bool>();
public readonly Bindable<bool> CanRedo = new Bindable<bool>();
public event Action OnStateChange;
private readonly LegacyEditorBeatmapPatcher patcher;
private readonly List<byte[]> savedStates = new List<byte[]>();
@ -109,6 +111,8 @@ namespace osu.Game.Screens.Edit
savedStates.Add(newState);
currentState = savedStates.Count - 1;
OnStateChange?.Invoke();
updateBindables();
}
}
@ -136,6 +140,7 @@ namespace osu.Game.Screens.Edit
isRestoring = false;
OnStateChange?.Invoke();
updateBindables();
}

View File

@ -1,6 +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;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit
@ -10,6 +11,11 @@ namespace osu.Game.Screens.Edit
/// </summary>
public interface IEditorChangeHandler
{
/// <summary>
/// Fired whenever a state change occurs.
/// </summary>
event Action OnStateChange;
/// <summary>
/// Begins a bulk state change event. <see cref="EndChange"/> should be invoked soon after.
/// </summary>

View File

@ -41,6 +41,7 @@ namespace osu.Game.Screens.Edit.Timing
private IReadOnlyList<Drawable> createSections() => new Drawable[]
{
new GroupSection(),
new TimingSection(),
new DifficultySection(),
new SampleSection(),

View File

@ -2,27 +2,23 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Bindables;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Edit.Timing
{
internal class DifficultySection : Section<DifficultyControlPoint>
{
private SettingsSlider<double> multiplier;
private SliderWithTextBoxInput<double> multiplierSlider;
[BackgroundDependencyLoader]
private void load()
{
Flow.AddRange(new[]
{
multiplier = new SettingsSlider<double>
multiplierSlider = new SliderWithTextBoxInput<double>("Speed Multiplier")
{
LabelText = "Speed Multiplier",
Bindable = new DifficultyControlPoint().SpeedMultiplierBindable,
RelativeSizeAxes = Axes.X,
Current = new DifficultyControlPoint().SpeedMultiplierBindable
}
});
}
@ -31,7 +27,7 @@ namespace osu.Game.Screens.Edit.Timing
{
if (point.NewValue != null)
{
multiplier.Bindable = point.NewValue.SpeedMultiplierBindable;
multiplierSlider.Current = point.NewValue.SpeedMultiplierBindable;
}
}

View File

@ -0,0 +1,119 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osuTK;
namespace osu.Game.Screens.Edit.Timing
{
internal class GroupSection : CompositeDrawable
{
private LabelledTextBox textBox;
private TriangleButton button;
[Resolved]
protected Bindable<ControlPointGroup> SelectedGroup { get; private set; }
[Resolved]
protected IBindable<WorkingBeatmap> Beatmap { get; private set; }
[Resolved]
private EditorClock clock { get; set; }
[Resolved(canBeNull: true)]
private IEditorChangeHandler changeHandler { get; set; }
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Padding = new MarginPadding(10);
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(10),
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
textBox = new LabelledTextBox
{
Label = "Time"
},
button = new TriangleButton
{
Text = "Use current time",
RelativeSizeAxes = Axes.X,
Action = () => changeSelectedGroupTime(clock.CurrentTime)
}
}
},
};
textBox.OnCommit += (sender, isNew) =>
{
if (!isNew)
return;
if (double.TryParse(sender.Text, out var newTime))
{
changeSelectedGroupTime(newTime);
}
else
{
SelectedGroup.TriggerChange();
}
};
SelectedGroup.BindValueChanged(group =>
{
if (group.NewValue == null)
{
textBox.Text = string.Empty;
textBox.Current.Disabled = true;
button.Enabled.Value = false;
return;
}
textBox.Current.Disabled = false;
button.Enabled.Value = true;
textBox.Text = $"{group.NewValue.Time:n0}";
}, true);
}
private void changeSelectedGroupTime(in double time)
{
if (SelectedGroup.Value == null || time == SelectedGroup.Value.Time)
return;
changeHandler?.BeginChange();
var currentGroupItems = SelectedGroup.Value.ControlPoints.ToArray();
Beatmap.Value.Beatmap.ControlPointInfo.RemoveGroup(SelectedGroup.Value);
foreach (var cp in currentGroupItems)
Beatmap.Value.Beatmap.ControlPointInfo.Add(time, cp);
SelectedGroup.Value = Beatmap.Value.Beatmap.ControlPointInfo.GroupAt(time);
changeHandler?.EndChange();
}
}
}

View File

@ -2,18 +2,17 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Edit.Timing
{
internal class SampleSection : Section<SampleControlPoint>
{
private LabelledTextBox bank;
private SettingsSlider<int> volume;
private SliderWithTextBoxInput<int> volume;
[BackgroundDependencyLoader]
private void load()
@ -24,10 +23,9 @@ namespace osu.Game.Screens.Edit.Timing
{
Label = "Bank Name",
},
volume = new SettingsSlider<int>
volume = new SliderWithTextBoxInput<int>("Volume")
{
Bindable = new SampleControlPoint().SampleVolumeBindable,
LabelText = "Volume",
Current = new SampleControlPoint().SampleVolumeBindable,
}
});
}
@ -37,7 +35,7 @@ namespace osu.Game.Screens.Edit.Timing
if (point.NewValue != null)
{
bank.Current = point.NewValue.SampleBankBindable;
volume.Bindable = point.NewValue.SampleVolumeBindable;
volume.Current = point.NewValue.SampleVolumeBindable;
}
}

View File

@ -0,0 +1,77 @@
// 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 osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Edit.Timing
{
internal class SliderWithTextBoxInput<T> : CompositeDrawable, IHasCurrentValue<T>
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
{
private readonly SettingsSlider<T> slider;
public SliderWithTextBoxInput(string labelText)
{
LabelledTextBox textbox;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
textbox = new LabelledTextBox
{
Label = labelText,
},
slider = new SettingsSlider<T>
{
RelativeSizeAxes = Axes.X,
}
}
},
};
textbox.OnCommit += (t, isNew) =>
{
if (!isNew) return;
try
{
slider.Bindable.Parse(t.Text);
}
catch
{
// TriggerChange below will restore the previous text value on failure.
}
// This is run regardless of parsing success as the parsed number may not actually trigger a change
// due to bindable clamping. Even in such a case we want to update the textbox to a sane visual state.
Current.TriggerChange();
};
Current.BindValueChanged(val =>
{
textbox.Text = val.NewValue.ToString();
}, true);
}
public Bindable<T> Current
{
get => slider.Bindable;
set => slider.Bindable = value;
}
}
}

View File

@ -65,18 +65,19 @@ namespace osu.Game.Screens.Edit.Timing
{
if (!isNew) return;
if (double.TryParse(Current.Value, out double doubleVal))
{
try
{
if (double.TryParse(Current.Value, out double doubleVal) && doubleVal > 0)
beatLengthBindable.Value = beatLengthToBpm(doubleVal);
}
catch
{
// will restore the previous text value on failure.
// TriggerChange below will restore the previous text value on failure.
}
// This is run regardless of parsing success as the parsed number may not actually trigger a change
// due to bindable clamping. Even in such a case we want to update the textbox to a sane visual state.
beatLengthBindable.TriggerChange();
}
}
};
beatLengthBindable.BindValueChanged(val =>

View File

@ -18,7 +18,7 @@ namespace osu.Game.Skinning
ComboPrefix,
ComboOverlap,
AnimationFramerate,
LayeredHitSounds,
LayeredHitSounds
}
}
}

View File

@ -41,7 +41,14 @@ namespace osu.Game.Skinning
// it's not easy to know if a sample has finished playing (to end).
// to keep things simple only resume playing looping samples.
else if (Looping)
{
// schedule so we don't start playing a sample which is no longer alive.
Schedule(() =>
{
if (RequestedPlaying)
base.Play();
});
}
}
});
}

View File

@ -24,7 +24,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.930.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1001.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
<PackageReference Include="Sentry" Version="2.1.6" />
<PackageReference Include="SharpCompress" Version="0.26.0" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.930.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1001.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
</ItemGroup>
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
@ -80,11 +80,11 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.930.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1001.0" />
<PackageReference Include="SharpCompress" Version="0.26.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.7.0" />
<PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2020.213.0" ExcludeAssets="all" />
<PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2020.923.0" ExcludeAssets="all" />
</ItemGroup>
</Project>