Merge remote-tracking branch 'upstream/master' into refactor-osd

This commit is contained in:
Dean Herbert
2019-08-13 11:35:54 +09:00
15 changed files with 285 additions and 28 deletions

View File

@ -62,7 +62,7 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.731.1" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.809.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2019.809.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2019.809.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -2,13 +2,19 @@
// 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;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.StateChanges;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModAutopilot : Mod public class OsuModAutopilot : Mod, IApplicableFailOverride, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
{ {
public override string Name => "Autopilot"; public override string Name => "Autopilot";
public override string Acronym => "AP"; public override string Acronym => "AP";
@ -17,5 +23,40 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => @"Automatic cursor movement - just follow the rhythm."; public override string Description => @"Automatic cursor movement - just follow the rhythm.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail), typeof(ModAutoplay) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail), typeof(ModAutoplay) };
public bool AllowFail => false;
private OsuInputManager inputManager;
private List<OsuReplayFrame> replayFrames;
private int currentFrame;
public void Update(Playfield playfield)
{
if (currentFrame == replayFrames.Count - 1) return;
double time = playfield.Time.Current;
// Very naive implementation of autopilot based on proximity to replay frames.
// TODO: this needs to be based on user interactions to better match stable (pausing until judgement is registered).
if (Math.Abs(replayFrames[currentFrame + 1].Time - time) <= Math.Abs(replayFrames[currentFrame].Time - time))
{
currentFrame++;
new MousePositionAbsoluteInput { Position = playfield.ToScreenSpace(replayFrames[currentFrame].Position) }.Apply(inputManager.CurrentState, inputManager);
}
// TODO: Implement the functionality to automatically spin spinners
}
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
// Grab the input manager to disable the user's cursor, and for future use
inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager;
inputManager.AllowUserCursorMovement = false;
// Generate the replay frames the cursor should follow
replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap).Generate().Frames.Cast<OsuReplayFrame>().ToList();
}
} }
} }

View File

