1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-14 03:25:11 +08:00

Merge branch 'master' of https://github.com/ppy/osu into mod-fl2

Changes how OsuModBlinds passes the playfield and how DrawableOsuBlinds
handles the restrictTo parameter.
This commit is contained in:
WebFreak001 2018-10-27 16:34:48 +02:00
commit 3b08ad0742
89 changed files with 1016 additions and 577 deletions

View File

@ -1,30 +0,0 @@
clone_depth: 1
version: '{build}'
skip_non_tags: true
image: Visual Studio 2017
install:
- git clone https://github.com/ppy/osu-deploy
before_build:
- ps: if($env:appveyor_repo_tag -eq 'True') { Update-AppveyorBuild -Version $env:appveyor_repo_tag_name }
- cmd: git submodule update --init --recursive --depth=5
- cmd: nuget restore -verbosity quiet
build_script:
- ps: iex ((New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/appveyor/secure-file/master/install.ps1'))
- appveyor DownloadFile https://puu.sh/BCrS8/7faccf7876.enc # signing certificate
- cmd: appveyor-tools\secure-file -decrypt 7faccf7876.enc -secret %decode_secret% -out %HOMEPATH%\deanherbert.pfx
- appveyor DownloadFile https://puu.sh/A6g75/fdc6f19b04.enc # deploy configuration
- cd osu-deploy
- nuget restore -verbosity quiet
- msbuild osu.Desktop.Deploy.csproj
- cmd: ..\appveyor-tools\secure-file -decrypt ..\fdc6f19b04.enc -secret %decode_secret% -out bin\Debug\netcoreapp2.1\osu.Desktop.Deploy.dll.config
- dotnet bin/Debug/netcoreapp2.1/osu.Desktop.Deploy.dll %code_signing_password% %APPVEYOR_REPO_TAG_NAME%
environment:
decode_secret:
secure: i67IC2xj6DjjxmA6Oj2jing3+MwzLkq6CbGsjfZ7rdY=
code_signing_password:
secure: 34tLNqvjmmZEi97MLKfrnQ==
artifacts:
- path: 'osu-deploy/releases/*'
deploy:
- provider: Environment
name: github

View File

@ -27,7 +27,7 @@
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="System.IO.Packaging" Version="4.5.0" />
<PackageReference Include="ppy.squirrel.windows" Version="1.8.0.8" />
<PackageReference Include="ppy.squirrel.windows" Version="1.9.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.1.4" />
</ItemGroup>

View File

@ -1,7 +1,6 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
@ -38,7 +37,7 @@ namespace osu.Game.Rulesets.Catch.Tests
beatmap.HitObjects.Add(new JuiceStream
{
X = 0.5f - width / 2,
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(width * CatchPlayfield.BASE_WIDTH, 0)

View File

@ -5,6 +5,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -146,7 +146,7 @@ namespace osu.Game.Rulesets.Catch.Objects
public SliderCurve Curve { get; } = new SliderCurve();
public List<Vector2> ControlPoints
public Vector2[] ControlPoints
{
get { return Curve.ControlPoints; }
set { Curve.ControlPoints = value; }

View File

@ -11,6 +11,7 @@ using osu.Game.Rulesets.Catch.Objects.Drawable;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using OpenTK;
namespace osu.Game.Rulesets.Catch.UI
{
@ -18,9 +19,6 @@ namespace osu.Game.Rulesets.Catch.UI
{
public const float BASE_WIDTH = 512;
protected override Container<Drawable> Content => content;
private readonly Container<Drawable> content;
private readonly CatcherArea catcherArea;
protected override bool UserScrollSpeedAdjustment => false;
@ -28,7 +26,6 @@ namespace osu.Game.Rulesets.Catch.UI
protected override SpeedChangeVisualisationMethod VisualisationMethod => SpeedChangeVisualisationMethod.Constant;
public CatchPlayfield(BeatmapDifficulty difficulty, Func<CatchHitObject, DrawableHitObject<CatchHitObject>> getVisualRepresentation)
: base(BASE_WIDTH)
{
Direction.Value = ScrollingDirection.Down;
@ -37,27 +34,27 @@ namespace osu.Game.Rulesets.Catch.UI
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
base.Content.Anchor = Anchor.BottomLeft;
base.Content.Origin = Anchor.BottomLeft;
Size = new Vector2(0.86f); // matches stable's vertical offset for catcher plate
base.Content.AddRange(new Drawable[]
InternalChild = new PlayfieldAdjustmentContainer
{
explodingFruitContainer = new Container
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
},
catcherArea = new CatcherArea(difficulty)
{
GetVisualRepresentation = getVisualRepresentation,
ExplodingFruitTarget = explodingFruitContainer,
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopLeft,
},
content = new Container<Drawable>
{
RelativeSizeAxes = Axes.Both,
},
});
explodingFruitContainer = new Container
{
RelativeSizeAxes = Axes.Both,
},
catcherArea = new CatcherArea(difficulty)
{
GetVisualRepresentation = getVisualRepresentation,
ExplodingFruitTarget = explodingFruitContainer,
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopLeft,
},
HitObjectContainer
}
};
VisibleTimeRange.Value = BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
}

View File

@ -13,7 +13,6 @@ using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using OpenTK;
namespace osu.Game.Rulesets.Catch.UI
{
@ -32,8 +31,6 @@ namespace osu.Game.Rulesets.Catch.UI
public override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo);
protected override Vector2 PlayfieldArea => new Vector2(0.86f); // matches stable's vertical offset for catcher plate
protected override DrawableHitObject<CatchHitObject> GetVisualRepresentation(CatchHitObject h)
{
switch (h)

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.UI
{
public class CatcherArea : Container
{
public const float CATCHER_SIZE = 84;
public const float CATCHER_SIZE = 100;
protected readonly Catcher MovableCatcher;

View File

@ -0,0 +1,42 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using OpenTK;
namespace osu.Game.Rulesets.Catch.UI
{
public class PlayfieldAdjustmentContainer : Container
{
protected override Container<Drawable> Content => content;
private readonly Container content;
public PlayfieldAdjustmentContainer()
{
InternalChild = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
FillAspectRatio = 4f / 3,
Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both }
};
}
/// <summary>
/// A <see cref="Container"/> which scales its content relative to a target width.
/// </summary>
private class ScalingContainer : Container
{
protected override void Update()
{
base.Update();
Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.BASE_WIDTH);
Size = Vector2.Divide(Vector2.One, Scale);
}
}
}
}

View File

@ -2,9 +2,10 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
TargetColumns = (int)Math.Max(1, roundedCircleSize);
else
{
float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasEndTime) / beatmap.HitObjects.Count();
float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasEndTime) / beatmap.HitObjects.Count;
if (percentSliderOrSpinner < 0.2)
TargetColumns = 7;
else if (percentSliderOrSpinner < 0.3 || roundedCircleSize >= 5)

View File

@ -112,7 +112,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
drainTime = 10000;
BeatmapDifficulty difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty;
conversionDifficulty = ((difficulty.DrainRate + MathHelper.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count() / drainTime * 9f) / 38f * 5f / 1.15;
conversionDifficulty = ((difficulty.DrainRate + MathHelper.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15;
conversionDifficulty = Math.Min(conversionDifficulty.Value, 12);
return conversionDifficulty.Value;

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
{
base.InitialiseDefaults();
Set(ManiaSetting.ScrollTime, 1500.0, 50.0, 10000.0, 50.0);
Set(ManiaSetting.ScrollTime, 2250.0, 50.0, 10000.0, 50.0);
Set(ManiaSetting.ScrollDirection, ManiaScrollingDirection.Down);
}

View File

@ -20,8 +20,7 @@ namespace osu.Game.Rulesets.Mania.Edit
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = Vector2.One
};
protected override Vector2 PlayfieldArea => Vector2.One;
}
}

View File

@ -30,8 +30,6 @@ namespace osu.Game.Rulesets.Mania.UI
internal readonly Container TopLevelContainer;
private readonly Container explosionContainer;
protected override Container<Drawable> Content => hitObjectArea;
public Column()
{
RelativeSizeAxes = Axes.Y;
@ -54,7 +52,10 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
hitObjectArea = new ColumnHitObjectArea { RelativeSizeAxes = Axes.Both },
hitObjectArea = new ColumnHitObjectArea(HitObjectContainer)
{
RelativeSizeAxes = Axes.Both,
},
explosionContainer = new Container
{
Name = "Hit explosions",

View File

@ -8,28 +8,24 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using OpenTK.Graphics;
namespace osu.Game.Rulesets.Mania.UI.Components
{
public class ColumnHitObjectArea : Container, IHasAccentColour
public class ColumnHitObjectArea : CompositeDrawable, IHasAccentColour
{
private const float hit_target_height = 10;
private const float hit_target_bar_height = 2;
private Container<Drawable> content;
protected override Container<Drawable> Content => content;
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private Container hitTargetLine;
private readonly Container hitTargetLine;
private readonly Drawable hitTargetBar;
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
public ColumnHitObjectArea(HitObjectContainer hitObjectContainer)
{
Drawable hitTargetBar;
InternalChildren = new[]
{
hitTargetBar = new Box
@ -45,13 +41,13 @@ namespace osu.Game.Rulesets.Mania.UI.Components
Masking = true,
Child = new Box { RelativeSizeAxes = Axes.Both }
},
content = new Container
{
Name = "Hit objects",
RelativeSizeAxes = Axes.Both,
},
hitObjectContainer
};
}
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(direction =>
{

View File

@ -10,6 +10,7 @@ using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using OpenTK;
namespace osu.Game.Rulesets.Mania.UI
{
@ -25,6 +26,8 @@ namespace osu.Game.Rulesets.Mania.UI
if (stageDefinitions.Count <= 0)
throw new ArgumentException("Can't have zero or fewer stages.");
Size = new Vector2(1, 0.8f);
GridContainer playfieldGrid;
AddInternal(playfieldGrid = new GridContainer
{

View File

@ -24,7 +24,6 @@ using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using OpenTK;
namespace osu.Game.Rulesets.Mania.UI
{
@ -110,8 +109,6 @@ namespace osu.Game.Rulesets.Mania.UI
}
}
protected override Vector2 PlayfieldArea => new Vector2(1, 0.8f);
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay);
}
}

View File

@ -7,7 +7,7 @@ using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.UI
{
public class ManiaScrollingPlayfield : ScrollingPlayfield
public abstract class ManiaScrollingPlayfield : ScrollingPlayfield
{
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();

View File

@ -30,8 +30,7 @@ namespace osu.Game.Rulesets.Mania.UI
public IReadOnlyList<Column> Columns => columnFlow.Children;
private readonly FillFlowContainer<Column> columnFlow;
protected override Container<Drawable> Content => barLineContainer;
private readonly Container<Drawable> barLineContainer;
private readonly Container barLineContainer;
public Container<DrawableManiaJudgement> Judgements => judgements;
private readonly JudgementContainer<DrawableManiaJudgement> judgements;
@ -105,6 +104,7 @@ namespace osu.Game.Rulesets.Mania.UI
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y,
Child = HitObjectContainer
}
},
judgements = new JudgementContainer<DrawableManiaJudgement>

View File

@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + 1000,
Position = new Vector2(239, 176),
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(154, 28),
@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + 1000,
Position = new Vector2(-(distance / 2), 0),
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(distance, 0),
@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + 1000,
Position = new Vector2(-200, 0),
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(200, 200),
@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Osu.Tests
CurveType = CurveType.Linear,
StartTime = Time.Current + 1000,
Position = new Vector2(-200, 0),
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(150, 75),
@ -210,7 +210,7 @@ namespace osu.Game.Rulesets.Osu.Tests
CurveType = CurveType.Bezier,
StartTime = Time.Current + 1000,
Position = new Vector2(-200, 0),
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(150, 75),
@ -235,7 +235,7 @@ namespace osu.Game.Rulesets.Osu.Tests
CurveType = CurveType.Linear,
StartTime = Time.Current + 1000,
Position = new Vector2(0, 0),
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(-200, 0),
@ -265,7 +265,7 @@ namespace osu.Game.Rulesets.Osu.Tests
StartTime = Time.Current + 1000,
Position = new Vector2(-100, 0),
CurveType = CurveType.Catmull,
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(50, -50),

View File

@ -2,9 +2,10 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -43,7 +43,10 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
Position = positionData?.Position ?? Vector2.Zero,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
LegacyLastTickOffset = legacyOffset?.LegacyLastTickOffset
LegacyLastTickOffset = legacyOffset?.LegacyLastTickOffset,
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / beatmap.ControlPointInfo.DifficultyPointAt(original.StartTime).SpeedMultiplier : 1
};
}
else if (endTimeData != null)

View File

@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double hitWindowGreat = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / timeRate;
double preempt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / timeRate;
int maxCombo = beatmap.HitObjects.Count();
int maxCombo = beatmap.HitObjects.Count;
// Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
maxCombo += beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
countHitCircles = Beatmap.HitObjects.Count(h => h is HitCircle);
beatmapMaxCombo = Beatmap.HitObjects.Count();
beatmapMaxCombo = Beatmap.HitObjects.Count;
// Add the ticks + tail of the slider. 1 is subtracted because the "headcircle" would be counted twice (once for the slider itself in the line above)
beatmapMaxCombo += Beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
}

View File

@ -75,15 +75,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
{
computeSliderCursorPosition(lastSlider);
lastCursorPosition = lastSlider.LazyEndPosition ?? lastCursorPosition;
TravelDistance = lastSlider.LazyTravelDistance * scalingFactor;
}
// Don't need to jump to reach spinners
if (!(BaseObject is Spinner))
JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
// Todo: BUG!!! Last slider's travel distance is considered ONLY IF we ourselves are also a slider!
if (BaseObject is Slider)
TravelDistance = (lastSlider?.LazyTravelDistance ?? 0) * scalingFactor;
}
private void setTimingValues()

View File

@ -4,6 +4,7 @@
using osu.Framework.Graphics.Cursor;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using OpenTK;
namespace osu.Game.Rulesets.Osu.Edit
@ -15,8 +16,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{
}
protected override Vector2 PlayfieldArea => Vector2.One;
protected override CursorContainer CreateCursor() => null;
protected override Playfield CreatePlayfield() => new OsuPlayfield { Size = Vector2.One };
}
}

View File

@ -3,6 +3,7 @@
using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
@ -31,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Edit
new HitObjectCompositionTool<Spinner>()
};
protected override ScalableContainer CreateLayerContainer() => new ScalableContainer(OsuPlayfield.BASE_SIZE.X) { RelativeSizeAxes = Axes.Both };
protected override Container CreateLayerContainer() => new PlayfieldAdjustmentContainer { RelativeSizeAxes = Axes.Both };
public override HitObjectMask CreateMaskFor(DrawableHitObject hitObject)
{

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Mods
if (mod is ModHardRock)
hasHardrock = true;
}
rulesetContainer.Overlays.Add(flashlight = new DrawableOsuBlinds(rulesetContainer.Playfield, hasEasy, hasHardrock, rulesetContainer.Beatmap));
rulesetContainer.Overlays.Add(flashlight = new DrawableOsuBlinds(rulesetContainer.Playfield.HitObjectContainer, hasEasy, hasHardrock, rulesetContainer.Beatmap));
}
public override void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)

