1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 11:37:28 +08:00

Merge branch 'master' into better-mania-scrollspeed

This commit is contained in:
Dean Herbert 2020-04-03 23:25:14 +09:00 committed by GitHub
commit 3289fa7c66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 418 additions and 146 deletions

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RiderProjectSettingsUpdater">
<option name="vcsConfiguration" value="1" />
<option name="vcsConfiguration" value="2" />
</component>
</project>

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.402.1" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.402.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.403.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.403.0" />
</ItemGroup>
</Project>

View File

@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Catch
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap);
public override ISkin CreateLegacySkinProvider(ISkinSource source) => new CatchLegacySkinTransformer(source);
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source);
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new CatchPerformanceCalculator(this, beatmap, score);

View File

@ -384,7 +384,7 @@ namespace osu.Game.Rulesets.Catch.UI
}
currentCatcher.Show();
(currentCatcher.Drawable as IAnimation)?.GotoFrame(0);
(currentCatcher.Drawable as IFramedAnimation)?.GotoFrame(0);
}
private void beginTrail()

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, 0), _ => new DefaultColumnBackground())
{
RelativeSizeAxes = Axes.Both
}
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, 0), _ => new DefaultColumnBackground())
{
RelativeSizeAxes = Axes.Both
}

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Child = new ColumnHitObjectArea(new HitObjectContainer())
Child = new ColumnHitObjectArea(0, new HitObjectContainer())
{
RelativeSizeAxes = Axes.Both
}
@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Child = new ColumnHitObjectArea(new HitObjectContainer())
Child = new ColumnHitObjectArea(1, new HitObjectContainer())
{
RelativeSizeAxes = Axes.Both
}

View File

@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
CreatedDrawables.OfType<Container>().ForEach(c =>
{
c.Add(new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion),
c.Add(new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, 0),
_ => new DefaultHitExplosion((runcount / 15) % 2 == 0 ? new Color4(94, 0, 57, 255) : new Color4(6, 84, 0, 255), runcount % 6 != 0)
{
Anchor = Anchor.Centre,

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, 0), _ => new DefaultKeyArea())
{
RelativeSizeAxes = Axes.Both
},
@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, 1), _ => new DefaultKeyArea())
{
RelativeSizeAxes = Axes.Both
},

View File

@ -0,0 +1,52 @@
// 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.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
public class TestScenePlayfield : ManiaSkinnableTestScene
{
private List<StageDefinition> stageDefinitions = new List<StageDefinition>();
[Test]
public void TestSingleStage()
{
AddStep("create stage", () =>
{
stageDefinitions = new List<StageDefinition>
{
new StageDefinition { Columns = 2 }
};
SetContents(() => new ManiaPlayfield(stageDefinitions));
});
}
[Test]
public void TestDualStages()
{
AddStep("create stage", () =>
{
stageDefinitions = new List<StageDefinition>
{
new StageDefinition { Columns = 2 },
new StageDefinition { Columns = 2 }
};
SetContents(() => new ManiaPlayfield(stageDefinitions));
});
}
protected override IBeatmap CreateBeatmapForSkinProvider()
{
var maniaBeatmap = (ManiaBeatmap)base.CreateBeatmapForSkinProvider();
maniaBeatmap.Stages = stageDefinitions;
return maniaBeatmap;
}
}
}

View File

