Merge branch 'master' into sharpen

This commit is contained in:
Dan Balasescu 2019-11-27 17:09:11 +09:00 committed by GitHub
commit c6a85a11bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 106 additions and 82 deletions

View File

@ -54,6 +54,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.1122.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2019.1126.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -47,55 +47,53 @@ namespace osu.Game.Rulesets.Catch.Difficulty
return 0; return 0;
// We are heavily relying on aim in catch the beat // We are heavily relying on aim in catch the beat
double value = Math.Pow(5.0f * Math.Max(1.0f, Attributes.StarRating / 0.0049f) - 4.0f, 2.0f) / 100000.0f; double value = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0;
// Longer maps are worth more. "Longer" means how many hits there are which can contribute to combo // Longer maps are worth more. "Longer" means how many hits there are which can contribute to combo
int numTotalHits = totalComboHits(); int numTotalHits = totalComboHits();
// Longer maps are worth more // Longer maps are worth more
float lengthBonus = double lengthBonus =
0.95f + 0.4f * Math.Min(1.0f, numTotalHits / 3000.0f) + 0.95 + 0.4 * Math.Min(1.0, numTotalHits / 3000.0) +
(numTotalHits > 3000 ? MathF.Log10(numTotalHits / 3000.0f) * 0.5f : 0.0f); (numTotalHits > 3000 ? Math.Log10(numTotalHits / 3000.0) * 0.5 : 0.0);
// Longer maps are worth more // Longer maps are worth more
value *= lengthBonus; value *= lengthBonus;
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
value *= Math.Pow(0.97f, misses); value *= Math.Pow(0.97, misses);
// Combo scaling // Combo scaling
float beatmapMaxCombo = Attributes.MaxCombo; if (Attributes.MaxCombo > 0)
if (beatmapMaxCombo > 0) value *= Math.Min(Math.Pow(Attributes.MaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
value *= Math.Min(Math.Pow(Attributes.MaxCombo, 0.8f) / Math.Pow(beatmapMaxCombo, 0.8f), 1.0f);
float approachRate = (float)Attributes.ApproachRate; double approachRateFactor = 1.0;
float approachRateFactor = 1.0f; if (Attributes.ApproachRate > 9.0)
if (approachRate > 9.0f) approachRateFactor += 0.1 * (Attributes.ApproachRate - 9.0); // 10% for each AR above 9
approachRateFactor += 0.1f * (approachRate - 9.0f); // 10% for each AR above 9 else if (Attributes.ApproachRate < 8.0)
else if (approachRate < 8.0f) approachRateFactor += 0.025 * (8.0 - Attributes.ApproachRate); // 2.5% for each AR below 8
approachRateFactor += 0.025f * (8.0f - approachRate); // 2.5% for each AR below 8
value *= approachRateFactor; value *= approachRateFactor;
if (mods.Any(m => m is ModHidden)) if (mods.Any(m => m is ModHidden))
// Hiddens gives nothing on max approach rate, and more the lower it is // Hiddens gives nothing on max approach rate, and more the lower it is
value *= 1.05f + 0.075f * (10.0f - Math.Min(10.0f, approachRate)); // 7.5% for each AR below 10 value *= 1.05 + 0.075 * (10.0 - Math.Min(10.0, Attributes.ApproachRate)); // 7.5% for each AR below 10
if (mods.Any(m => m is ModFlashlight)) if (mods.Any(m => m is ModFlashlight))
// Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps. // Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps.
value *= 1.35f * lengthBonus; value *= 1.35 * lengthBonus;
// Scale the aim value with accuracy _slightly_ // Scale the aim value with accuracy _slightly_
value *= Math.Pow(accuracy(), 5.5f); value *= Math.Pow(accuracy(), 5.5);
// Custom multipliers for NoFail. SpunOut is not applicable. // Custom multipliers for NoFail. SpunOut is not applicable.
if (mods.Any(m => m is ModNoFail)) if (mods.Any(m => m is ModNoFail))
value *= 0.90f; value *= 0.90;
return value; return value;
} }
private float accuracy() => totalHits() == 0 ? 0 : Math.Clamp((float)totalSuccessfulHits() / totalHits(), 0f, 1f); private float accuracy() => totalHits() == 0 ? 0 : Math.Clamp((float)totalSuccessfulHits() / totalHits(), 0, 1);
private int totalHits() => tinyTicksHit + ticksHit + fruitsHit + misses + tinyTicksMissed; private int totalHits() => tinyTicksHit + ticksHit + fruitsHit + misses + tinyTicksMissed;
private int totalSuccessfulHits() => tinyTicksHit + ticksHit + fruitsHit; private int totalSuccessfulHits() => tinyTicksHit + ticksHit + fruitsHit;
private int totalComboHits() => misses + ticksHit + fruitsHit; private int totalComboHits() => misses + ticksHit + fruitsHit;

View File

@ -55,22 +55,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return 0; return 0;
// Custom multipliers for NoFail and SpunOut. // Custom multipliers for NoFail and SpunOut.
double multiplier = 1.12f; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
if (mods.Any(m => m is OsuModNoFail)) if (mods.Any(m => m is OsuModNoFail))
multiplier *= 0.90f; multiplier *= 0.90;
if (mods.Any(m => m is OsuModSpunOut)) if (mods.Any(m => m is OsuModSpunOut))
multiplier *= 0.95f; multiplier *= 0.95;
double aimValue = computeAimValue(); double aimValue = computeAimValue();
double speedValue = computeSpeedValue(); double speedValue = computeSpeedValue();
double accuracyValue = computeAccuracyValue(); double accuracyValue = computeAccuracyValue();
double totalValue = double totalValue =
Math.Pow( Math.Pow(
Math.Pow(aimValue, 1.1f) + Math.Pow(aimValue, 1.1) +
Math.Pow(speedValue, 1.1f) + Math.Pow(speedValue, 1.1) +
Math.Pow(accuracyValue, 1.1f), 1.0f / 1.1f Math.Pow(accuracyValue, 1.1), 1.0 / 1.1
) * multiplier; ) * multiplier;
if (categoryRatings != null) if (categoryRatings != null)
@ -93,82 +93,82 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (mods.Any(m => m is OsuModTouchDevice)) if (mods.Any(m => m is OsuModTouchDevice))
rawAim = Math.Pow(rawAim, 0.8); rawAim = Math.Pow(rawAim, 0.8);
double aimValue = Math.Pow(5.0f * Math.Max(1.0f, rawAim / 0.0675f) - 4.0f, 3.0f) / 100000.0f; double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0;
// Longer maps are worth more // Longer maps are worth more
double lengthBonus = 0.95f + 0.4f * Math.Min(1.0f, totalHits / 2000.0f) + double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0f) * 0.5f : 0.0f); (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
aimValue *= lengthBonus; aimValue *= lengthBonus;
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
aimValue *= Math.Pow(0.97f, countMiss); aimValue *= Math.Pow(0.97, countMiss);
// Combo scaling // Combo scaling
if (beatmapMaxCombo > 0) if (beatmapMaxCombo > 0)
aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8f) / Math.Pow(beatmapMaxCombo, 0.8f), 1.0f); aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(beatmapMaxCombo, 0.8), 1.0);
double approachRateFactor = 1.0f; double approachRateFactor = 1.0;
if (Attributes.ApproachRate > 10.33f) if (Attributes.ApproachRate > 10.33)
approachRateFactor += 0.3f * (Attributes.ApproachRate - 10.33f); approachRateFactor += 0.3 * (Attributes.ApproachRate - 10.33);
else if (Attributes.ApproachRate < 8.0f) else if (Attributes.ApproachRate < 8.0)
{ {
approachRateFactor += 0.01f * (8.0f - Attributes.ApproachRate); approachRateFactor += 0.01 * (8.0 - Attributes.ApproachRate);
} }
aimValue *= approachRateFactor; aimValue *= approachRateFactor;
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
if (mods.Any(h => h is OsuModHidden)) if (mods.Any(h => h is OsuModHidden))
aimValue *= 1.0f + 0.04f * (12.0f - Attributes.ApproachRate); aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
if (mods.Any(h => h is OsuModFlashlight)) if (mods.Any(h => h is OsuModFlashlight))
{ {
// Apply object-based bonus for flashlight. // Apply object-based bonus for flashlight.
aimValue *= 1.0f + 0.35f * Math.Min(1.0f, totalHits / 200.0f) + aimValue *= 1.0 + 0.35 * Math.Min(1.0, totalHits / 200.0) +
(totalHits > 200 (totalHits > 200
? 0.3f * Math.Min(1.0f, (totalHits - 200) / 300.0f) + ? 0.3 * Math.Min(1.0, (totalHits - 200) / 300.0) +
(totalHits > 500 ? (totalHits - 500) / 1200.0f : 0.0f) (totalHits > 500 ? (totalHits - 500) / 1200.0 : 0.0)
: 0.0f); : 0.0);
} }
// Scale the aim value with accuracy _slightly_ // Scale the aim value with accuracy _slightly_
aimValue *= 0.5f + accuracy / 2.0f; aimValue *= 0.5 + accuracy / 2.0;
// It is important to also consider accuracy difficulty when doing that // It is important to also consider accuracy difficulty when doing that
aimValue *= 0.98f + Math.Pow(Attributes.OverallDifficulty, 2) / 2500; aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;
return aimValue; return aimValue;
} }
private double computeSpeedValue() private double computeSpeedValue()
{ {
double speedValue = Math.Pow(5.0f * Math.Max(1.0f, Attributes.SpeedStrain / 0.0675f) - 4.0f, 3.0f) / 100000.0f; double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedStrain / 0.0675) - 4.0, 3.0) / 100000.0;
// Longer maps are worth more // Longer maps are worth more
speedValue *= 0.95f + 0.4f * Math.Min(1.0f, totalHits / 2000.0f) + speedValue *= 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0f) * 0.5f : 0.0f); (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
speedValue *= Math.Pow(0.97f, countMiss); speedValue *= Math.Pow(0.97, countMiss);
// Combo scaling // Combo scaling
if (beatmapMaxCombo > 0) if (beatmapMaxCombo > 0)
speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8f) / Math.Pow(beatmapMaxCombo, 0.8f), 1.0f); speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(beatmapMaxCombo, 0.8), 1.0);
double approachRateFactor = 1.0f; double approachRateFactor = 1.0;
if (Attributes.ApproachRate > 10.33f) if (Attributes.ApproachRate > 10.33)
approachRateFactor += 0.3f * (Attributes.ApproachRate - 10.33f); approachRateFactor += 0.3 * (Attributes.ApproachRate - 10.33);
speedValue *= approachRateFactor; speedValue *= approachRateFactor;
if (mods.Any(m => m is OsuModHidden)) if (mods.Any(m => m is OsuModHidden))
speedValue *= 1.0f + 0.04f * (12.0f - Attributes.ApproachRate); speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
// Scale the speed value with accuracy _slightly_ // Scale the speed value with accuracy _slightly_
speedValue *= 0.02f + accuracy; speedValue *= 0.02 + accuracy;
// It is important to also consider accuracy difficulty when doing that // It is important to also consider accuracy difficulty when doing that
speedValue *= 0.96f + Math.Pow(Attributes.OverallDifficulty, 2) / 1600; speedValue *= 0.96 + Math.Pow(Attributes.OverallDifficulty, 2) / 1600;
return speedValue; return speedValue;
} }
@ -190,15 +190,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Lots of arbitrary values from testing. // Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
double accuracyValue = Math.Pow(1.52163f, Attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83f; double accuracyValue = Math.Pow(1.52163, Attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer // Bonus for many hitcircles - it's harder to keep good accuracy up for longer
accuracyValue *= Math.Min(1.15f, Math.Pow(amountHitObjectsWithAccuracy / 1000.0f, 0.3f)); accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));
if (mods.Any(m => m is OsuModHidden)) if (mods.Any(m => m is OsuModHidden))
accuracyValue *= 1.08f; accuracyValue *= 1.08;
if (mods.Any(m => m is OsuModFlashlight)) if (mods.Any(m => m is OsuModFlashlight))
accuracyValue *= 1.02f; accuracyValue *= 1.02;
return accuracyValue; return accuracyValue;
} }

View File

@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
double strainValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0; double strainValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0;
// Longer maps are worth more // Longer maps are worth more
double lengthBonus = 1 + 0.1f * Math.Min(1.0, totalHits / 1500.0); double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
strainValue *= lengthBonus; strainValue *= lengthBonus;
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available

View File

@ -95,6 +95,19 @@ namespace osu.Game.Tests.Visual.Gameplay
seekAndAssertBreak("seek to break after end", testBreaks[1].EndTime + 500, false); seekAndAssertBreak("seek to break after end", testBreaks[1].EndTime + 500, false);
} }
[TestCase(true)]
[TestCase(false)]
public void TestBeforeGameplayStart(bool withBreaks)
{
setClock(true);
if (withBreaks)
loadBreaksStep("multiple breaks", testBreaks);
seekAndAssertBreak("seek to break intro time", -100, true);
seekAndAssertBreak("seek to break intro time", 0, false);
}
private void addShowBreakStep(double seconds) private void addShowBreakStep(double seconds)
{ {
AddStep($"show '{seconds}s' break", () => breakOverlay.Breaks = new List<BreakPeriod> AddStep($"show '{seconds}s' break", () => breakOverlay.Breaks = new List<BreakPeriod>

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.Collections.Generic; using System.Collections.Generic;
@ -16,6 +16,8 @@ namespace osu.Game.Screens.Play
{ {
public class BreakOverlay : Container public class BreakOverlay : Container
{ {
private readonly ScoreProcessor scoreProcessor;
/// <summary> /// <summary>
/// The duration of the break overlay fading. /// The duration of the break overlay fading.
/// </summary> /// </summary>
@ -60,9 +62,12 @@ namespace osu.Game.Screens.Play
private readonly RemainingTimeCounter remainingTimeCounter; private readonly RemainingTimeCounter remainingTimeCounter;
private readonly BreakInfo info; private readonly BreakInfo info;
private readonly BreakArrows breakArrows; private readonly BreakArrows breakArrows;
private readonly double gameplayStartTime;
public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor = null) public BreakOverlay(bool letterboxing, double gameplayStartTime = 0, ScoreProcessor scoreProcessor = null)
{ {
this.gameplayStartTime = gameplayStartTime;
this.scoreProcessor = scoreProcessor;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
Child = fadeContainer = new Container Child = fadeContainer = new Container
{ {
@ -135,26 +140,34 @@ namespace osu.Game.Screens.Play
updateBreakTimeBindable(); updateBreakTimeBindable();
} }
private void updateBreakTimeBindable() private void updateBreakTimeBindable() =>
isBreakTime.Value = getCurrentBreak()?.HasEffect == true
|| Clock.CurrentTime < gameplayStartTime
|| scoreProcessor?.HasCompleted == true;
private BreakPeriod getCurrentBreak()
{ {
if (breaks == null || breaks.Count == 0) if (breaks?.Count > 0)
return;
var time = Clock.CurrentTime;
if (time > breaks[CurrentBreakIndex].EndTime)
{ {
while (time > breaks[CurrentBreakIndex].EndTime && CurrentBreakIndex < breaks.Count - 1) var time = Clock.CurrentTime;
CurrentBreakIndex++;
} if (time > breaks[CurrentBreakIndex].EndTime)
else {
{ while (time > breaks[CurrentBreakIndex].EndTime && CurrentBreakIndex < breaks.Count - 1)
while (time < breaks[CurrentBreakIndex].StartTime && CurrentBreakIndex > 0) CurrentBreakIndex++;
CurrentBreakIndex--; }
else
{
while (time < breaks[CurrentBreakIndex].StartTime && CurrentBreakIndex > 0)
CurrentBreakIndex--;
}
var closest = breaks[CurrentBreakIndex];
return closest.Contains(time) ? closest : null;
} }
var currentBreak = breaks[CurrentBreakIndex]; return null;
isBreakTime.Value = currentBreak.HasEffect && currentBreak.Contains(time);
} }
private void initializeBreaks() private void initializeBreaks()

View File

@ -179,7 +179,7 @@ namespace osu.Game.Screens.Play
{ {
target.AddRange(new[] target.AddRange(new[]
{ {
breakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) breakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, DrawableRuleset.GameplayStartTime, ScoreProcessor)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -468,7 +468,7 @@ namespace osu.Game.Screens.Play
PauseOverlay.Hide(); PauseOverlay.Hide();
// breaks and time-based conditions may allow instant resume. // breaks and time-based conditions may allow instant resume.
if (breakOverlay.IsBreakTime.Value || GameplayClockContainer.GameplayClock.CurrentTime < Beatmap.Value.Beatmap.HitObjects.First().StartTime) if (breakOverlay.IsBreakTime.Value)
completeResume(); completeResume();
else else
DrawableRuleset.RequestResume(completeResume); DrawableRuleset.RequestResume(completeResume);

View File

@ -21,7 +21,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.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<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.1122.0" /> <PackageReference Include="ppy.osu.Framework" Version="2019.1126.0" />
<PackageReference Include="Sentry" Version="1.2.0" /> <PackageReference Include="Sentry" Version="1.2.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" />

View File

@ -74,7 +74,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<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.iOS" Version="2019.1122.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2019.1126.0" />
</ItemGroup> </ItemGroup>
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. --> <!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
@ -82,7 +82,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.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2019.1122.0" /> <PackageReference Include="ppy.osu.Framework" Version="2019.1126.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" />