View File

@ -1,7 +1,6 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Mods;
@ -33,8 +32,9 @@ namespace osu.Game.Rulesets.Osu.Mods
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
slider.NestedHitObjects.OfType<RepeatPoint>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
var newControlPoints = new List<Vector2>();
slider.ControlPoints.ForEach(c => newControlPoints.Add(new Vector2(c.X, -c.Y)));
var newControlPoints = new Vector2[slider.ControlPoints.Length];
for (int i = 0; i < slider.ControlPoints.Length; i++)
newControlPoints[i] = new Vector2(slider.ControlPoints[i].X, -slider.ControlPoints[i].Y);
slider.ControlPoints = newControlPoints;
slider.Curve?.Calculate(); // Recalculate the slider curve

View File

@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private const float fg_panel_depth = 4;
private const float npc_depth = 6;
private readonly Container restrictTo;
private readonly CompositeDrawable restrictTo;
private readonly bool modEasy, modHardrock;
/// <summary>
@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// </summary>
private const float leniency = 0.1f;
public DrawableOsuBlinds(Container restrictTo, bool hasEasy, bool hasHardrock, Beatmap<OsuHitObject> beatmap)
public DrawableOsuBlinds(CompositeDrawable restrictTo, bool hasEasy, bool hasHardrock, Beatmap<OsuHitObject> beatmap)
{
this.restrictTo = restrictTo;
this.beatmap = beatmap;

View File

@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public SliderCurve Curve { get; } = new SliderCurve();
public List<Vector2> ControlPoints
public Vector2[] ControlPoints
{
get { return Curve.ControlPoints; }
set { Curve.ControlPoints = value; }
@ -92,8 +92,21 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary>
public double SpanDuration => Duration / this.SpanCount();
public double Velocity;
public double TickDistance;
/// <summary>
/// Velocity of this <see cref="Slider"/>.
/// </summary>
public double Velocity { get; private set; }
/// <summary>
/// Spacing between <see cref="SliderTick"/>s of this <see cref="Slider"/>.
/// </summary>
public double TickDistance { get; private set; }
/// <summary>
/// An extra multiplier that affects the number of <see cref="SliderTick"/>s generated by this <see cref="Slider"/>.
/// An increase in this value increases <see cref="TickDistance"/>, which reduces the number of ticks generated.
/// </summary>
public double TickDistanceMultiplier = 1;
public HitCircle HeadCircle;
public SliderTailCircle TailCircle;
@ -108,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Objects
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate;
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
}
protected override void CreateNestedHitObjects()

View File

@ -23,29 +23,35 @@ namespace osu.Game.Rulesets.Osu.UI
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
public OsuPlayfield()
: base(BASE_SIZE.X)
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
AddRange(new Drawable[]
Size = new Vector2(0.75f);
InternalChild = new PlayfieldAdjustmentContainer
{
connectionLayer = new FollowPointRenderer
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Depth = 2,
},
judgementLayer = new JudgementContainer<DrawableOsuJudgement>
{
RelativeSizeAxes = Axes.Both,
Depth = 1,
},
approachCircles = new Container
{
RelativeSizeAxes = Axes.Both,
Depth = -1,
},
});
connectionLayer = new FollowPointRenderer
{
RelativeSizeAxes = Axes.Both,
Depth = 2,
},
judgementLayer = new JudgementContainer<DrawableOsuJudgement>
{
RelativeSizeAxes = Axes.Both,
Depth = 1,
},
HitObjectContainer,
approachCircles = new Container
{
RelativeSizeAxes = Axes.Both,
Depth = -1,
},
}
};
}
public override void Add(DrawableHitObject h)