@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania
public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this);
public override ISkin CreateLegacySkinProvider(ISkinSource source) => new ManiaLegacySkinTransformer(source);
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new ManiaLegacySkinTransformer(source, beatmap);
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)
{

View File

@ -1,15 +1,28 @@
// 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.Rulesets.Mania.UI;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania
{
public class ManiaSkinComponent : GameplaySkinComponent<ManiaSkinComponents>
{
public ManiaSkinComponent(ManiaSkinComponents component)
/// <summary>
/// The intended <see cref="Column"/> index for this component.
/// May be null if the component does not exist in a <see cref="Column"/>.
/// </summary>
public readonly int? TargetColumn;
/// <summary>
/// Creates a new <see cref="ManiaSkinComponent"/>.
/// </summary>
/// <param name="component">The component.</param>
/// <param name="targetColumn">The intended <see cref="Column"/> index for this component. May be null if the component does not exist in a <see cref="Column"/>.</param>
public ManiaSkinComponent(ManiaSkinComponents component, int? targetColumn = null)
: base(component)
{
TargetColumn = targetColumn;
}
protected override string RulesetPrefix => ManiaRuleset.SHORT_NAME;

View File

@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
AddRangeInternal(new[]
{
bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody), _ => new DefaultBodyPiece())
bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece())
{
RelativeSizeAxes = Axes.X
},

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
AddInternal(headPiece = new SkinnableDrawable(new ManiaSkinComponent(Component), _ => new DefaultNotePiece())
AddInternal(headPiece = new SkinnableDrawable(new ManiaSkinComponent(Component, hitObject.Column), _ => new DefaultNotePiece())
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y

View File

@ -6,7 +6,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
@ -21,12 +20,6 @@ namespace osu.Game.Rulesets.Mania.Skinning
private Drawable sprite;
[Resolved(CanBeNull = true)]
private ManiaStage stage { get; set; }
[Resolved]
private Column column { get; set; }
public LegacyBodyPiece()
{
RelativeSizeAxes = Axes.Both;

View File

@ -18,12 +18,14 @@ namespace osu.Game.Rulesets.Mania.Skinning
public class LegacyColumnBackground : LegacyManiaColumnElement, IKeyBindingHandler<ManiaAction>
{
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private readonly bool isLastColumn;
private Container lightContainer;
private Sprite light;
public LegacyColumnBackground()
public LegacyColumnBackground(bool isLastColumn)
{
this.isLastColumn = isLastColumn;
RelativeSizeAxes = Axes.Both;
}
@ -40,10 +42,9 @@ namespace osu.Game.Rulesets.Mania.Skinning
bool hasLeftLine = leftLineWidth > 0;
bool hasRightLine = rightLineWidth > 0 && skin.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.4m
|| Stage == null || Column.Index == Stage.Columns.Count - 1;
|| isLastColumn;
float lightPosition = skin.GetConfig<LegacyManiaSkinConfigurationLookup, float>(
new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.LightPosition))?.Value
float lightPosition = GetManiaSkinConfig<float>(skin, LegacyManiaSkinConfigurationLookups.LightPosition)?.Value
?? 0;
Color4 lineColour = GetManiaSkinConfig<Color4>(skin, LegacyManiaSkinConfigurationLookups.ColumnLineColour)?.Value

View File

@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
// This animation is discarded and re-queried with the appropriate frame length afterwards.
var tmp = skin.GetAnimation(imageName, true, false);
double frameLength = 0;
if (tmp is IAnimation tmpAnimation && tmpAnimation.FrameCount > 0)
if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0)
frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount);
explosion = skin.GetAnimation(imageName, true, false, startAtCurrentTime: true, frameLength: frameLength).With(d =>

View File

@ -22,9 +22,6 @@ namespace osu.Game.Rulesets.Mania.Skinning
private Sprite upSprite;
private Sprite downSprite;
[Resolved(CanBeNull = true)]
private ManiaStage stage { get; set; }
[Resolved]
private Column column { get; set; }

View File

@ -1,11 +1,8 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Skinning
@ -15,10 +12,6 @@ namespace osu.Game.Rulesets.Mania.Skinning
/// </summary>
public class LegacyManiaElement : CompositeDrawable
{
[Resolved(CanBeNull = true)]
[CanBeNull]
protected ManiaStage Stage { get; private set; }
/// <summary>
/// Retrieve a per-column-count skin configuration.
/// </summary>
@ -26,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
/// <param name="lookup">The value to retrieve.</param>
/// <param name="index">If not null, denotes the index of the column to which the entry applies.</param>
protected virtual IBindable<T> GetManiaSkinConfig<T>(ISkin skin, LegacyManiaSkinConfigurationLookups lookup, int? index = null)
=> skin.GetConfig<LegacyManiaSkinConfigurationLookup, T>(
new LegacyManiaSkinConfigurationLookup(Stage?.Columns.Count ?? 4, lookup, index));
=> skin.GetConfig<ManiaSkinConfigurationLookup, T>(
new ManiaSkinConfigurationLookup(lookup, index));
}
}

View File

@ -8,6 +8,8 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Skinning
@ -15,6 +17,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
public class ManiaLegacySkinTransformer : ISkin
{
private readonly ISkin source;
private readonly ManiaBeatmap beatmap;
private Lazy<bool> isLegacySkin;
@ -24,9 +27,10 @@ namespace osu.Game.Rulesets.Mania.Skinning
/// </summary>
private Lazy<bool> hasKeyTexture;
public ManiaLegacySkinTransformer(ISkinSource source)
public ManiaLegacySkinTransformer(ISkinSource source, IBeatmap beatmap)
{
this.source = source;
this.beatmap = (ManiaBeatmap)beatmap;
source.SourceChanged += sourceChanged;
sourceChanged();
@ -36,8 +40,8 @@ namespace osu.Game.Rulesets.Mania.Skinning
{
isLegacySkin = new Lazy<bool>(() => source.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version) != null);
hasKeyTexture = new Lazy<bool>(() => source.GetAnimation(
source.GetConfig<LegacyManiaSkinConfigurationLookup, string>(
new LegacyManiaSkinConfigurationLookup(4, LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value
source.GetConfig<ManiaSkinConfigurationLookup, string>(
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value
?? "mania-key1", true, true) != null);
}
@ -55,7 +59,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
switch (maniaComponent.Component)
{
case ManiaSkinComponents.ColumnBackground:
return new LegacyColumnBackground();
return new LegacyColumnBackground(maniaComponent.TargetColumn == beatmap.TotalColumns - 1);
case ManiaSkinComponents.HitTarget:
return new LegacyHitTarget();
@ -115,7 +119,12 @@ namespace osu.Game.Rulesets.Mania.Skinning
public SampleChannel GetSample(ISampleInfo sample) => source.GetSample(sample);
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) =>
source.GetConfig<TLookup, TValue>(lookup);
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{
if (lookup is ManiaSkinConfigurationLookup maniaLookup)
return source.GetConfig<LegacyManiaSkinConfigurationLookup, TValue>(new LegacyManiaSkinConfigurationLookup(beatmap.TotalColumns, maniaLookup.Lookup, maniaLookup.TargetColumn));
return source.GetConfig<TLookup, TValue>(lookup);
}
}
}

View File

@ -0,0 +1,33 @@
// 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.Rulesets.Mania.UI;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Skinning
{
public class ManiaSkinConfigurationLookup
{
/// <summary>
/// The configuration lookup value.
/// </summary>
public readonly LegacyManiaSkinConfigurationLookups Lookup;
/// <summary>
/// The intended <see cref="Column"/> index for the configuration.
/// May be null if the configuration does not apply to a <see cref="Column"/>.
/// </summary>
public readonly int? TargetColumn;
/// <summary>
/// Creates a new <see cref="ManiaSkinConfigurationLookup"/>.
/// </summary>
/// <param name="lookup">The lookup value.</param>
/// <param name="targetColumn">The intended <see cref="Column"/> index for the configuration. May be null if the configuration does not apply to a <see cref="Column"/>.</param>
public ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups lookup, int? targetColumn = null)
{
Lookup = lookup;
TargetColumn = targetColumn;
}
}
}

View File

@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Y;
Drawable background = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground), _ => new DefaultColumnBackground())
Drawable background = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.ColumnBackground, Index), _ => new DefaultColumnBackground())
{
RelativeSizeAxes = Axes.Both
};
@ -52,8 +52,8 @@ namespace osu.Game.Rulesets.Mania.UI
{
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements
background.CreateProxy(),
hitObjectArea = new ColumnHitObjectArea(HitObjectContainer) { RelativeSizeAxes = Axes.Both },
new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea), _ => new DefaultKeyArea())
hitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both },
new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, Index), _ => new DefaultKeyArea())
{
RelativeSizeAxes = Axes.Both
},
@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.Mania.UI
if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
var explosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion), _ =>
var explosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, Index), _ =>
new DefaultHitExplosion(judgedObject.AccentColour.Value, judgedObject is DrawableHoldNoteTick))
{
RelativeSizeAxes = Axes.Both

View File

@ -14,12 +14,12 @@ namespace osu.Game.Rulesets.Mania.UI.Components
public readonly Container Explosions;
private readonly Drawable hitTarget;
public ColumnHitObjectArea(HitObjectContainer hitObjectContainer)
public ColumnHitObjectArea(int columnIndex, HitObjectContainer hitObjectContainer)
: base(hitObjectContainer)
{
AddRangeInternal(new[]
{
hitTarget = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitTarget), _ => new DefaultHitTarget())
hitTarget = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitTarget, columnIndex), _ => new DefaultHitTarget())
{
RelativeSizeAxes = Axes.X,
Depth = 1

View File

@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
@ -14,9 +15,6 @@ namespace osu.Game.Rulesets.Mania.UI.Components
{
protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
[Resolved(CanBeNull = true)]
private ManiaStage stage { get; set; }
public HitObjectArea(HitObjectContainer hitObjectContainer)
{
InternalChildren = new[]
@ -45,8 +43,8 @@ namespace osu.Game.Rulesets.Mania.UI.Components
protected virtual void UpdateHitPosition()
{
float hitPosition = CurrentSkin.GetConfig<LegacyManiaSkinConfigurationLookup, float>(
new LegacyManiaSkinConfigurationLookup(stage?.Columns.Count ?? 4, LegacyManiaSkinConfigurationLookups.HitPosition))?.Value
float hitPosition = CurrentSkin.GetConfig<ManiaSkinConfigurationLookup, float>(
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value
?? ManiaStage.HIT_TARGET_POSITION;
Padding = Direction.Value == ScrollingDirection.Up

View File

@ -8,6 +8,7 @@ using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Input.Handlers;
using osu.Game.Replays;
using osu.Game.Rulesets.Mania.Beatmaps;
@ -57,6 +58,18 @@ namespace osu.Game.Rulesets.Mania.UI
[BackgroundDependencyLoader]
private void load()
{
bool isForCurrentRuleset = Beatmap.BeatmapInfo.Ruleset.Equals(Ruleset.RulesetInfo);
foreach (var p in ControlPoints)
{
// Mania doesn't care about global velocity
p.Velocity = 1;
// For non-mania beatmap, speed changes should only happen through timing points
if (!isForCurrentRuleset)
p.DifficultyPoint = new DifficultyControlPoint();
}
BarLines.ForEach(Playfield.Add);
Config.BindWith(ManiaRulesetSetting.ScrollDirection, configDirection);

View File

@ -11,6 +11,7 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
@ -24,7 +25,6 @@ namespace osu.Game.Rulesets.Mania.UI
/// <summary>
/// A collection of <see cref="Column"/>s.
/// </summary>
[Cached]
public class ManiaStage : ScrollingPlayfield
{
public const float COLUMN_SPACING = 1;
@ -146,15 +146,15 @@ namespace osu.Game.Rulesets.Mania.UI
{
if (col.Index > 0)
{
float spacing = currentSkin.GetConfig<LegacyManiaSkinConfigurationLookup, float>(
new LegacyManiaSkinConfigurationLookup(Columns.Count, LegacyManiaSkinConfigurationLookups.ColumnSpacing, col.Index - 1))
float spacing = currentSkin.GetConfig<ManiaSkinConfigurationLookup, float>(
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, col.Index - 1))
?.Value ?? COLUMN_SPACING;
col.Margin = new MarginPadding { Left = spacing };
}
float? width = currentSkin.GetConfig<LegacyManiaSkinConfigurationLookup, float>(
new LegacyManiaSkinConfigurationLookup(Columns.Count, LegacyManiaSkinConfigurationLookups.ColumnWidth, col.Index))
float? width = currentSkin.GetConfig<ManiaSkinConfigurationLookup, float>(
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, col.Index))
?.Value;
if (width == null)

View File

@ -179,7 +179,7 @@ namespace osu.Game.Rulesets.Osu
public override RulesetSettingsSubsection CreateSettings() => new OsuSettingsSubsection(this);
public override ISkin CreateLegacySkinProvider(ISkinSource source) => new OsuLegacySkinTransformer(source);
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new OsuLegacySkinTransformer(source);
public int LegacyID => 0;

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -4,31 +4,28 @@
using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
using osuTK;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Taiko.Audio;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests
{
[TestFixture]
public class TestSceneInputDrum : OsuTestScene
public class TestSceneInputDrum : SkinnableTestScene
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(InputDrum),
typeof(DrumSampleMapping),
typeof(HitSampleInfo),
typeof(SampleControlPoint)
};
public TestSceneInputDrum()
[BackgroundDependencyLoader]
private void load()
{
Add(new TaikoInputManager(new RulesetInfo { ID = 1 })
SetContents(() => new TaikoInputManager(new RulesetInfo { ID = 1 })
{
RelativeSizeAxes = Axes.Both,
Child = new Container

View File

@ -0,0 +1,144 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Taiko.Audio;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Skinning
{
/// <summary>
/// A component of the playfield that captures input and displays input as a drum.
/// </summary>
internal class LegacyInputDrum : Container
{
public LegacyInputDrum()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
Children = new Drawable[]
{
new Sprite
{
Texture = skin.GetTexture("taiko-bar-left")
},
new LegacyHalfDrum(false)
{
Name = "Left Half",
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
RimAction = TaikoAction.LeftRim,
CentreAction = TaikoAction.LeftCentre
},
new LegacyHalfDrum(true)
{
Name = "Right Half",
Anchor = Anchor.TopRight,
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Scale = new Vector2(-1, 1),
RimAction = TaikoAction.RightRim,
CentreAction = TaikoAction.RightCentre
}
};
}
/// <summary>
/// A half-drum. Contains one centre and one rim hit.
/// </summary>
private class LegacyHalfDrum : Container, IKeyBindingHandler<TaikoAction>
{
/// <summary>
/// The key to be used for the rim of the half-drum.
/// </summary>
public TaikoAction RimAction;
/// <summary>
/// The key to be used for the centre of the half-drum.
/// </summary>
public TaikoAction CentreAction;
private readonly Sprite rimHit;
private readonly Sprite centreHit;
[Resolved]
private DrumSampleMapping sampleMappings { get; set; }
public LegacyHalfDrum(bool flipped)
{
Masking = true;
Children = new Drawable[]
{
rimHit = new Sprite
{
Anchor = flipped ? Anchor.CentreRight : Anchor.CentreLeft,
Origin = flipped ? Anchor.CentreLeft : Anchor.CentreRight,
Scale = new Vector2(-1, 1),
Alpha = 0,
},
centreHit = new Sprite
{
Anchor = flipped ? Anchor.CentreRight : Anchor.CentreLeft,
Origin = flipped ? Anchor.CentreRight : Anchor.CentreLeft,
Alpha = 0,
}
};
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
rimHit.Texture = skin.GetTexture(@"taiko-drum-outer");
centreHit.Texture = skin.GetTexture(@"taiko-drum-inner");
}
public bool OnPressed(TaikoAction action)
{
Drawable target = null;
var drumSample = sampleMappings.SampleAt(Time.Current);
if (action == CentreAction)
{
target = centreHit;
drumSample.Centre?.Play();
}
else if (action == RimAction)
{
target = rimHit;
drumSample.Rim?.Play();
}
if (target != null)
{
const float alpha_amount = 1;
const float down_time = 80;
const float up_time = 50;
target.Animate(
t => t.FadeTo(Math.Min(target.Alpha + alpha_amount, 1), down_time)
).Then(
t => t.FadeOut(up_time)
);
}
return false;
}
public void OnReleased(TaikoAction action)
{
}
}
}
}

View File

@ -20,7 +20,22 @@ namespace osu.Game.Rulesets.Taiko.Skinning
this.source = source;
}
public Drawable GetDrawableComponent(ISkinComponent component) => source.GetDrawableComponent(component);
public Drawable GetDrawableComponent(ISkinComponent component)
{
if (!(component is TaikoSkinComponent taikoComponent))
return null;
switch (taikoComponent.Component)
{
case TaikoSkinComponents.InputDrum:
if (GetTexture("taiko-bar-left") != null)
return new LegacyInputDrum();
return null;
}
return source.GetDrawableComponent(component);
}
public Texture GetTexture(string componentName) => source.GetTexture(componentName);

View File

@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap, this);
public override ISkin CreateLegacySkinProvider(ISkinSource source) => new TaikoLegacySkinTransformer(source);
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new TaikoLegacySkinTransformer(source);
public const string SHORT_NAME = "taiko";

View File

@ -5,5 +5,6 @@ namespace osu.Game.Rulesets.Taiko
{
public enum TaikoSkinComponents
{
InputDrum,
}
}

View File

@ -12,6 +12,7 @@ using osu.Framework.Input.Bindings;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Rulesets.Taiko.Audio;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.UI
{
@ -22,11 +23,12 @@ namespace osu.Game.Rulesets.Taiko.UI
{
private const float middle_split = 0.025f;
private readonly ControlPointInfo controlPoints;
[Cached]
private DrumSampleMapping sampleMapping;
public InputDrum(ControlPointInfo controlPoints)
{
this.controlPoints = controlPoints;
sampleMapping = new DrumSampleMapping(controlPoints);
RelativeSizeAxes = Axes.Both;
FillMode = FillMode.Fit;
@ -35,35 +37,37 @@ namespace osu.Game.Rulesets.Taiko.UI
[BackgroundDependencyLoader]
private void load()
{
var sampleMappings = new DrumSampleMapping(controlPoints);
Children = new Drawable[]
Child = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.InputDrum), _ => new Container
{
new TaikoHalfDrum(false, sampleMappings)
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
Name = "Left Half",
Anchor = Anchor.Centre,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.X,
X = -middle_split / 2,
RimAction = TaikoAction.LeftRim,
CentreAction = TaikoAction.LeftCentre
},
new TaikoHalfDrum(true, sampleMappings)
{
Name = "Right Half",
Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.X,
X = middle_split / 2,
RimAction = TaikoAction.RightRim,
CentreAction = TaikoAction.RightCentre
new TaikoHalfDrum(false)
{
Name = "Left Half",
Anchor = Anchor.Centre,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.X,
X = -middle_split / 2,
RimAction = TaikoAction.LeftRim,
CentreAction = TaikoAction.LeftCentre
},
new TaikoHalfDrum(true)
{
Name = "Right Half",
Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.X,
X = middle_split / 2,
RimAction = TaikoAction.RightRim,
CentreAction = TaikoAction.RightCentre
}
}
};
});
AddRangeInternal(sampleMappings.Sounds);
AddRangeInternal(sampleMapping.Sounds);
}
/// <summary>
@ -86,12 +90,11 @@ namespace osu.Game.Rulesets.Taiko.UI
private readonly Sprite centre;
private readonly Sprite centreHit;
private readonly DrumSampleMapping sampleMappings;
[Resolved]
private DrumSampleMapping sampleMappings { get; set; }
public TaikoHalfDrum(bool flipped, DrumSampleMapping sampleMappings)
public TaikoHalfDrum(bool flipped)
{
this.sampleMappings = sampleMappings;
Masking = true;
Children = new Drawable[]

View File

@ -22,7 +22,6 @@ namespace osu.Game.Tests.Visual.Menus
{
typeof(StartupScreen),
typeof(IntroScreen),
typeof(OsuScreen),
typeof(IntroTestScene),
};

View File

@ -16,7 +16,7 @@ namespace osu.Game.Tournament.Components
{
private readonly string filename;
private readonly bool drawFallbackGradient;
private VideoSprite video;
private Video video;
private ManualClock manualClock;
@ -33,7 +33,7 @@ namespace osu.Game.Tournament.Components
if (stream != null)
{
InternalChild = video = new VideoSprite(stream, false)
InternalChild = video = new Video(stream, false)
{
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,

View File

@ -102,7 +102,7 @@ namespace osu.Game.Rulesets
public ModAutoplay GetAutoplayMod() => GetAllMods().OfType<ModAutoplay>().First();
public virtual ISkin CreateLegacySkinProvider(ISkinSource source) => null;
public virtual ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => null;
protected Ruleset()
{

View File

@ -76,11 +76,9 @@ namespace osu.Game.Rulesets.UI.Scrolling
protected virtual bool RelativeScaleBeatLengths => false;
/// <summary>
/// Provides the default <see cref="MultiplierControlPoint"/>s that adjust the scrolling rate of <see cref="HitObject"/>s
/// inside this <see cref="DrawableRuleset{TObject}"/>.
/// The <see cref="MultiplierControlPoint"/>s that adjust the scrolling rate of <see cref="HitObject"/>s inside this <see cref="DrawableRuleset{TObject}"/>.
/// </summary>
/// <returns></returns>
private readonly SortedList<MultiplierControlPoint> controlPoints = new SortedList<MultiplierControlPoint>(Comparer<MultiplierControlPoint>.Default);
protected readonly SortedList<MultiplierControlPoint> ControlPoints = new SortedList<MultiplierControlPoint>(Comparer<MultiplierControlPoint>.Default);
protected IScrollingInfo ScrollingInfo => scrollingInfo;
@ -97,11 +95,11 @@ namespace osu.Game.Rulesets.UI.Scrolling
switch (VisualisationMethod)
{
case ScrollVisualisationMethod.Sequential:
scrollingInfo.Algorithm = new SequentialScrollAlgorithm(controlPoints);
scrollingInfo.Algorithm = new SequentialScrollAlgorithm(ControlPoints);
break;
case ScrollVisualisationMethod.Overlapping:
scrollingInfo.Algorithm = new OverlappingScrollAlgorithm(controlPoints);
scrollingInfo.Algorithm = new OverlappingScrollAlgorithm(ControlPoints);
break;
case ScrollVisualisationMethod.Constant:
@ -170,10 +168,18 @@ namespace osu.Game.Rulesets.UI.Scrolling
// Collapse sections with the same start time
.GroupBy(s => s.StartTime).Select(g => g.Last()).OrderBy(s => s.StartTime);
controlPoints.AddRange(timingChanges);
ControlPoints.AddRange(timingChanges);
if (controlPoints.Count == 0)
controlPoints.Add(new MultiplierControlPoint { Velocity = Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier });
if (ControlPoints.Count == 0)
ControlPoints.Add(new MultiplierControlPoint { Velocity = Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier });
}
protected override void LoadComplete()
{
base.LoadComplete();
if (!(Playfield is ScrollingPlayfield))
throw new ArgumentException($"{nameof(Playfield)} must be a {nameof(ScrollingPlayfield)} when using {nameof(DrawableScrollingRuleset<TObject>)}.");
}
protected override void LoadComplete()

View File

@ -25,7 +25,7 @@ namespace osu.Game.Screens.Edit.Compose
// the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation
// full access to all skin sources.
var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider));
var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider, EditorBeatmap.PlayableBeatmap));
// load the skinning hierarchy first.
// this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.

View File

@ -270,10 +270,9 @@ namespace osu.Game.Screens.Menu
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new VideoSprite(videoStream, false)
InternalChild = new Video(videoStream, false)
{
RelativeSizeAxes = Axes.Both,
Clock = new FramedOffsetClock(Clock) { Offset = -logo_1 }
};
}
}

View File

@ -176,7 +176,7 @@ namespace osu.Game.Screens.Play
dependencies.CacheAs(gameplayBeatmap);
addUnderlayComponents(GameplayClockContainer);
addGameplayComponents(GameplayClockContainer, Beatmap.Value);
addGameplayComponents(GameplayClockContainer, Beatmap.Value, playableBeatmap);
addOverlayComponents(GameplayClockContainer, Beatmap.Value);
DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true);
@ -214,13 +214,13 @@ namespace osu.Game.Screens.Play
target.Add(DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both });
}
private void addGameplayComponents(Container target, WorkingBeatmap working)
private void addGameplayComponents(Container target, WorkingBeatmap working, IBeatmap playableBeatmap)
{
var beatmapSkinProvider = new BeatmapSkinProvidingContainer(working.Skin);
// the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation
// full access to all skin sources.
var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider));
var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider, playableBeatmap));
// load the skinning hierarchy first.
// this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources.