@ -18,6 +18,12 @@ namespace osu.Game.Rulesets.Osu
set => ((OsuKeyBindingContainer)KeyBindingContainer).AllowUserPresses = value; set => ((OsuKeyBindingContainer)KeyBindingContainer).AllowUserPresses = value;
} }
/// <summary>
/// Whether the user's cursor movement events should be accepted.
/// Can be used to block only movement while still accepting button input.
/// </summary>
public bool AllowUserCursorMovement { get; set; } = true;
protected override RulesetKeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) protected override RulesetKeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
=> new OsuKeyBindingContainer(ruleset, variant, unique); => new OsuKeyBindingContainer(ruleset, variant, unique);
@ -26,6 +32,13 @@ namespace osu.Game.Rulesets.Osu
{ {
} }
protected override bool Handle(UIEvent e)
{
if (e is MouseMoveEvent && !AllowUserCursorMovement) return false;
return base.Handle(e);
}
private class OsuKeyBindingContainer : RulesetKeyBindingContainer private class OsuKeyBindingContainer : RulesetKeyBindingContainer
{ {
public bool AllowUserPresses = true; public bool AllowUserPresses = true;

View File

@ -119,14 +119,14 @@ namespace osu.Game.Tests.Visual.Background
{ {
performFullSetup(); performFullSetup();
createFakeStoryboard(); createFakeStoryboard();
AddStep("Storyboard Enabled", () => AddStep("Enable Storyboard", () =>
{ {
player.ReplacesBackground.Value = true; player.ReplacesBackground.Value = true;
player.StoryboardEnabled.Value = true; player.StoryboardEnabled.Value = true;
}); });
waitForDim(); waitForDim();
AddAssert("Background is invisible, storyboard is visible", () => songSelect.IsBackgroundInvisible() && player.IsStoryboardVisible); AddAssert("Background is invisible, storyboard is visible", () => songSelect.IsBackgroundInvisible() && player.IsStoryboardVisible);
AddStep("Storyboard Disabled", () => AddStep("Disable Storyboard", () =>
{ {
player.ReplacesBackground.Value = false; player.ReplacesBackground.Value = false;
player.StoryboardEnabled.Value = false; player.StoryboardEnabled.Value = false;
@ -149,22 +149,44 @@ namespace osu.Game.Tests.Visual.Background
} }
/// <summary> /// <summary>
/// Check if the <see cref="UserDimContainer"/> is properly accepting user-defined visual changes at all. /// Ensure <see cref="UserDimContainer"/> is properly accepting user-defined visual changes for a background.
/// </summary> /// </summary>
[Test] [Test]
public void DisableUserDimTest() public void DisableUserDimBackgroundTest()
{ {
performFullSetup(); performFullSetup();
waitForDim(); waitForDim();
AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddStep("EnableUserDim disabled", () => songSelect.DimEnabled.Value = false); AddStep("Enable user dim", () => songSelect.DimEnabled.Value = false);
waitForDim(); waitForDim();
AddAssert("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsUserBlurDisabled()); AddAssert("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsUserBlurDisabled());
AddStep("EnableUserDim enabled", () => songSelect.DimEnabled.Value = true); AddStep("Disable user dim", () => songSelect.DimEnabled.Value = true);
waitForDim(); waitForDim();
AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
} }
/// <summary>
/// Ensure <see cref="UserDimContainer"/> is properly accepting user-defined visual changes for a storyboard.
/// </summary>
[Test]
public void DisableUserDimStoryboardTest()
{
performFullSetup();
createFakeStoryboard();
AddStep("Enable Storyboard", () =>
{
player.ReplacesBackground.Value = true;
player.StoryboardEnabled.Value = true;
});
AddStep("Enable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = true);
AddStep("Set dim level to 1", () => songSelect.DimLevel.Value = 1f);
waitForDim();
AddAssert("Storyboard is invisible", () => !player.IsStoryboardVisible);
AddStep("Disable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = false);
waitForDim();
AddAssert("Storyboard is visible", () => player.IsStoryboardVisible);
}
/// <summary> /// <summary>
/// Check if the visual settings container retains dim and blur when pausing /// Check if the visual settings container retains dim and blur when pausing
/// </summary> /// </summary>

View File

@ -0,0 +1,36 @@
// 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.Game.Overlays.BeatmapSet;
using System;
using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Bindables;
using osu.Game.Screens.Select.Leaderboards;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneLeaderboardScopeSelector : OsuTestScene
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(LeaderboardScopeSelector),
};
public TestSceneLeaderboardScopeSelector()
{
Bindable<BeatmapLeaderboardScope> scope = new Bindable<BeatmapLeaderboardScope>();
Add(new LeaderboardScopeSelector
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Current = { BindTarget = scope }
});
AddStep(@"Select global", () => scope.Value = BeatmapLeaderboardScope.Global);
AddStep(@"Select country", () => scope.Value = BeatmapLeaderboardScope.Country);
AddStep(@"Select friend", () => scope.Value = BeatmapLeaderboardScope.Friend);
}
}
}

View File

@ -82,14 +82,13 @@ namespace osu.Game.Tests.Visual.UserInterface
var easierMods = instance.GetModsFor(ModType.DifficultyReduction); var easierMods = instance.GetModsFor(ModType.DifficultyReduction);
var harderMods = instance.GetModsFor(ModType.DifficultyIncrease); var harderMods = instance.GetModsFor(ModType.DifficultyIncrease);
var assistMods = instance.GetModsFor(ModType.Automation);
var noFailMod = easierMods.FirstOrDefault(m => m is OsuModNoFail); var noFailMod = easierMods.FirstOrDefault(m => m is OsuModNoFail);
var hiddenMod = harderMods.FirstOrDefault(m => m is OsuModHidden); var hiddenMod = harderMods.FirstOrDefault(m => m is OsuModHidden);
var doubleTimeMod = harderMods.OfType<MultiMod>().FirstOrDefault(m => m.Mods.Any(a => a is OsuModDoubleTime)); var doubleTimeMod = harderMods.OfType<MultiMod>().FirstOrDefault(m => m.Mods.Any(a => a is OsuModDoubleTime));
var autoPilotMod = assistMods.FirstOrDefault(m => m is OsuModAutopilot); var spunOutMod = easierMods.FirstOrDefault(m => m is OsuModSpunOut);
var easy = easierMods.FirstOrDefault(m => m is OsuModEasy); var easy = easierMods.FirstOrDefault(m => m is OsuModEasy);
var hardRock = harderMods.FirstOrDefault(m => m is OsuModHardRock); var hardRock = harderMods.FirstOrDefault(m => m is OsuModHardRock);
@ -101,7 +100,7 @@ namespace osu.Game.Tests.Visual.UserInterface
testMultiplierTextColour(noFailMod, modSelect.LowMultiplierColour); testMultiplierTextColour(noFailMod, modSelect.LowMultiplierColour);
testMultiplierTextColour(hiddenMod, modSelect.HighMultiplierColour); testMultiplierTextColour(hiddenMod, modSelect.HighMultiplierColour);
testUnimplementedMod(autoPilotMod); testUnimplementedMod(spunOutMod);
} }
[Test] [Test]

View File

@ -6,7 +6,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Configuration; using osu.Game.Configuration;
using osuTK.Graphics;
namespace osu.Game.Graphics.Containers namespace osu.Game.Graphics.Containers
{ {
@ -36,6 +35,8 @@ namespace osu.Game.Graphics.Containers
protected Bindable<bool> ShowStoryboard { get; private set; } protected Bindable<bool> ShowStoryboard { get; private set; }
protected double DimLevel => EnableUserDim.Value ? UserDimLevel.Value : 0;
protected override Container<Drawable> Content => dimContent; protected override Container<Drawable> Content => dimContent;
private Container dimContent { get; } private Container dimContent { get; }
@ -78,8 +79,8 @@ namespace osu.Game.Graphics.Containers
{ {
ContentDisplayed = ShowDimContent; ContentDisplayed = ShowDimContent;
dimContent.FadeTo((ContentDisplayed) ? 1 : 0, BACKGROUND_FADE_DURATION, Easing.OutQuint); dimContent.FadeTo(ContentDisplayed ? 1 : 0, BACKGROUND_FADE_DURATION, Easing.OutQuint);
dimContent.FadeColour(EnableUserDim.Value ? OsuColour.Gray(1 - (float)UserDimLevel.Value) : Color4.White, BACKGROUND_FADE_DURATION, Easing.OutQuint); dimContent.FadeColour(OsuColour.Gray(1 - (float)DimLevel), BACKGROUND_FADE_DURATION, Easing.OutQuint);
} }
} }
} }

View File

@ -31,6 +31,11 @@ namespace osu.Game.Graphics.UserInterface
protected virtual float StripWidth() => TabContainer.Children.Sum(c => c.IsPresent ? c.DrawWidth + TabContainer.Spacing.X : 0) - TabContainer.Spacing.X; protected virtual float StripWidth() => TabContainer.Children.Sum(c => c.IsPresent ? c.DrawWidth + TabContainer.Spacing.X : 0) - TabContainer.Spacing.X;
protected virtual float StripHeight() => 1; protected virtual float StripHeight() => 1;
/// <summary>
/// Whether entries should be automatically populated if <see cref="T"/> is an <see cref="Enum"/> type.
/// </summary>
protected virtual bool AddEnumEntriesAutomatically => true;
private static bool isEnumType => typeof(T).IsEnum; private static bool isEnumType => typeof(T).IsEnum;
public OsuTabControl() public OsuTabControl()
@ -45,7 +50,7 @@ namespace osu.Game.Graphics.UserInterface
Colour = Color4.White.Opacity(0), Colour = Color4.White.Opacity(0),
}); });
if (isEnumType) if (isEnumType && AddEnumEntriesAutomatically)
foreach (var val in (T[])Enum.GetValues(typeof(T))) foreach (var val in (T[])Enum.GetValues(typeof(T)))
AddItem(val); AddItem(val);
} }

View File

@ -24,7 +24,13 @@ namespace osu.Game.Graphics.UserInterface
Height = 30; Height = 30;
} }
public class PageTabItem : TabItem<T> [BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AccentColour = colours.Yellow;
}
public class PageTabItem : TabItem<T>, IHasAccentColour
{ {
private const float transition_duration = 100; private const float transition_duration = 100;
@ -32,6 +38,18 @@ namespace osu.Game.Graphics.UserInterface
protected readonly SpriteText Text; protected readonly SpriteText Text;
private Color4 accentColour;
public Color4 AccentColour
{
get => accentColour;
set
{
accentColour = value;
box.Colour = accentColour;
}
}
public PageTabItem(T value) public PageTabItem(T value)
: base(value) : base(value)
{ {
@ -63,12 +81,6 @@ namespace osu.Game.Graphics.UserInterface
Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Exo, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true); Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Exo, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true);
} }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
box.Colour = colours.Yellow;
}
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
if (!Active.Value) if (!Active.Value)

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 osu.Framework.Graphics.UserInterface;
using osu.Game.Screens.Select.Leaderboards;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osuTK;
using osu.Game.Graphics.UserInterface;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Framework.Allocation;
using osuTK.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Input.Events;
namespace osu.Game.Overlays.BeatmapSet
{
public class LeaderboardScopeSelector : PageTabControl<BeatmapLeaderboardScope>
{
protected override bool AddEnumEntriesAutomatically => false;
protected override Dropdown<BeatmapLeaderboardScope> CreateDropdown() => null;
protected override TabItem<BeatmapLeaderboardScope> CreateTabItem(BeatmapLeaderboardScope value) => new ScopeSelectorTabItem(value);
public LeaderboardScopeSelector()
{
RelativeSizeAxes = Axes.X;
AddItem(BeatmapLeaderboardScope.Global);
AddItem(BeatmapLeaderboardScope.Country);
AddItem(BeatmapLeaderboardScope.Friend);
AddInternal(new GradientLine
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
});
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AccentColour = colours.Blue;
}
protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(20, 0),
};
private class ScopeSelectorTabItem : PageTabItem
{
public ScopeSelectorTabItem(BeatmapLeaderboardScope value)
: base(value)
{
Text.Font = OsuFont.GetFont(size: 16);
}
protected override bool OnHover(HoverEvent e)
{
Text.FadeColour(AccentColour);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
Text.FadeColour(Color4.White);
}
}
private class GradientLine : GridContainer
{
public GradientLine()
{
RelativeSizeAxes = Axes.X;
Size = new Vector2(0.8f, 1.5f);
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(mode: GridSizeMode.Relative, size: 0.4f),
new Dimension(),
};
Content = new[]
{
new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(Color4.Transparent, Color4.Gray),
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Gray,
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(Color4.Gray, Color4.Transparent),
},
}
};
}
}
}
}

View File

@ -398,7 +398,7 @@ namespace osu.Game.Rulesets.Scoring
if (rollingMaxBaseScore != 0) if (rollingMaxBaseScore != 0)
Accuracy.Value = baseScore / rollingMaxBaseScore; Accuracy.Value = baseScore / rollingMaxBaseScore;
TotalScore.Value = getScore(Mode.Value) * scoreMultiplier; TotalScore.Value = getScore(Mode.Value);
} }
private double getScore(ScoringMode mode) private double getScore(ScoringMode mode)
@ -407,11 +407,11 @@ namespace osu.Game.Rulesets.Scoring
{ {
default: default:
case ScoringMode.Standardised: case ScoringMode.Standardised:
return max_score * (base_portion * baseScore / maxBaseScore + combo_portion * HighestCombo.Value / maxHighestCombo) + bonusScore; return (max_score * (base_portion * baseScore / maxBaseScore + combo_portion * HighestCombo.Value / maxHighestCombo) + bonusScore) * scoreMultiplier;
case ScoringMode.Classic: case ScoringMode.Classic:
// should emulate osu-stable's scoring as closely as we can (https://osu.ppy.sh/help/wiki/Score/ScoreV1) // should emulate osu-stable's scoring as closely as we can (https://osu.ppy.sh/help/wiki/Score/ScoreV1)
return bonusScore + baseScore * (1 + Math.Max(0, HighestCombo.Value - 1) / 25); return bonusScore + baseScore * ((1 + Math.Max(0, HighestCombo.Value - 1) * scoreMultiplier) / 25);
} }
} }

View File

@ -33,7 +33,7 @@ namespace osu.Game.Screens.Play
base.LoadComplete(); base.LoadComplete();
} }
protected override bool ShowDimContent => ShowStoryboard.Value && UserDimLevel.Value < 1; protected override bool ShowDimContent => ShowStoryboard.Value && DimLevel < 1;
private void initializeStoryboard(bool async) private void initializeStoryboard(bool async)
{ {

View File

@ -1,13 +1,22 @@
// 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.ComponentModel;
namespace osu.Game.Screens.Select.Leaderboards namespace osu.Game.Screens.Select.Leaderboards
{ {
public enum BeatmapLeaderboardScope public enum BeatmapLeaderboardScope
{ {
[Description("Local Ranking")]
Local, Local,
[Description("Country Ranking")]
Country, Country,
[Description("Global Ranking")]
Global, Global,
[Description("Friend Ranking")]
Friend, Friend,
} }
} }

View File

@ -14,7 +14,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<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.731.1" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.809.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.809.0" /> <PackageReference Include="ppy.osu.Framework" Version="2019.809.0" />
<PackageReference Include="SharpCompress" Version="0.23.0" /> <PackageReference Include="SharpCompress" Version="0.23.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />

View File

@ -104,7 +104,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.1" />
<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.731.1" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2019.809.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.809.0" /> <PackageReference Include="ppy.osu.Framework" Version="2019.809.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2019.809.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2019.809.0" />
<PackageReference Include="SharpCompress" Version="0.22.0" /> <PackageReference Include="SharpCompress" Version="0.22.0" />