View File

@ -4,7 +4,6 @@
using System.Linq;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Input;
using OpenTK;
using osu.Game.Beatmaps;
using osu.Game.Input.Handlers;
using osu.Game.Rulesets.Objects.Drawables;
@ -58,12 +57,6 @@ namespace osu.Game.Rulesets.Osu.UI
}
}
protected override Vector2 GetAspectAdjustedSize()
{
var aspectSize = DrawSize.X * 0.75f < DrawSize.Y ? new Vector2(DrawSize.X, DrawSize.X * 0.75f) : new Vector2(DrawSize.Y * 4f / 3f, DrawSize.Y);
return new Vector2(aspectSize.X / DrawSize.X, aspectSize.Y / DrawSize.Y);
}
protected override CursorContainer CreateCursor() => new GameplayCursor();
}
}

View File

@ -0,0 +1,42 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using OpenTK;
namespace osu.Game.Rulesets.Osu.UI
{
public class PlayfieldAdjustmentContainer : Container
{
protected override Container<Drawable> Content => content;
private readonly Container content;
public PlayfieldAdjustmentContainer()
{
InternalChild = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
FillAspectRatio = 4f / 3,
Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both }
};
}
/// <summary>
/// A <see cref="Container"/> which scales its content relative to a target width.
/// </summary>
private class ScalingContainer : Container
{
protected override void Update()
{
base.Update();
Scale = new Vector2(Parent.ChildSize.X / OsuPlayfield.BASE_SIZE.X);
Size = Vector2.Divide(Vector2.One, Scale);
}
}
}
}

View File

@ -2,9 +2,10 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -0,0 +1,22 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics.Containers;
using OpenTK;
namespace osu.Game.Rulesets.Taiko.UI
{
public class PlayfieldAdjustmentContainer : Container
{
private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
private const float default_aspect = 16f / 9f;
protected override void Update()
{
base.Update();
float aspectAdjust = MathHelper.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect;
Size = new Vector2(1, default_relative_height * aspectAdjust);
}
}
}

View File

@ -47,9 +47,6 @@ namespace osu.Game.Rulesets.Taiko.UI
private readonly Container<KiaiHitExplosion> kiaiExplosionContainer;
private readonly JudgementContainer<DrawableTaikoJudgement> judgementContainer;
protected override Container<Drawable> Content => content;
private readonly Container content;
private readonly Container topLevelHitContainer;
private readonly Container barlineContainer;
@ -64,140 +61,147 @@ namespace osu.Game.Rulesets.Taiko.UI
{
Direction.Value = ScrollingDirection.Left;
AddRangeInternal(new Drawable[]
InternalChild = new PlayfieldAdjustmentContainer
{
backgroundContainer = new Container
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
Name = "Transparent playfield background",
RelativeSizeAxes = Axes.Both,
Masking = true,
EdgeEffect = new EdgeEffectParameters
backgroundContainer = new Container
{
Type = EdgeEffectType.Shadow,
Colour = Color4.Black.Opacity(0.2f),
Radius = 5,
},
Children = new Drawable[]
{
background = new Box
Name = "Transparent playfield background",
RelativeSizeAxes = Axes.Both,
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.6f
Type = EdgeEffectType.Shadow,
Colour = Color4.Black.Opacity(0.2f),
Radius = 5,
},
}
},
new Container
{
Name = "Right area",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = left_area_size },
Children = new Drawable[]
{
new Container
Children = new Drawable[]
{
Name = "Masked elements before hit objects",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = HIT_TARGET_OFFSET },
Masking = true,
Children = new Drawable[]
background = new Box
{
hitExplosionContainer = new Container<HitExplosion>
{
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Blending = BlendingMode.Additive,
},
new HitTarget
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit
}
}
},
barlineContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = HIT_TARGET_OFFSET }
},
content = new Container
{
Name = "Hit objects",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = HIT_TARGET_OFFSET },
Masking = true
},
kiaiExplosionContainer = new Container<KiaiHitExplosion>
{
Name = "Kiai hit explosions",
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Margin = new MarginPadding { Left = HIT_TARGET_OFFSET },
Blending = BlendingMode.Additive
},
judgementContainer = new JudgementContainer<DrawableTaikoJudgement>
{
Name = "Judgements",
RelativeSizeAxes = Axes.Y,
Margin = new MarginPadding { Left = HIT_TARGET_OFFSET },
Blending = BlendingMode.Additive
},
}
},
overlayBackgroundContainer = new Container
{
Name = "Left overlay",
RelativeSizeAxes = Axes.Y,
Size = new Vector2(left_area_size, 1),
Children = new Drawable[]
{
overlayBackground = new Box
{
RelativeSizeAxes = Axes.Both,
},
new InputDrum(controlPoints)
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Scale = new Vector2(0.9f),
Margin = new MarginPadding { Right = 20 }
},
new Box
{
Anchor = Anchor.TopRight,
RelativeSizeAxes = Axes.Y,
Width = 10,
Colour = Framework.Graphics.Colour.ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.6f), Color4.Black.Opacity(0)),
},
}
},
new Container
{
Name = "Border",
RelativeSizeAxes = Axes.Both,
Masking = true,
MaskingSmoothness = 0,
BorderThickness = 2,
AlwaysPresent = true,
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
RelativeSizeAxes = Axes.Both,
Alpha = 0.6f
},
}
},
new Container
{
Name = "Right area",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = left_area_size },
Children = new Drawable[]
{
new Container
{
Name = "Masked elements before hit objects",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = HIT_TARGET_OFFSET },
Masking = true,
Children = new Drawable[]
{
hitExplosionContainer = new Container<HitExplosion>
{
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Blending = BlendingMode.Additive,
},
new HitTarget
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit
}
}
},
barlineContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = HIT_TARGET_OFFSET }
},
new Container
{
Name = "Hit objects",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = HIT_TARGET_OFFSET },
Masking = true,
Child = HitObjectContainer
},
kiaiExplosionContainer = new Container<KiaiHitExplosion>
{
Name = "Kiai hit explosions",
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Margin = new MarginPadding { Left = HIT_TARGET_OFFSET },
Blending = BlendingMode.Additive
},
judgementContainer = new JudgementContainer<DrawableTaikoJudgement>
{
Name = "Judgements",
RelativeSizeAxes = Axes.Y,
Margin = new MarginPadding { Left = HIT_TARGET_OFFSET },
Blending = BlendingMode.Additive
},
}
},
overlayBackgroundContainer = new Container
{
Name = "Left overlay",
RelativeSizeAxes = Axes.Y,
Size = new Vector2(left_area_size, 1),
Children = new Drawable[]
{
overlayBackground = new Box
{
RelativeSizeAxes = Axes.Both,
},
new InputDrum(controlPoints)
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Scale = new Vector2(0.9f),
Margin = new MarginPadding { Right = 20 }
},
new Box
{
Anchor = Anchor.TopRight,
RelativeSizeAxes = Axes.Y,
Width = 10,
Colour = Framework.Graphics.Colour.ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.6f), Color4.Black.Opacity(0)),
},
}
},
new Container
{
Name = "Border",
RelativeSizeAxes = Axes.Both,
Masking = true,
MaskingSmoothness = 0,
BorderThickness = 2,
AlwaysPresent = true,
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
}
},
topLevelHitContainer = new Container
{
Name = "Top level hit objects",
RelativeSizeAxes = Axes.Both,
}
},
topLevelHitContainer = new Container
{
Name = "Top level hit objects",
RelativeSizeAxes = Axes.Both,
}
});
};
VisibleTimeRange.Value = 6000;
VisibleTimeRange.Value = 7000;
}
[BackgroundDependencyLoader]

View File

@ -2,7 +2,6 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
@ -13,7 +12,6 @@ using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.Taiko.Replays;
using OpenTK;
using System.Linq;
using osu.Framework.Input;
using osu.Game.Input.Handlers;
@ -74,27 +72,11 @@ namespace osu.Game.Rulesets.Taiko.UI
}
}
protected override Vector2 GetAspectAdjustedSize()
{
const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
const float default_aspect = 16f / 9f;
float aspectAdjust = MathHelper.Clamp(DrawWidth / DrawHeight, 0.4f, 4) / default_aspect;
return new Vector2(1, default_relative_height * aspectAdjust);
}
protected override Vector2 PlayfieldArea => Vector2.One;
public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(this);
public override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);
protected override Playfield CreatePlayfield() => new TaikoPlayfield(Beatmap.ControlPointInfo)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
};
protected override Playfield CreatePlayfield() => new TaikoPlayfield(Beatmap.ControlPointInfo);
protected override DrawableHitObject<TaikoHitObject> GetVisualRepresentation(TaikoHitObject h)
{

View File

@ -8,11 +8,13 @@ using OpenTK.Graphics;
using osu.Game.Tests.Resources;
using System.Linq;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Skinning;
@ -21,6 +23,25 @@ namespace osu.Game.Tests.Beatmaps.Formats
[TestFixture]
public class LegacyBeatmapDecoderTest
{
[Test]
public void TestDecodeBeatmapVersion()
{
using (var resStream = Resource.OpenResource("beatmap-version.osu"))
using (var stream = new StreamReader(resStream))
{
var decoder = Decoder.GetDecoder<Beatmap>(stream);
stream.BaseStream.Position = 0;
stream.DiscardBufferedData();
var working = new TestWorkingBeatmap(decoder.Decode(stream));
Assert.AreEqual(6, working.BeatmapInfo.BeatmapVersion);
Assert.AreEqual(6, working.Beatmap.BeatmapInfo.BeatmapVersion);
Assert.AreEqual(6, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapVersion);
}
}
[Test]
public void TestDecodeBeatmapGeneral()
{

View File

@ -0,0 +1 @@
osu file format v6

View File

@ -44,14 +44,12 @@ namespace osu.Game.Tests.Visual
new Slider
{
Position = new Vector2(128, 256),
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(216, 0),
},
Distance = 400,
Velocity = 1,
TickDistance = 100,
Distance = 216,
Scale = 0.5f,
}
},

View File

@ -121,14 +121,21 @@ namespace osu.Game.Tests.Visual
Direction = direction;
Padding = new MarginPadding(2);
Content.Masking = true;
AddInternal(new Box
InternalChildren = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.5f,
Depth = float.MaxValue
});
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.5f,
},
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
Child = HitObjectContainer
}
};
}
}

View File

@ -2,9 +2,11 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
<PackageReference Include="DeepEqual" Version="1.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -1,7 +1,7 @@
<!-- Contains required properties for osu!framework projects. -->
<Project>
<PropertyGroup Label="C#">
<LangVersion>7</LangVersion>
<LangVersion>7.2</LangVersion>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>..\app.manifest</ApplicationManifest>

View File

@ -48,7 +48,7 @@ namespace osu.Game.Beatmaps
[JsonConverter(typeof(TypedListConverter<HitObject>))]
public List<T> HitObjects = new List<T>();
IEnumerable<HitObject> IBeatmap.HitObjects => HitObjects;
IReadOnlyList<HitObject> IBeatmap.HitObjects => HitObjects;
public virtual IEnumerable<BeatmapStatistic> GetStatistics() => Enumerable.Empty<BeatmapStatistic>();

View File

@ -55,39 +55,40 @@ namespace osu.Game.Beatmaps
beatmap.BeatmapInfo = original.BeatmapInfo;
beatmap.ControlPointInfo = original.ControlPointInfo;
beatmap.HitObjects = original.HitObjects.SelectMany(h => convert(h, original)).ToList();
beatmap.HitObjects = convertHitObjects(original.HitObjects, original);
beatmap.Breaks = original.Breaks;
return beatmap;
}
/// <summary>
/// Converts a hit object.
/// </summary>
/// <param name="original">The hit object to convert.</param>
/// <param name="beatmap">The un-converted Beatmap.</param>
/// <returns>The converted hit object.</returns>
private IEnumerable<T> convert(HitObject original, IBeatmap beatmap)
private List<T> convertHitObjects(IReadOnlyList<HitObject> hitObjects, IBeatmap beatmap)
{
// Check if the hitobject is already the converted type
T tObject = original as T;
if (tObject != null)
{
yield return tObject;
yield break;
}
var result = new List<T>(hitObjects.Count);
var converted = ConvertHitObject(original, beatmap).ToList();
ObjectConverted?.Invoke(original, converted);
// Convert the hit object
foreach (var obj in converted)
foreach (var obj in hitObjects)
{
if (obj == null)
if (obj is T tObj)
{
result.Add(tObj);
continue;
}
yield return obj;
var converted = ConvertHitObject(obj, beatmap);
if (ObjectConverted != null)
{
converted = converted.ToList();
ObjectConverted.Invoke(obj, converted);
}
foreach (var c in converted)
{
if (c != null)
result.Add(c);
}
}
return result;
}
/// <summary>

View File

@ -19,7 +19,6 @@ namespace osu.Game.Beatmaps
[JsonIgnore]
public int ID { get; set; }
//TODO: should be in database
public int BeatmapVersion;
private int? onlineBeatmapID;

View File

@ -148,11 +148,12 @@ namespace osu.Game.Beatmaps
/// </summary>
/// <param name="beatmapSetInfo">The <see cref="BeatmapSetInfo"/> to be downloaded.</param>
/// <param name="noVideo">Whether the beatmap should be downloaded without video. Defaults to false.</param>
public void Download(BeatmapSetInfo beatmapSetInfo, bool noVideo = false)
/// <returns>Downloading can happen</returns>
public bool Download(BeatmapSetInfo beatmapSetInfo, bool noVideo = false)
{
var existing = GetExistingDownload(beatmapSetInfo);
if (existing != null || api == null) return;
if (existing != null || api == null) return false;
if (!api.LocalUser.Value.IsSupporter)
{
@ -161,7 +162,7 @@ namespace osu.Game.Beatmaps
Icon = FontAwesome.fa_superpowers,
Text = "You gotta be an osu!supporter to download for now 'yo"
});
return;
return false;
}
var downloadNotification = new DownloadNotification
@ -227,6 +228,7 @@ namespace osu.Game.Beatmaps
// don't run in the main api queue as this is a long-running task.
Task.Factory.StartNew(() => request.Perform(api), TaskCreationOptions.LongRunning);
BeatmapDownloadBegan?.Invoke(request);
return true;
}
protected override void PresentCompletedImport(IEnumerable<BeatmapSetInfo> imported)

View File

@ -55,14 +55,14 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary>
/// <param name="time">The time to find the sound control point at.</param>
/// <returns>The sound control point.</returns>
public SampleControlPoint SamplePointAt(double time) => binarySearch(SamplePoints, time, SamplePoints.FirstOrDefault());
public SampleControlPoint SamplePointAt(double time) => binarySearch(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : null);
/// <summary>
/// Finds the timing control point that is active at <paramref name="time"/>.
/// </summary>
/// <param name="time">The time to find the timing control point at.</param>
/// <returns>The timing control point.</returns>
public TimingControlPoint TimingPointAt(double time) => binarySearch(TimingPoints, time, TimingPoints.FirstOrDefault());
public TimingControlPoint TimingPointAt(double time) => binarySearch(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : null);
/// <summary>
/// Finds the maximum BPM represented by any timing control point.
@ -104,17 +104,26 @@ namespace osu.Game.Beatmaps.ControlPoints
if (time < list[0].Time)
return prePoint ?? new T();
int index = list.BinarySearch(new T { Time = time });
if (time >= list[list.Count - 1].Time)
return list[list.Count - 1];
// Check if we've found an exact match (t == time)
if (index >= 0)
return list[index];
int l = 0;
int r = list.Count - 2;
index = ~index;
while (l <= r)
{
int pivot = l + ((r - l) >> 1);
// BinarySearch will return the index of the first element _greater_ than the search
// This is the inactive point - the active point is the one before it (index - 1)
return list[index - 1];
if (list[pivot].Time < time)
l = pivot + 1;
else if (list[pivot].Time > time)
r = pivot - 1;
else
return list[pivot];
}
// l will be the first control point with Time > time, but we want the one before it
return list[l - 1];
}
}
}

View File

@ -71,9 +71,11 @@ namespace osu.Game.Beatmaps.Drawables
if (DownloadState.Value > DownloadStatus.NotDownloaded)
return;
beatmaps.Download(set, noVideo);
DownloadState.Value = DownloadStatus.Downloading;
if (beatmaps.Download(set, noVideo))
{
// Only change state if download can happen
DownloadState.Value = DownloadStatus.Downloading;
}
}
private void setAdded(BeatmapSetInfo s)

View File