View File

@ -8,7 +8,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Timing;
namespace osu.Game.Skinning
{
@ -28,7 +27,7 @@ namespace osu.Game.Skinning
var animation = new SkinnableTextureAnimation(startAtCurrentTime)
{
DefaultFrameLength = frameLength ?? getFrameLength(source, applyConfigFrameRate, textures),
Repeat = looping,
Loop = looping,
};
foreach (var t in textures)
@ -71,7 +70,10 @@ namespace osu.Game.Skinning
base.LoadComplete();
if (timeReference != null)
Clock = new FramedOffsetClock(timeReference.Clock) { Offset = -timeReference.AnimationStartTime };
{
Clock = timeReference.Clock;
PlaybackPosition = timeReference.AnimationStartTime - timeReference.Clock.CurrentTime;
}
}
}

View File

@ -108,7 +108,7 @@ namespace osu.Game.Storyboards.Drawables
Animation = animation;
Origin = animation.Origin;
Position = animation.InitialPosition;
Repeat = animation.LoopType == AnimationLoopType.LoopForever;
Loop = animation.LoopType == AnimationLoopType.LoopForever;
LifetimeStart = animation.StartTime;
LifetimeEnd = animation.EndTime;

View File

@ -8,7 +8,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
using osu.Framework.Graphics.Video;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
namespace osu.Game.Storyboards.Drawables
@ -16,7 +15,7 @@ namespace osu.Game.Storyboards.Drawables
public class DrawableStoryboardVideo : CompositeDrawable
{
public readonly StoryboardVideo Video;
private VideoSprite videoSprite;
private Video video;
public override bool RemoveWhenNotAlive => false;
@ -40,14 +39,14 @@ namespace osu.Game.Storyboards.Drawables
if (stream == null)
return;
InternalChild = videoSprite = new VideoSprite(stream, false)
InternalChild = video = new Video(stream, false)
{
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0,
Clock = new FramedOffsetClock(Clock) { Offset = -Video.StartTime }
PlaybackPosition = Video.StartTime
};
}
@ -55,10 +54,10 @@ namespace osu.Game.Storyboards.Drawables
{
base.LoadComplete();
if (videoSprite == null) return;
if (video == null) return;
using (videoSprite.BeginAbsoluteSequence(0))
videoSprite.FadeIn(500);
using (video.BeginAbsoluteSequence(0))
video.FadeIn(500);
}
}
}

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.Skinning;
using osuTK;
@ -47,16 +48,18 @@ namespace osu.Game.Tests.Visual
{
createdDrawables.Clear();
Cell(0).Child = createProvider(null, creationFunction);
Cell(1).Child = createProvider(metricsSkin, creationFunction);
Cell(2).Child = createProvider(defaultSkin, creationFunction);
Cell(3).Child = createProvider(specialSkin, creationFunction);
Cell(4).Child = createProvider(oldSkin, creationFunction);
var beatmap = CreateBeatmapForSkinProvider();
Cell(0).Child = createProvider(null, creationFunction, beatmap);
Cell(1).Child = createProvider(metricsSkin, creationFunction, beatmap);
Cell(2).Child = createProvider(defaultSkin, creationFunction, beatmap);
Cell(3).Child = createProvider(specialSkin, creationFunction, beatmap);
Cell(4).Child = createProvider(oldSkin, creationFunction, beatmap);
}
protected IEnumerable<Drawable> CreatedDrawables => createdDrawables;
private Drawable createProvider(Skin skin, Func<Drawable> creationFunction)
private Drawable createProvider(Skin skin, Func<Drawable> creationFunction, IBeatmap beatmap)
{
var created = creationFunction();
createdDrawables.Add(created);
@ -100,7 +103,7 @@ namespace osu.Game.Tests.Visual
{
new OutlineBox { Alpha = autoSize ? 1 : 0 },
mainProvider.WithChild(
new SkinProvidingContainer(Ruleset.Value.CreateInstance().CreateLegacySkinProvider(mainProvider))
new SkinProvidingContainer(Ruleset.Value.CreateInstance().CreateLegacySkinProvider(mainProvider, beatmap))
{
Child = created,
RelativeSizeAxes = !autoSize ? Axes.Both : Axes.None,
@ -113,6 +116,8 @@ namespace osu.Game.Tests.Visual
};
}
protected virtual IBeatmap CreateBeatmapForSkinProvider() => CreateWorkingBeatmap(Ruleset.Value).GetPlayableBeatmap(Ruleset.Value);
private class OutlineBox : CompositeDrawable
{
public OutlineBox()

View File

@ -22,8 +22,8 @@
<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.Game.Resources" Version="2020.402.1" />
<PackageReference Include="ppy.osu.Framework" Version="2020.402.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.403.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.403.0" />
<PackageReference Include="Sentry" Version="2.1.1" />
<PackageReference Include="SharpCompress" Version="0.24.0" />
<PackageReference Include="NUnit" Version="3.12.0" />

View File

@ -70,8 +70,8 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.402.1" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.402.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.403.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.403.0" />
</ItemGroup>
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
<ItemGroup Label="Transitive Dependencies">
@ -79,7 +79,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.402.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.403.0" />
<PackageReference Include="SharpCompress" Version="0.24.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />