1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 21:02:54 +08:00

Merge branch 'master' into score-encoding-cleanup

This commit is contained in:
Dean Herbert 2023-10-27 18:03:38 +09:00
commit fc25e24397
No known key found for this signature in database
20 changed files with 258 additions and 59 deletions

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
@ -179,5 +180,33 @@ namespace osu.Game.Rulesets.Catch.Edit
return null;
}
}
protected override void Update()
{
base.Update();
updateDistanceSnapGrid();
}
private void updateDistanceSnapGrid()
{
if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True)
{
distanceSnapGrid.Hide();
return;
}
var sourceHitObject = getDistanceSnapGridSourceHitObject();
if (sourceHitObject == null)
{
distanceSnapGrid.Hide();
return;
}
distanceSnapGrid.Show();
distanceSnapGrid.StartTime = sourceHitObject.GetEndTime();
distanceSnapGrid.StartX = sourceHitObject.EffectiveX;
}
}
}

View File

@ -38,6 +38,42 @@ namespace osu.Game.Rulesets.Osu.Tests
private readonly List<JudgementResult> judgementResults = new List<JudgementResult>();
[TestCase(30, 0)]
[TestCase(30, 1)]
[TestCase(40, 0)]
[TestCase(40, 1)]
[TestCase(50, 1)]
[TestCase(60, 1)]
[TestCase(70, 1)]
[TestCase(80, 1)]
[TestCase(80, 0)]
[TestCase(80, 10)]
[TestCase(90, 1)]
[Ignore("headless test doesn't run at high enough precision for this to always enter a tracking state in time.")]
public void TestVeryShortSliderMissHead(float sliderLength, int repeatCount)
{
performTest(new List<ReplayFrame>
{
new OsuReplayFrame { Position = new Vector2(50, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start - 10 },
new OsuReplayFrame { Position = new Vector2(50, 0), Actions = { OsuAction.LeftButton, OsuAction.RightButton }, Time = time_slider_start + 2000 },
}, new Slider
{
StartTime = time_slider_start,
Position = new Vector2(0, 0),
SliderVelocityMultiplier = 10f,
RepeatCount = repeatCount,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(sliderLength, 0),
}),
}, 240, 1);
AddAssert("Head judgement is first", () => judgementResults[0].HitObject is SliderHeadCircle);
AddAssert("Tail judgement is second last", () => judgementResults[^2].HitObject is SliderTailCircle);
AddAssert("Slider judgement is last", () => judgementResults[^1].HitObject is Slider);
}
// Making these too short causes breakage from frames not being processed fast enough.
// To keep things simple, these tests are crafted to always be >16ms length.
// If sliders shorter than this are ever used in gameplay it will probably break things and we can revisit.
@ -76,6 +112,8 @@ namespace osu.Game.Rulesets.Osu.Tests
assertAllMaxJudgements();
AddAssert("Head judgement is first", () => judgementResults.First().HitObject is SliderHeadCircle);
// Even if the last tick is hit early, the slider should always execute its final judgement at its endtime.
// If not, hitsounds will not play on time.
AddAssert("Judgement offset is zero", () => judgementResults.Last().TimeOffset == 0);
@ -121,6 +159,8 @@ namespace osu.Game.Rulesets.Osu.Tests
else
AddAssert("Tracking dropped", assertMidSliderJudgementFail);
AddAssert("Head judgement is first", () => judgementResults.First().HitObject is SliderHeadCircle);
// Even if the last tick is hit early, the slider should always execute its final judgement at its endtime.
// If not, hitsounds will not play on time.
AddAssert("Judgement offset is zero", () => judgementResults.Last().TimeOffset == 0);

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override LocalisableString Description => "Burn the notes into your memory.";
//Alters the transforms of the approach circles, breaking the effects of these mods.
public override Type[] IncompatibleMods => new[] { typeof(OsuModApproachDifferent) };
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModApproachDifferent), typeof(OsuModTransform) }).ToArray();
public override ModType Type => ModType.Fun;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override LocalisableString Description => "Everything rotates. EVERYTHING.";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel) };
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModFreezeFrame) }).ToArray();
private float theta;