@ -5,6 +5,7 @@ using System;
using System.Globalization;
using System.IO;
using System.Linq;
using osu.Framework.IO.File;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Beatmaps.ControlPoints;
@ -58,7 +59,7 @@ namespace osu.Game.Beatmaps.Formats
hitObject.ApplyDefaults(this.beatmap.ControlPointInfo, this.beatmap.BeatmapInfo.BaseDifficulty);
}
protected override bool ShouldSkipLine(string line) => base.ShouldSkipLine(line) || line.StartsWith(" ") || line.StartsWith("_");
protected override bool ShouldSkipLine(string line) => base.ShouldSkipLine(line) || line.StartsWith(" ", StringComparison.Ordinal) || line.StartsWith("_", StringComparison.Ordinal);
protected override void ParseLine(Beatmap beatmap, Section section, string line)
{
@ -100,7 +101,7 @@ namespace osu.Game.Beatmaps.Formats
switch (pair.Key)
{
case @"AudioFilename":
metadata.AudioFile = pair.Value;
metadata.AudioFile = FileSafety.PathStandardise(pair.Value);
break;
case @"AudioLeadIn":
beatmap.BeatmapInfo.AudioLeadIn = int.Parse(pair.Value);
@ -256,7 +257,7 @@ namespace osu.Game.Beatmaps.Formats
{
case EventType.Background:
string filename = split[2].Trim('"');
beatmap.BeatmapInfo.Metadata.BackgroundFile = filename;
beatmap.BeatmapInfo.Metadata.BackgroundFile = FileSafety.PathStandardise(filename);
break;
case EventType.Break:
var breakEvent = new BreakPeriod

View File

@ -69,7 +69,7 @@ namespace osu.Game.Beatmaps.Formats
protected string StripComments(string line)
{
var index = line.IndexOf("//", StringComparison.Ordinal);
var index = line.AsSpan().IndexOf("//".AsSpan());
if (index > 0)
return line.Substring(0, index);
return line;

View File

@ -298,6 +298,6 @@ namespace osu.Game.Beatmaps.Formats
}
}
private string cleanFilename(string path) => FileSafety.PathStandardise(FileSafety.PathSanitise(path.Trim('\"')));
private string cleanFilename(string path) => FileSafety.PathStandardise(path.Trim('"'));
}
}

View File

@ -39,7 +39,7 @@ namespace osu.Game.Beatmaps
/// <summary>
/// The hitobjects contained by this beatmap.
/// </summary>
IEnumerable<HitObject> HitObjects { get; }
IReadOnlyList<HitObject> HitObjects { get; }
/// <summary>
/// Returns statistics for the <see cref="HitObjects"/> contained in this beatmap.

View File

@ -41,8 +41,13 @@ namespace osu.Game.Beatmaps
beatmap = new RecyclableLazy<IBeatmap>(() =>
{
var b = GetBeatmap() ?? new Beatmap();
// use the database-backed info.
// The original beatmap version needs to be preserved as the database doesn't contain it
BeatmapInfo.BeatmapVersion = b.BeatmapInfo.BeatmapVersion;
// Use the database-backed info for more up-to-date values (beatmap id, ranked status, etc)
b.BeatmapInfo = BeatmapInfo;
return b;
});

View File

@ -404,7 +404,7 @@ namespace osu.Game.Database
using (Stream s = reader.GetStream(file))
fileInfos.Add(new TFileModel
{
Filename = FileSafety.PathSanitise(file),
Filename = FileSafety.PathStandardise(file),
FileInfo = files.Add(s)
});

View File

@ -30,7 +30,7 @@ namespace osu.Game.Graphics.UserInterface
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
if (accentColour == default(Color4))
if (accentColour == default)
accentColour = colours.PinkDarker;
updateAccentColour();
}

View File

@ -53,7 +53,7 @@ namespace osu.Game.Graphics.UserInterface
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
if (accentColour == default(Color4))
if (accentColour == default)
AccentColour = colours.Blue;
}
@ -142,7 +142,7 @@ namespace osu.Game.Graphics.UserInterface
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
if (accentColour == default(Color4))
if (accentColour == default)
AccentColour = colours.Blue;
}

View File

@ -134,7 +134,7 @@ namespace osu.Game.Graphics.UserInterface
/// </summary>
public virtual void ResetCount()
{
SetCountWithoutRolling(default(T));
SetCountWithoutRolling(default);
}
/// <summary>

View File

@ -0,0 +1,380 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using osu.Game.Database;
namespace osu.Game.Migrations
{
[DbContext(typeof(OsuDbContext))]
[Migration("20181007180454_StandardizePaths")]
partial class StandardizePaths
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.3-rtm-32065");
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<float>("ApproachRate");
b.Property<float>("CircleSize");
b.Property<float>("DrainRate");
b.Property<float>("OverallDifficulty");
b.Property<double>("SliderMultiplier");
b.Property<double>("SliderTickRate");
b.HasKey("ID");
b.ToTable("BeatmapDifficulty");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("AudioLeadIn");
b.Property<int>("BaseDifficultyID");
b.Property<int>("BeatDivisor");
b.Property<int>("BeatmapSetInfoID");
b.Property<bool>("Countdown");
b.Property<double>("DistanceSpacing");
b.Property<int>("GridSize");
b.Property<string>("Hash");
b.Property<bool>("Hidden");
b.Property<bool>("LetterboxInBreaks");
b.Property<string>("MD5Hash");
b.Property<int?>("MetadataID");
b.Property<int?>("OnlineBeatmapID");
b.Property<string>("Path");
b.Property<int>("RulesetID");
b.Property<bool>("SpecialStyle");
b.Property<float>("StackLeniency");
b.Property<double>("StarDifficulty");
b.Property<int>("Status");
b.Property<string>("StoredBookmarks");
b.Property<double>("TimelineZoom");
b.Property<string>("Version");
b.Property<bool>("WidescreenStoryboard");
b.HasKey("ID");
b.HasIndex("BaseDifficultyID");
b.HasIndex("BeatmapSetInfoID");
b.HasIndex("Hash");
b.HasIndex("MD5Hash");
b.HasIndex("MetadataID");
b.HasIndex("OnlineBeatmapID")
.IsUnique();
b.HasIndex("RulesetID");
b.ToTable("BeatmapInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Artist");
b.Property<string>("ArtistUnicode");
b.Property<string>("AudioFile");
b.Property<string>("AuthorString")
.HasColumnName("Author");
b.Property<string>("BackgroundFile");
b.Property<int>("PreviewTime");
b.Property<string>("Source");
b.Property<string>("Tags");
b.Property<string>("Title");
b.Property<string>("TitleUnicode");
b.HasKey("ID");
b.ToTable("BeatmapMetadata");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("BeatmapSetInfoID");
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.HasKey("ID");
b.HasIndex("BeatmapSetInfoID");
b.HasIndex("FileInfoID");
b.ToTable("BeatmapSetFileInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<bool>("DeletePending");
b.Property<string>("Hash");
b.Property<int?>("MetadataID");
b.Property<int?>("OnlineBeatmapSetID");
b.Property<bool>("Protected");
b.Property<int>("Status");
b.HasKey("ID");
b.HasIndex("DeletePending");
b.HasIndex("Hash")
.IsUnique();
b.HasIndex("MetadataID");
b.HasIndex("OnlineBeatmapSetID")
.IsUnique();
b.ToTable("BeatmapSetInfo");
});
modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("IntKey")
.HasColumnName("Key");
b.Property<int?>("RulesetID");
b.Property<string>("StringValue")
.HasColumnName("Value");
b.Property<int?>("Variant");
b.HasKey("ID");
b.HasIndex("RulesetID", "Variant");
b.ToTable("Settings");
});
modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("IntAction")
.HasColumnName("Action");
b.Property<string>("KeysString")
.HasColumnName("Keys");
b.Property<int?>("RulesetID");
b.Property<int?>("Variant");
b.HasKey("ID");
b.HasIndex("IntAction");
b.HasIndex("RulesetID", "Variant");
b.ToTable("KeyBinding");
});
modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Hash");
b.Property<int>("ReferenceCount");
b.HasKey("ID");
b.HasIndex("Hash")
.IsUnique();
b.HasIndex("ReferenceCount");
b.ToTable("FileInfo");
});
modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
{
b.Property<int?>("ID")
.ValueGeneratedOnAdd();
b.Property<bool>("Available");
b.Property<string>("InstantiationInfo");
b.Property<string>("Name");
b.Property<string>("ShortName");
b.HasKey("ID");
b.HasIndex("Available");
b.HasIndex("ShortName")
.IsUnique();
b.ToTable("RulesetInfo");
});
modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.Property<int>("SkinInfoID");
b.HasKey("ID");
b.HasIndex("FileInfoID");
b.HasIndex("SkinInfoID");
b.ToTable("SkinFileInfo");
});
modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Creator");
b.Property<bool>("DeletePending");
b.Property<string>("Name");
b.HasKey("ID");
b.ToTable("SkinInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
.WithMany()
.HasForeignKey("BaseDifficultyID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
.WithMany("Beatmaps")
.HasForeignKey("BeatmapSetInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
.WithMany("Beatmaps")
.HasForeignKey("MetadataID");
b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
.WithMany()
.HasForeignKey("RulesetID")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
.WithMany("Files")
.HasForeignKey("BeatmapSetInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
.WithMany("BeatmapSets")
.HasForeignKey("MetadataID");
});
modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
{
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Skinning.SkinInfo")
.WithMany("Files")
.HasForeignKey("SkinInfoID")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,26 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using System.IO;
namespace osu.Game.Migrations
{
public partial class StandardizePaths : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
string windowsStyle = @"\";
string standardized = "/";
// Escaping \ does not seem to be needed.
migrationBuilder.Sql($"UPDATE `BeatmapInfo` SET `Path` = REPLACE(`Path`, '{windowsStyle}', '{standardized}')");
migrationBuilder.Sql($"UPDATE `BeatmapMetadata` SET `AudioFile` = REPLACE(`AudioFile`, '{windowsStyle}', '{standardized}')");
migrationBuilder.Sql($"UPDATE `BeatmapMetadata` SET `BackgroundFile` = REPLACE(`BackgroundFile`, '{windowsStyle}', '{standardized}')");
migrationBuilder.Sql($"UPDATE `BeatmapSetFileInfo` SET `Filename` = REPLACE(`Filename`, '{windowsStyle}', '{standardized}')");
migrationBuilder.Sql($"UPDATE `SkinFileInfo` SET `Filename` = REPLACE(`Filename`, '{windowsStyle}', '{standardized}')");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@ -14,7 +14,7 @@ namespace osu.Game.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.2-rtm-30932");
.HasAnnotation("ProductVersion", "2.1.3-rtm-32065");
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
{

View File

@ -1,16 +1,14 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using System.ComponentModel;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Direct;
using osu.Game.Rulesets;
namespace osu.Game.Online.API.Requests
{
public class SearchBeatmapSetsRequest : APIRequest<IEnumerable<APIBeatmapSet>>
public class SearchBeatmapSetsRequest : APIRequest<SearchBeatmapSetsResponse>
{
private readonly string query;
private readonly RulesetInfo ruleset;
@ -35,6 +33,7 @@ namespace osu.Game.Online.API.Requests
public enum BeatmapSearchCategory
{
Any = 7,
[Description("Ranked & Approved")]
RankedApproved = 0,
Approved = 1,
@ -43,6 +42,7 @@ namespace osu.Game.Online.API.Requests
Qualified = 3,
Pending = 4,
Graveyard = 5,
[Description("My Maps")]
MyMaps = 6,
}

View File

@ -0,0 +1,20 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using Newtonsoft.Json;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests
{
public class SearchBeatmapSetsResponse
{
public IEnumerable<APIBeatmapSet> BeatmapSets;
/// <summary>
/// A collection of parameters which should be passed to the search endpoint to fetch the next page.
/// </summary>
[JsonProperty("cursor")]
public dynamic CursorJson;
}
}

View File

@ -288,7 +288,7 @@ namespace osu.Game.Overlays
{
Task.Run(() =>
{
var sets = response.Select(r => r.ToBeatmapSet(rulesets)).ToList();
var sets = response.BeatmapSets.Select(r => r.ToBeatmapSet(rulesets)).ToList();
// may not need scheduling; loads async internally.
Schedule(() =>

View File

@ -74,7 +74,7 @@ namespace osu.Game.Overlays.Profile.Header
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.X,
Height = 75,
Height = 60,
Y = -secondary_textsize,
Alpha = 0,
}

View File

@ -165,6 +165,6 @@ namespace osu.Game.Rulesets.Edit
/// <summary>
/// Creates a <see cref="ScalableContainer"/> which provides a layer above or below the <see cref="Playfield"/>.
/// </summary>
protected virtual ScalableContainer CreateLayerContainer() => new ScalableContainer { RelativeSizeAxes = Axes.Both };
protected virtual Container CreateLayerContainer() => new Container { RelativeSizeAxes = Axes.Both };
}
}

View File

@ -1,25 +1,26 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using OpenTK;
namespace osu.Game.Rulesets.Objects
{
public class BezierApproximator
public readonly ref struct BezierApproximator
{
private readonly int count;
private readonly List<Vector2> controlPoints;
private readonly ReadOnlySpan<Vector2> controlPoints;
private readonly Vector2[] subdivisionBuffer1;
private readonly Vector2[] subdivisionBuffer2;
private const float tolerance = 0.25f;
private const float tolerance_sq = tolerance * tolerance;
public BezierApproximator(List<Vector2> controlPoints)
public BezierApproximator(ReadOnlySpan<Vector2> controlPoints)
{
this.controlPoints = controlPoints;
count = controlPoints.Count;
count = controlPoints.Length;
subdivisionBuffer1 = new Vector2[count];
subdivisionBuffer2 = new Vector2[count * 2 - 1];

View File

@ -1,40 +1,40 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using OpenTK;
namespace osu.Game.Rulesets.Objects
{
public class CatmullApproximator
public readonly ref struct CatmullApproximator
{
/// <summary>
/// The amount of pieces to calculate for each controlpoint quadruplet.
/// </summary>
private const int detail = 50;
private readonly List<Vector2> controlPoints;
private readonly ReadOnlySpan<Vector2> controlPoints;
public CatmullApproximator(List<Vector2> controlPoints)
public CatmullApproximator(ReadOnlySpan<Vector2> controlPoints)
{
this.controlPoints = controlPoints;
}
/// <summary>
/// Creates a piecewise-linear approximation of a Catmull-Rom spline.
/// </summary>
/// <returns>A list of vectors representing the piecewise-linear approximation.</returns>
public List<Vector2> CreateCatmull()
{
var result = new List<Vector2>();
var result = new List<Vector2>((controlPoints.Length - 1) * detail * 2);
for (int i = 0; i < controlPoints.Count - 1; i++)
for (int i = 0; i < controlPoints.Length - 1; i++)
{
var v1 = i > 0 ? controlPoints[i - 1] : controlPoints[i];
var v2 = controlPoints[i];
var v3 = i < controlPoints.Count - 1 ? controlPoints[i + 1] : v2 + v2 - v1;
var v4 = i < controlPoints.Count - 2 ? controlPoints[i + 2] : v3 + v3 - v2;
var v3 = i < controlPoints.Length - 1 ? controlPoints[i + 1] : v2 + v2 - v1;
var v4 = i < controlPoints.Length - 2 ? controlPoints[i + 2] : v3 + v3 - v2;
for (int c = 0; c < detail; c++)
{

View File

@ -8,21 +8,15 @@ using OpenTK;
namespace osu.Game.Rulesets.Objects
{
public class CircularArcApproximator
public readonly ref struct CircularArcApproximator
{
private readonly Vector2 a;
private readonly Vector2 b;
private readonly Vector2 c;
private int amountPoints;
private const float tolerance = 0.1f;
public CircularArcApproximator(Vector2 a, Vector2 b, Vector2 c)
private readonly ReadOnlySpan<Vector2> controlPoints;
public CircularArcApproximator(ReadOnlySpan<Vector2> controlPoints)
{
this.a = a;
this.b = b;
this.c = c;
this.controlPoints = controlPoints;
}
/// <summary>
@ -31,6 +25,10 @@ namespace osu.Game.Rulesets.Objects
/// <returns>A list of vectors representing the piecewise-linear approximation.</returns>
public List<Vector2> CreateArc()
{
Vector2 a = controlPoints[0];
Vector2 b = controlPoints[1];
Vector2 c = controlPoints[2];
float aSq = (b - c).LengthSquared;
float bSq = (a - c).LengthSquared;
float cSq = (a - b).LengthSquared;
@ -81,7 +79,7 @@ namespace osu.Game.Rulesets.Objects
// is: 2 * Math.Acos(1 - TOLERANCE / r)
// The special case is required for extremely short sliders where the radius is smaller than
// the tolerance. This is a pathological rather than a realistic case.
amountPoints = 2 * r <= tolerance ? 2 : Math.Max(2, (int)Math.Ceiling(thetaRange / (2 * Math.Acos(1 - tolerance / r))));
int amountPoints = 2 * r <= tolerance ? 2 : Math.Max(2, (int)Math.Ceiling(thetaRange / (2 * Math.Acos(1 - tolerance / r))));
List<Vector2> output = new List<Vector2>(amountPoints);

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
};
}
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples)
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples)
{
newCombo |= forceNewCombo;
comboOffset += extraComboOffset;

View File

@ -72,10 +72,18 @@ namespace osu.Game.Rulesets.Objects.Legacy
{
CurveType curveType = CurveType.Catmull;
double length = 0;
var points = new List<Vector2> { Vector2.Zero };
string[] pointsplit = split[5].Split('|');
foreach (string t in pointsplit)
string[] pointSplit = split[5].Split('|');
int pointCount = 1;
foreach (var t in pointSplit)
if (t.Length > 1)
pointCount++;
var points = new Vector2[pointCount];
int pointIndex = 1;
foreach (string t in pointSplit)
{
if (t.Length == 1)
{
@ -94,16 +102,18 @@ namespace osu.Game.Rulesets.Objects.Legacy
curveType = CurveType.PerfectCurve;
break;
}
continue;
}
string[] temp = t.Split(':');
points.Add(new Vector2((int)Convert.ToDouble(temp[0], CultureInfo.InvariantCulture), (int)Convert.ToDouble(temp[1], CultureInfo.InvariantCulture)) - pos);
points[pointIndex++] = new Vector2((int)Convert.ToDouble(temp[0], CultureInfo.InvariantCulture), (int)Convert.ToDouble(temp[1], CultureInfo.InvariantCulture)) - pos;
}
// osu-stable special-cased colinear perfect curves to a CurveType.Linear
bool isLinear(List<Vector2> p) => Precision.AlmostEquals(0, (p[1].Y - p[0].Y) * (p[2].X - p[0].X) - (p[1].X - p[0].X) * (p[2].Y - p[0].Y));
if (points.Count == 3 && curveType == CurveType.PerfectCurve && isLinear(points))
bool isLinear(Vector2[] p) => Precision.AlmostEquals(0, (p[1].Y - p[0].Y) * (p[2].X - p[0].X) - (p[1].X - p[0].X) * (p[2].Y - p[0].Y));
if (points.Length == 3 && curveType == CurveType.PerfectCurve && isLinear(points))
curveType = CurveType.Linear;
int repeatCount = Convert.ToInt32(split[6], CultureInfo.InvariantCulture);
@ -262,7 +272,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// <param name="repeatCount">The slider repeat count.</param>
/// <param name="repeatSamples">The samples to be played when the repeat nodes are hit. This includes the head and tail of the slider.</param>
/// <returns>The hit object.</returns>
protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples);
protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples);
/// <summary>
/// Creates a legacy Spinner-type hit object.

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// <see cref="ConvertSlider"/>s don't need a curve since they're converted to ruleset-specific hitobjects.
/// </summary>
public SliderCurve Curve { get; } = null;
public List<Vector2> ControlPoints { get; set; }
public Vector2[] ControlPoints { get; set; }
public CurveType CurveType { get; set; }
public double Distance { get; set; }

View File

@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania
};
}
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples)
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples)
{
return new ConvertSlider
{

View File

@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
};
}
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples)
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples)
{
newCombo |= forceNewCombo;
comboOffset += extraComboOffset;

View File

@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko
return new ConvertHit();
}
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples)
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, Vector2[] controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples)
{
return new ConvertSlider
{

View File

@ -1,6 +1,7 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.MathUtils;
@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Objects
{
public double Distance;
public List<Vector2> ControlPoints;
public Vector2[] ControlPoints;
public CurveType CurveType = CurveType.PerfectCurve;
@ -22,19 +23,23 @@ namespace osu.Game.Rulesets.Objects
private readonly List<Vector2> calculatedPath = new List<Vector2>();
private readonly List<double> cumulativeLength = new List<double>();
private List<Vector2> calculateSubpath(List<Vector2> subControlPoints)
private List<Vector2> calculateSubpath(ReadOnlySpan<Vector2> subControlPoints)
{
switch (CurveType)
{
case CurveType.Linear:
return subControlPoints;
var result = new List<Vector2>(subControlPoints.Length);
foreach (var c in subControlPoints)
result.Add(c);
return result;
case CurveType.PerfectCurve:
//we can only use CircularArc iff we have exactly three control points and no dissection.
if (ControlPoints.Count != 3 || subControlPoints.Count != 3)
if (ControlPoints.Length != 3 || subControlPoints.Length != 3)
break;
// Here we have exactly 3 control points. Attempt to fit a circular arc.
List<Vector2> subpath = new CircularArcApproximator(subControlPoints[0], subControlPoints[1], subControlPoints[2]).CreateArc();
List<Vector2> subpath = new CircularArcApproximator(subControlPoints).CreateArc();
// If for some reason a circular arc could not be fit to the 3 given points, fall back to a numerically stable bezier approximation.
if (subpath.Count == 0)
@ -55,18 +60,23 @@ namespace osu.Game.Rulesets.Objects
// Sliders may consist of various subpaths separated by two consecutive vertices
// with the same position. The following loop parses these subpaths and computes
// their shape independently, consecutively appending them to calculatedPath.
List<Vector2> subControlPoints = new List<Vector2>();
for (int i = 0; i < ControlPoints.Count; ++i)
int start = 0;
int end = 0;
for (int i = 0; i < ControlPoints.Length; ++i)
{
subControlPoints.Add(ControlPoints[i]);
if (i == ControlPoints.Count - 1 || ControlPoints[i] == ControlPoints[i + 1])
end++;
if (i == ControlPoints.Length - 1 || ControlPoints[i] == ControlPoints[i + 1])
{
List<Vector2> subpath = calculateSubpath(subControlPoints);
foreach (Vector2 t in subpath)
ReadOnlySpan<Vector2> cpSpan = ControlPoints.AsSpan().Slice(start, end - start);
foreach (Vector2 t in calculateSubpath(cpSpan))
if (calculatedPath.Count == 0 || calculatedPath.Last() != t)
calculatedPath.Add(t);
subControlPoints.Clear();
start = end;
}
}
}
@ -166,7 +176,7 @@ namespace osu.Game.Rulesets.Objects
/// <param name="p1">End progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).</param>
public void GetPathToProgress(List<Vector2> path, double p0, double p1)
{
if (calculatedPath.Count == 0 && ControlPoints.Count > 0)
if (calculatedPath.Count == 0 && ControlPoints.Length > 0)
Calculate();
double d0 = progressToDistance(p0);
@ -193,7 +203,7 @@ namespace osu.Game.Rulesets.Objects
/// <returns></returns>
public Vector2 PositionAt(double progress)
{
if (calculatedPath.Count == 0 && ControlPoints.Count > 0)
if (calculatedPath.Count == 0 && ControlPoints.Length > 0)
Calculate();
double d = progressToDistance(progress);

View File

@ -1,7 +1,6 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using OpenTK;
namespace osu.Game.Rulesets.Objects.Types
@ -19,7 +18,7 @@ namespace osu.Game.Rulesets.Objects.Types
/// <summary>
/// The control points that shape the curve.
/// </summary>
List<Vector2> ControlPoints { get; }
Vector2[] ControlPoints { get; }
/// <summary>
/// The type of curve.

View File

@ -14,6 +14,11 @@ namespace osu.Game.Rulesets.UI
public IEnumerable<DrawableHitObject> Objects => InternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime);
public IEnumerable<DrawableHitObject> AliveObjects => AliveInternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime);
public HitObjectContainer()
{
RelativeSizeAxes = Axes.Both;
}
public virtual void Add(DrawableHitObject hitObject) => AddInternal(hitObject);
public virtual bool Remove(DrawableHitObject hitObject) => RemoveInternal(hitObject);

View File

@ -9,17 +9,26 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Configuration;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using OpenTK;
namespace osu.Game.Rulesets.UI
{
public abstract class Playfield : ScalableContainer
public abstract class Playfield : CompositeDrawable
{
/// <summary>
/// The <see cref="DrawableHitObject"/> contained in this Playfield.
/// </summary>
public HitObjectContainer HitObjectContainer { get; private set; }
public HitObjectContainer HitObjectContainer => hitObjectContainerLazy.Value;
private readonly Lazy<HitObjectContainer> hitObjectContainerLazy;
/// <summary>
/// A function that converts gamefield coordinates to screen space.
/// </summary>
public Func<Vector2, Vector2> GamefieldToScreenSpace => HitObjectContainer.ToScreenSpace;
/// <summary>
/// All the <see cref="DrawableHitObject"/>s contained in this <see cref="Playfield"/> and all <see cref="NestedPlayfields"/>.
@ -39,18 +48,13 @@ namespace osu.Game.Rulesets.UI
public readonly BindableBool DisplayJudgements = new BindableBool(true);
/// <summary>
/// A container for keeping track of DrawableHitObjects.
/// Creates a new <see cref="Playfield"/>.
/// </summary>
/// <param name="customWidth">The width to scale the internal coordinate space to.
/// May be null if scaling based on <paramref name="customHeight"/> is desired. If <paramref name="customHeight"/> is also null, no scaling will occur.
/// </param>
/// <param name="customHeight">The height to scale the internal coordinate space to.
/// May be null if scaling based on <paramref name="customWidth"/> is desired. If <paramref name="customWidth"/> is also null, no scaling will occur.
/// </param>
protected Playfield(float? customWidth = null, float? customHeight = null)
: base(customWidth, customHeight)
protected Playfield()
{
RelativeSizeAxes = Axes.Both;
hitObjectContainerLazy = new Lazy<HitObjectContainer>(CreateHitObjectContainer);
}
private WorkingBeatmap beatmap;
@ -59,11 +63,6 @@ namespace osu.Game.Rulesets.UI
private void load(IBindableBeatmap beatmap)
{
this.beatmap = beatmap.Value;
HitObjectContainer = CreateHitObjectContainer();
HitObjectContainer.RelativeSizeAxes = Axes.Both;
Add(HitObjectContainer);
}
/// <summary>
@ -94,10 +93,14 @@ namespace osu.Game.Rulesets.UI
nestedPlayfields.Value.Add(otherPlayfield);
}
/// <summary>
/// Creates the container that will be used to contain the <see cref="DrawableHitObject"/>s.
/// </summary>
protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer();
protected override void LoadComplete()
{
base.LoadComplete();
// in the case a consumer forgets to add the HitObjectContainer, we will add it here.
if (HitObjectContainer.Parent == null)
AddInternal(HitObjectContainer);
}
protected override void Update()
{
@ -108,5 +111,10 @@ namespace osu.Game.Rulesets.UI
if (mod is IUpdatableByPlayfield updatable)
updatable.Update(this);
}
/// <summary>
/// Creates the container that will be used to contain the <see cref="DrawableHitObject"/>s.
/// </summary>
protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer();
}
}

View File

@ -22,7 +22,6 @@ using osu.Game.Overlays;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using OpenTK;
namespace osu.Game.Rulesets.UI
{
@ -321,28 +320,11 @@ namespace osu.Game.Rulesets.UI
mod.ApplyToDrawableHitObjects(Playfield.HitObjectContainer.Objects);
}
protected override void Update()
{
base.Update();
Playfield.Size = GetAspectAdjustedSize() * PlayfieldArea;
}
/// <summary>
/// Returns the currently selected mods for this ruleset container.
/// </summary>
public IEnumerable<Mod> ActiveMods { get => Mods; }
/// <summary>
/// Computes the size of the <see cref="Playfield"/> in relative coordinate space after aspect adjustments.
/// </summary>
/// <returns>The aspect-adjusted size.</returns>
protected virtual Vector2 GetAspectAdjustedSize() => Vector2.One;
/// <summary>
/// The area of this <see cref="RulesetContainer"/> that is available for the <see cref="Playfield"/> to use.
/// Must be specified in relative coordinate space to this <see cref="RulesetContainer"/>.
/// This affects the final size of the <see cref="Playfield"/> but does not affect the <see cref="Playfield"/>'s scale.
/// </summary>
protected virtual Vector2 PlayfieldArea => new Vector2(0.75f); // A sane default
/// <summary>
/// Creates a DrawableHitObject from a HitObject.
/// </summary>

View File

@ -1,99 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using OpenTK;
namespace osu.Game.Rulesets.UI
{
/// <summary>
/// A <see cref="Container"/> which can have its internal coordinate system scaled to a specific size.
/// </summary>
public class ScalableContainer : Container
{
/// <summary>
/// A function that converts coordinates from gamefield to screen space.
/// </summary>
public Func<Vector2, Vector2> GamefieldToScreenSpace => scaledContent.GamefieldToScreenSpace;
/// <summary>
/// The scaled content.
/// </summary>
private readonly ScaledContainer scaledContent;
protected override Container<Drawable> Content => scaledContent;
/// <summary>
/// A <see cref="Container"/> which can have its internal coordinate system scaled to a specific size.
/// </summary>
/// <param name="customWidth">The width to scale the internal coordinate space to.
/// May be null if scaling based on <paramref name="customHeight"/> is desired. If <paramref name="customHeight"/> is also null, no scaling will occur.
/// </param>
/// <param name="customHeight">The height to scale the internal coordinate space to.
/// May be null if scaling based on <paramref name="customWidth"/> is desired. If <paramref name="customWidth"/> is also null, no scaling will occur.
/// </param>
public ScalableContainer(float? customWidth = null, float? customHeight = null)
{
AddInternal(scaledContent = new ScaledContainer
{
CustomWidth = customWidth,
CustomHeight = customHeight,
RelativeSizeAxes = Axes.Both,
});
}
private class ScaledContainer : Container
{
/// <summary>
/// A function that converts coordinates from gamefield to screen space.
/// </summary>
public Func<Vector2, Vector2> GamefieldToScreenSpace => content.ToScreenSpace;
/// <summary>
/// The value to scale the width of the content to match.
/// If null, <see cref="CustomHeight"/> is used.
/// </summary>
public float? CustomWidth;
/// <summary>
/// The value to scale the height of the content to match.
/// if null, <see cref="CustomWidth"/> is used.
/// </summary>
public float? CustomHeight;
private readonly Container content;
protected override Container<Drawable> Content => content;
public ScaledContainer()
{
AddInternal(content = new Container { RelativeSizeAxes = Axes.Both });
}
protected override void Update()
{
base.Update();
content.Scale = sizeScale;
content.Size = Vector2.Divide(Vector2.One, sizeScale);
}
/// <summary>
/// The scale that is required for the size of the content to match <see cref="CustomWidth"/> and <see cref="CustomHeight"/>.
/// </summary>
private Vector2 sizeScale
{
get
{
if (CustomWidth.HasValue && CustomHeight.HasValue)
return Vector2.Divide(DrawSize, new Vector2(CustomWidth.Value, CustomHeight.Value));
if (CustomWidth.HasValue)
return new Vector2(DrawSize.X / CustomWidth.Value);
if (CustomHeight.HasValue)
return new Vector2(DrawSize.Y / CustomHeight.Value);
return Vector2.One;
}
}
}
}
}

View File

@ -65,20 +65,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
protected virtual SpeedChangeVisualisationMethod VisualisationMethod => SpeedChangeVisualisationMethod.Sequential;
/// <summary>
/// Creates a new <see cref="ScrollingPlayfield"/>.
/// </summary>
/// <param name="customWidth">The width to scale the internal coordinate space to.
/// May be null if scaling based on <paramref name="customHeight"/> is desired. If <paramref name="customHeight"/> is also null, no scaling will occur.
/// </param>
/// <param name="customHeight">The height to scale the internal coordinate space to.
/// May be null if scaling based on <paramref name="customWidth"/> is desired. If <paramref name="customWidth"/> is also null, no scaling will occur.
/// </param>
protected ScrollingPlayfield(float? customWidth = null, float? customHeight = null)
: base(customWidth, customHeight)
{
}
[BackgroundDependencyLoader]
private void load()
{

View File

@ -307,10 +307,10 @@ namespace osu.Game.Screens.Select
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Alpha = 0;
InternalChild = textContainer = new FillFlowContainer
{
Alpha = 0,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(spacing / 2),
@ -337,7 +337,7 @@ namespace osu.Game.Screens.Select
{
if (string.IsNullOrEmpty(value))
{
this.FadeOut(transition_duration);
textContainer.FadeOut(transition_duration);
return;
}
@ -345,8 +345,6 @@ namespace osu.Game.Screens.Select
}
}
public override bool IsPresent => base.IsPresent || textFlow == null; // Visibility is updated in the LoadComponentAsync callback
private void setTextAsync(string text)
{
LoadComponentAsync(new OsuTextFlowContainer(s => s.TextSize = 14)
@ -361,7 +359,7 @@ namespace osu.Game.Screens.Select
textContainer.Add(textFlow = loaded);
// fade in if we haven't yet.
this.FadeIn(transition_duration);
textContainer.FadeIn(transition_duration);
});
}
}

View File

@ -21,8 +21,8 @@ namespace osu.Game.Storyboards
private Cached<double> endTimeBacking;
public double EndTime => endTimeBacking.IsValid ? endTimeBacking : endTimeBacking.Value = HasCommands ? commands.Max(c => c.EndTime) : double.MaxValue;
public T StartValue => HasCommands ? commands.OrderBy(c => c.StartTime).First().StartValue : default(T);
public T EndValue => HasCommands ? commands.OrderByDescending(c => c.EndTime).First().EndValue : default(T);
public T StartValue => HasCommands ? commands.OrderBy(c => c.StartTime).First().StartValue : default;
public T EndValue => HasCommands ? commands.OrderByDescending(c => c.EndTime).First().EndValue : default;
public void Add(Easing easing, double startTime, double endTime, T startValue, T endValue)
{

View File

@ -15,10 +15,10 @@
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Humanizer" Version="2.5.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.1.4" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="ppy.osu.Framework" Version="2018.1012.0" />
<PackageReference Include="ppy.osu.Framework" Version="2018.1018.0" />
<PackageReference Include="SharpCompress" Version="0.22.0" />
<PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />

View File

@ -10,10 +10,6 @@
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
<ProjectReference Include="..\osu-resources\osu.Game.Resources\osu.Game.Resources.csproj" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.3" />
<PackageReference Include="DeepEqual" Version="1.6.0" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\osu.Game\Tests\VisualTestRunner.cs">
<Link>VisualTestRunner.cs</Link>

View File

@ -666,6 +666,7 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpRenamePlacementToArrangementMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAlwaysTreatStructAsNotReorderableMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>