View File

@ -4,7 +4,6 @@
#nullable disable
using System.Diagnostics;
using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
@ -15,9 +14,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public new SliderHeadCircle HitObject => (SliderHeadCircle)base.HitObject;
[CanBeNull]
public Slider Slider => DrawableSlider?.HitObject;
public DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult;

View File

@ -17,7 +17,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public partial class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking
public partial class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking, IRequireTracking
{
public new SliderRepeat HitObject => (SliderRepeat)base.HitObject;
@ -36,6 +36,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public override bool DisplayResult => false;
public bool Tracking { get; set; }
public DrawableSliderRepeat()
: base(null)
{
@ -85,8 +87,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (HitObject.StartTime <= Time.Current)
ApplyResult(r => r.Type = DrawableSlider.Tracking.Value ? r.Judgement.MaxResult : r.Judgement.MinResult);
// shared implementation with DrawableSliderTick.
if (timeOffset >= 0)
{
// Attempt to preserve correct ordering of judgements as best we can by forcing
// an un-judged head to be missed when the user has clearly skipped it.
//
// This check is applied to all nested slider objects apart from the head (ticks, repeats, tail).
if (Tracking && !DrawableSlider.HeadCircle.Judged)
DrawableSlider.HeadCircle.MissForcefully();
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
}
protected override void UpdateInitialTransforms()

View File

@ -4,6 +4,7 @@
#nullable disable
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@ -129,16 +130,27 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (userTriggered)
return;
// Ensure the tail can only activate after all previous ticks already have.
// Ensure the tail can only activate after all previous ticks/repeats already have.
//
// This covers the edge case where the lenience may allow the tail to activate before
// the last tick, changing ordering of score/combo awarding.
if (DrawableSlider.NestedHitObjects.Count > 1 && !DrawableSlider.NestedHitObjects[^2].Judged)
var lastTick = DrawableSlider.NestedHitObjects.LastOrDefault(o => o.HitObject is SliderTick || o.HitObject is SliderRepeat);
if (lastTick?.Judged == false)
return;
if (timeOffset < SliderEventGenerator.TAIL_LENIENCY)
return;
// Attempt to preserve correct ordering of judgements as best we can by forcing
// an un-judged head to be missed when the user has clearly skipped it.
//
// This check is applied to all nested slider objects apart from the head (ticks, repeats, tail).
if (Tracking && !DrawableSlider.HeadCircle.Judged)
DrawableSlider.HeadCircle.MissForcefully();
// The player needs to have engaged in tracking at any point after the tail leniency cutoff.
// An actual tick miss should only occur if reaching the tick itself.
if (timeOffset >= SliderEventGenerator.TAIL_LENIENCY && Tracking)
if (Tracking)
ApplyResult(r => r.Type = r.Judgement.MaxResult);
else if (timeOffset > 0)
ApplyResult(r => r.Type = r.Judgement.MinResult);

View File

@ -75,9 +75,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
// shared implementation with DrawableSliderRepeat.
if (timeOffset >= 0)
{
// Attempt to preserve correct ordering of judgements as best we can by forcing
// an un-judged head to be missed when the user has clearly skipped it.
//
// This check is applied to all nested slider objects apart from the head (ticks, repeats, tail).
if (Tracking && !DrawableSlider.HeadCircle.Judged)
DrawableSlider.HeadCircle.MissForcefully();
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
}
protected override void UpdateInitialTransforms()
{

View File

@ -108,6 +108,28 @@ namespace osu.Game.Tests.Gameplay
AddAssert("gameplay clock time = 10000", () => gameplayClockContainer.CurrentTime, () => Is.EqualTo(10000).Within(10f));
}
[Test]
public void TestStopUsingBeatmapClock()
{
ClockBackedTestWorkingBeatmap working = null;
MasterGameplayClockContainer gameplayClockContainer = null;
BindableDouble frequencyAdjustment = new BindableDouble(2);
AddStep("create container", () =>
{
working = new ClockBackedTestWorkingBeatmap(new OsuRuleset().RulesetInfo, new FramedClock(new ManualClock()), Audio);
Child = gameplayClockContainer = new MasterGameplayClockContainer(working, 0);
gameplayClockContainer.Reset(startClock: true);
});
AddStep("apply frequency adjustment", () => gameplayClockContainer.AdjustmentsFromMods.AddAdjustment(AdjustableProperty.Frequency, frequencyAdjustment));
AddAssert("track frequency changed", () => working.Track.AggregateFrequency.Value, () => Is.EqualTo(2));
AddStep("stop using beatmap clock", () => gameplayClockContainer.StopUsingBeatmapClock());
AddAssert("frequency adjustment unapplied", () => working.Track.AggregateFrequency.Value, () => Is.EqualTo(1));
}
protected override void Dispose(bool isDisposing)
{
localConfig?.Dispose();

View File

@ -7,6 +7,7 @@ using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -16,6 +17,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Leaderboards;
@ -34,6 +36,7 @@ using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel;
using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Screens.Select.Options;
using osu.Game.Tests.Beatmaps.IO;
@ -165,6 +168,41 @@ namespace osu.Game.Tests.Visual.Navigation
ConfirmAtMainMenu();
}
[Test]
public void TestSongSelectScrollHandling()
{
TestPlaySongSelect songSelect = null;
double scrollPosition = 0;
AddStep("set game volume to max", () => Game.Dependencies.Get<FrameworkConfigManager>().SetValue(FrameworkSetting.VolumeUniversal, 1d));
AddUntilStep("wait for volume overlay to hide", () => Game.ChildrenOfType<VolumeOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
AddStep("store scroll position", () => scrollPosition = getCarouselScrollPosition());
AddStep("move to left side", () => InputManager.MoveMouseTo(
songSelect.ChildrenOfType<Screens.Select.SongSelect.LeftSideInteractionContainer>().Single().ScreenSpaceDrawQuad.TopLeft + new Vector2(1)));
AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1));
AddAssert("carousel didn't move", getCarouselScrollPosition, () => Is.EqualTo(scrollPosition));
AddRepeatStep("alt-scroll down", () =>
{
InputManager.PressKey(Key.AltLeft);
InputManager.ScrollVerticalBy(-1);
InputManager.ReleaseKey(Key.AltLeft);
}, 5);
AddAssert("game volume decreased", () => Game.Dependencies.Get<FrameworkConfigManager>().Get<double>(FrameworkSetting.VolumeUniversal), () => Is.LessThan(1));
AddStep("move to carousel", () => InputManager.MoveMouseTo(songSelect.ChildrenOfType<BeatmapCarousel>().Single()));
AddStep("scroll down", () => InputManager.ScrollVerticalBy(-1));
AddAssert("carousel moved", getCarouselScrollPosition, () => Is.Not.EqualTo(scrollPosition));
double getCarouselScrollPosition() => Game.ChildrenOfType<UserTrackingScrollContainer<DrawableCarouselItem>>().Single().Current;
}
/// <summary>
/// This tests that the F1 key will open the mod select overlay, and not be handled / blocked by the music controller (which has the same default binding
/// but should be handled *after* song select).

View File

@ -18,26 +18,24 @@ namespace osu.Game.Tests.Visual.UserInterface
[Cached]
private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Purple);
private readonly BindableDouble current = new BindableDouble(5)
{
Precision = 0.1f,
MinValue = 0,
MaxValue = 15
};
private RoundedSliderBar<double> slider = null!;
[BackgroundDependencyLoader]
private void load()
[SetUpSteps]
public void SetUpSteps()
{
Child = slider = new RoundedSliderBar<double>
AddStep("create slider", () => Child = slider = new RoundedSliderBar<double>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Current = current,
Current = new BindableDouble(5)
{
Precision = 0.1,
MinValue = 0,
MaxValue = 15
},
RelativeSizeAxes = Axes.X,
Width = 0.4f
};
});
}
[Test]
@ -55,5 +53,22 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("slider is default", () => slider.Current.IsDefault);
}
[Test]
public void TestNubDoubleClickOnDisabledSliderDoesNothing()
{
AddStep("set slider to 1", () => slider.Current.Value = 1);
AddStep("disable slider", () => slider.Current.Disabled = true);
AddStep("move mouse to nub", () => InputManager.MoveMouseTo(slider.ChildrenOfType<Nub>().Single()));
AddStep("double click nub", () =>
{
InputManager.Click(MouseButton.Left);
InputManager.Click(MouseButton.Left);
});
AddAssert("slider is still at 1", () => slider.Current.Value, () => Is.EqualTo(1));
}
}
}

View File

@ -18,26 +18,24 @@ namespace osu.Game.Tests.Visual.UserInterface
[Cached]
private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Purple);
private readonly BindableDouble current = new BindableDouble(5)
{
Precision = 0.1f,
MinValue = 0,
MaxValue = 15
};
private ShearedSliderBar<double> slider = null!;
[BackgroundDependencyLoader]
private void load()
[SetUpSteps]
public void SetUpSteps()
{
Child = slider = new ShearedSliderBar<double>
AddStep("create slider", () => Child = slider = new ShearedSliderBar<double>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Current = current,
Current = new BindableDouble(5)
{
Precision = 0.1,
MinValue = 0,
MaxValue = 15
},
RelativeSizeAxes = Axes.X,
Width = 0.4f
};
});
}
[Test]
@ -55,5 +53,22 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("slider is default", () => slider.Current.IsDefault);
}
[Test]
public void TestNubDoubleClickOnDisabledSliderDoesNothing()
{
AddStep("set slider to 1", () => slider.Current.Value = 1);
AddStep("disable slider", () => slider.Current.Disabled = true);
AddStep("move mouse to nub", () => InputManager.MoveMouseTo(slider.ChildrenOfType<ShearedNub>().Single()));
AddStep("double click nub", () =>
{
InputManager.Click(MouseButton.Left);
InputManager.Click(MouseButton.Left);
});
AddAssert("slider is still at 1", () => slider.Current.Value, () => Is.EqualTo(1));
}
}
}

View File

@ -10,7 +10,7 @@ namespace osu.Game.Tournament.Components
{
public partial class DateTextBox : SettingsTextBox
{
private readonly BindableWithCurrent<DateTimeOffset> current = new BindableWithCurrent<DateTimeOffset>();
private readonly BindableWithCurrent<DateTimeOffset> current = new BindableWithCurrent<DateTimeOffset>(DateTimeOffset.Now);
public new Bindable<DateTimeOffset>? Current
{

View File

@ -98,7 +98,11 @@ namespace osu.Game.Graphics.UserInterface
Origin = Anchor.TopCentre,
RelativePositionAxes = Axes.X,
Current = { Value = true },
OnDoubleClicked = () => Current.SetDefault(),
OnDoubleClicked = () =>
{
if (!Current.Disabled)
Current.SetDefault();
},
},
},
hoverClickSounds = new HoverClickSounds()

View File

@ -101,7 +101,11 @@ namespace osu.Game.Graphics.UserInterface
Origin = Anchor.TopCentre,
RelativePositionAxes = Axes.X,
Current = { Value = true },
OnDoubleClicked = () => Current.SetDefault(),
OnDoubleClicked = () =>
{
if (!Current.Disabled)
Current.SetDefault();
},
},
},
hoverClickSounds = new HoverClickSounds()

View File

@ -149,6 +149,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene),
new KeyBinding(InputKey.ExtraMouseButton2, GlobalAction.SkipCutscene),
new KeyBinding(InputKey.Tilde, GlobalAction.QuickRetry),
new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.QuickRetry),
new KeyBinding(new[] { InputKey.Control, InputKey.Tilde }, GlobalAction.QuickExit),
new KeyBinding(new[] { InputKey.F3 }, GlobalAction.DecreaseScrollSpeed),
new KeyBinding(new[] { InputKey.F4 }, GlobalAction.IncreaseScrollSpeed),

View File

@ -98,12 +98,6 @@ namespace osu.Game.Rulesets.Edit
}
});
if (DistanceSpacingMultiplier.Disabled)
{
distanceSpacingSlider.Hide();
return;
}
DistanceSpacingMultiplier.Value = editorBeatmap.BeatmapInfo.DistanceSpacing;
DistanceSpacingMultiplier.BindValueChanged(multiplier =>
{
@ -116,6 +110,8 @@ namespace osu.Game.Rulesets.Edit
editorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue;
}, true);
DistanceSpacingMultiplier.BindDisabledChanged(disabled => distanceSpacingSlider.Alpha = disabled ? 0 : 1, true);
// Manual binding to handle enabling distance spacing when the slider is interacted with.
distanceSpacingSlider.Current.BindValueChanged(spacing =>
{

View File

@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play
private readonly WorkingBeatmap beatmap;
private readonly Track track;
private Track track;
private readonly double skipTargetTime;
@ -145,7 +145,7 @@ namespace osu.Game.Screens.Play
protected override void StartGameplayClock()
{
addSourceClockAdjustments();
addAdjustmentsToTrack();
base.StartGameplayClock();
@ -186,20 +186,20 @@ namespace osu.Game.Screens.Play
/// </summary>
public void StopUsingBeatmapClock()
{
removeSourceClockAdjustments();
removeAdjustmentsFromTrack();
var virtualTrack = new TrackVirtual(beatmap.Track.Length);
virtualTrack.Seek(CurrentTime);
track = new TrackVirtual(beatmap.Track.Length);
track.Seek(CurrentTime);
if (IsRunning)
virtualTrack.Start();
ChangeSource(virtualTrack);
track.Start();
ChangeSource(track);
addSourceClockAdjustments();
addAdjustmentsToTrack();
}
private bool speedAdjustmentsApplied;
private void addSourceClockAdjustments()
private void addAdjustmentsToTrack()
{
if (speedAdjustmentsApplied)
return;
@ -213,7 +213,7 @@ namespace osu.Game.Screens.Play
speedAdjustmentsApplied = true;
}
private void removeSourceClockAdjustments()
private void removeAdjustmentsFromTrack()
{
if (!speedAdjustmentsApplied)
return;
@ -228,7 +228,7 @@ namespace osu.Game.Screens.Play
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
removeSourceClockAdjustments();
removeAdjustmentsFromTrack();
}
ControlPointInfo IBeatSyncProvider.ControlPoints => beatmap.Beatmap.ControlPointInfo;

View File

@ -1019,7 +1019,7 @@ namespace osu.Game.Screens.Select
/// <summary>
/// Handles mouse interactions required when moving away from the carousel.
/// </summary>
private partial class LeftSideInteractionContainer : Container
internal partial class LeftSideInteractionContainer : Container
{
private readonly Action? resetCarouselPosition;
@ -1028,7 +1028,10 @@ namespace osu.Game.Screens.Select
this.resetCarouselPosition = resetCarouselPosition;
}
protected override bool OnScroll(ScrollEvent e) => true;
// we want to block plain scrolls on the left side so that they don't scroll the carousel,
// but also we *don't* want to handle scrolls when they're combined with keyboard modifiers
// as those will usually correspond to other interactions like adjusting volume.
protected override bool OnScroll(ScrollEvent e) => !e.ControlPressed && !e.AltPressed && !e.ShiftPressed && !e.SuperPressed;
protected override bool OnMouseDown(MouseDownEvent e) => true;

View File

@ -39,7 +39,8 @@
<PackageReference Include="ppy.osu.Framework" Version="2023.1012.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.1023.0" />
<PackageReference Include="Sentry" Version="3.40.0" />
<PackageReference Include="SharpCompress" Version="0.34.1" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
<PackageReference Include="SharpCompress" Version="0.33.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.6" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />