mirror of
https://github.com/osukey/osukey.git
synced 2025-08-06 16:13:57 +09:00
Merge branch 'master' into ruleset-shaders
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@ -336,3 +336,6 @@ inspectcode
|
|||||||
/BenchmarkDotNet.Artifacts
|
/BenchmarkDotNet.Artifacts
|
||||||
|
|
||||||
*.GeneratedMSBuildEditorConfig.editorconfig
|
*.GeneratedMSBuildEditorConfig.editorconfig
|
||||||
|
|
||||||
|
# Fody (pulled in by Realm) - schema file
|
||||||
|
FodyWeavers.xsd
|
||||||
|
@ -43,7 +43,7 @@ If your platform is not listed above, there is still a chance you can manually b
|
|||||||
|
|
||||||
osu! is designed to have extensible modular gameplay modes, called "rulesets". Building one of these allows a developer to harness the power of osu! for their own game style. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu/tree/master/Templates).
|
osu! is designed to have extensible modular gameplay modes, called "rulesets". Building one of these allows a developer to harness the power of osu! for their own game style. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu/tree/master/Templates).
|
||||||
|
|
||||||
You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/issues/5852).
|
You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/discussions/13096).
|
||||||
|
|
||||||
## Developing osu!
|
## Developing osu!
|
||||||
|
|
||||||
|
@ -54,4 +54,8 @@
|
|||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.618.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.618.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.616.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.616.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup Label="Transitive Dependencies">
|
||||||
|
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||||
|
<PackageReference Include="Realm" Version="10.2.0" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
@ -34,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
// only check the X position; handle all vertical space.
|
// only check the X position; handle all vertical space.
|
||||||
base.ReceivePositionalInputAt(new Vector2(screenSpacePos.X, ScreenSpaceDrawQuad.Centre.Y));
|
base.ReceivePositionalInputAt(new Vector2(screenSpacePos.X, ScreenSpaceDrawQuad.Centre.Y));
|
||||||
|
|
||||||
public CatchPlayfield(BeatmapDifficulty difficulty, Func<CatchHitObject, DrawableHitObject<CatchHitObject>> createDrawableRepresentation)
|
public CatchPlayfield(BeatmapDifficulty difficulty)
|
||||||
{
|
{
|
||||||
var droppedObjectContainer = new Container<CaughtObject>
|
var droppedObjectContainer = new Container<CaughtObject>
|
||||||
{
|
{
|
||||||
|
@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
|
|
||||||
protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield);
|
protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield);
|
||||||
|
|
||||||
protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty, CreateDrawableRepresentation);
|
protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty);
|
||||||
|
|
||||||
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchPlayfieldAdjustmentContainer();
|
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchPlayfieldAdjustmentContainer();
|
||||||
|
|
||||||
|
@ -0,0 +1,260 @@
|
|||||||
|
// 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 System.Linq;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Rulesets.Osu.Edit.Checks;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class CheckLowDiffOverlapsTest
|
||||||
|
{
|
||||||
|
private CheckLowDiffOverlaps check;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
check = new CheckLowDiffOverlaps();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNoOverlapFarApart()
|
||||||
|
{
|
||||||
|
assertOk(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 500, Position = new Vector2(200, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNoOverlapClose()
|
||||||
|
{
|
||||||
|
assertShouldProbablyOverlap(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 167, Position = new Vector2(200, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNoOverlapTooClose()
|
||||||
|
{
|
||||||
|
assertShouldOverlap(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 100, Position = new Vector2(200, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNoOverlapTooCloseExpert()
|
||||||
|
{
|
||||||
|
assertOk(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 100, Position = new Vector2(200, 0) }
|
||||||
|
}
|
||||||
|
}, DifficultyRating.Expert);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOverlapClose()
|
||||||
|
{
|
||||||
|
assertOk(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 167, Position = new Vector2(20, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOverlapFarApart()
|
||||||
|
{
|
||||||
|
assertShouldNotOverlap(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 500, Position = new Vector2(20, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAlmostOverlapFarApart()
|
||||||
|
{
|
||||||
|
assertOk(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
// Default circle diameter is 128 px, but part of that is the fade/border of the circle.
|
||||||
|
// We want this to only be a problem when it actually looks like an overlap.
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 500, Position = new Vector2(125, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAlmostNotOverlapFarApart()
|
||||||
|
{
|
||||||
|
assertShouldNotOverlap(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 500, Position = new Vector2(110, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOverlapFarApartExpert()
|
||||||
|
{
|
||||||
|
assertOk(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 500, Position = new Vector2(20, 0) }
|
||||||
|
}
|
||||||
|
}, DifficultyRating.Expert);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestOverlapTooFarApart()
|
||||||
|
{
|
||||||
|
// Far apart enough to where the objects are not visible at the same time, and so overlapping is fine.
|
||||||
|
assertOk(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 2000, Position = new Vector2(20, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSliderTailOverlapFarApart()
|
||||||
|
{
|
||||||
|
assertShouldNotOverlap(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
getSliderMock(startTime: 0, endTime: 500, startPosition: new Vector2(0, 0), endPosition: new Vector2(100, 0)).Object,
|
||||||
|
new HitCircle { StartTime = 1000, Position = new Vector2(120, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSliderTailOverlapClose()
|
||||||
|
{
|
||||||
|
assertOk(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
getSliderMock(startTime: 0, endTime: 900, startPosition: new Vector2(0, 0), endPosition: new Vector2(100, 0)).Object,
|
||||||
|
new HitCircle { StartTime = 1000, Position = new Vector2(120, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSliderTailNoOverlapFarApart()
|
||||||
|
{
|
||||||
|
assertOk(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
getSliderMock(startTime: 0, endTime: 500, startPosition: new Vector2(0, 0), endPosition: new Vector2(100, 0)).Object,
|
||||||
|
new HitCircle { StartTime = 1000, Position = new Vector2(300, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSliderTailNoOverlapClose()
|
||||||
|
{
|
||||||
|
// If these were circles they would need to overlap, but overlapping with slider tails is not required.
|
||||||
|
assertOk(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
getSliderMock(startTime: 0, endTime: 900, startPosition: new Vector2(0, 0), endPosition: new Vector2(100, 0)).Object,
|
||||||
|
new HitCircle { StartTime = 1000, Position = new Vector2(300, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mock<Slider> getSliderMock(double startTime, double endTime, Vector2 startPosition, Vector2 endPosition)
|
||||||
|
{
|
||||||
|
var mockSlider = new Mock<Slider>();
|
||||||
|
mockSlider.SetupGet(s => s.StartTime).Returns(startTime);
|
||||||
|
mockSlider.SetupGet(s => s.Position).Returns(startPosition);
|
||||||
|
mockSlider.SetupGet(s => s.EndPosition).Returns(endPosition);
|
||||||
|
mockSlider.As<IHasDuration>().Setup(d => d.EndTime).Returns(endTime);
|
||||||
|
|
||||||
|
return mockSlider;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertOk(IBeatmap beatmap, DifficultyRating difficultyRating = DifficultyRating.Easy)
|
||||||
|
{
|
||||||
|
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating);
|
||||||
|
Assert.That(check.Run(context), Is.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertShouldProbablyOverlap(IBeatmap beatmap, int count = 1)
|
||||||
|
{
|
||||||
|
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy);
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(count));
|
||||||
|
Assert.That(issues.All(issue => issue.Template is CheckLowDiffOverlaps.IssueTemplateShouldProbablyOverlap));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertShouldOverlap(IBeatmap beatmap, int count = 1)
|
||||||
|
{
|
||||||
|
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy);
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(count));
|
||||||
|
Assert.That(issues.All(issue => issue.Template is CheckLowDiffOverlaps.IssueTemplateShouldOverlap));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertShouldNotOverlap(IBeatmap beatmap, int count = 1)
|
||||||
|
{
|
||||||
|
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy);
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(count));
|
||||||
|
Assert.That(issues.All(issue => issue.Template is CheckLowDiffOverlaps.IssueTemplateShouldNotOverlap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,324 @@
|
|||||||
|
// 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 System.Linq;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Rulesets.Osu.Edit.Checks;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class CheckTimeDistanceEqualityTest
|
||||||
|
{
|
||||||
|
private CheckTimeDistanceEquality check;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
check = new CheckTimeDistanceEquality();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCirclesEquidistant()
|
||||||
|
{
|
||||||
|
assertOk(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||||
|
new HitCircle { StartTime = 1000, Position = new Vector2(100, 0) },
|
||||||
|
new HitCircle { StartTime = 1500, Position = new Vector2(150, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCirclesOneSlightlyOff()
|
||||||
|
{
|
||||||
|
assertWarning(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||||
|
new HitCircle { StartTime = 1000, Position = new Vector2(80, 0) }, // Distance a quite low compared to previous.
|
||||||
|
new HitCircle { StartTime = 1500, Position = new Vector2(130, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCirclesOneOff()
|
||||||
|
{
|
||||||
|
assertProblem(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||||
|
new HitCircle { StartTime = 1000, Position = new Vector2(150, 0) }, // Twice the regular spacing.
|
||||||
|
new HitCircle { StartTime = 1500, Position = new Vector2(100, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCirclesTwoOff()
|
||||||
|
{
|
||||||
|
assertProblem(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||||
|
new HitCircle { StartTime = 1000, Position = new Vector2(150, 0) }, // Twice the regular spacing.
|
||||||
|
new HitCircle { StartTime = 1500, Position = new Vector2(250, 0) } // Also twice the regular spacing.
|
||||||
|
}
|
||||||
|
}, count: 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCirclesStacked()
|
||||||
|
{
|
||||||
|
assertOk(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||||
|
new HitCircle { StartTime = 1000, Position = new Vector2(50, 0) }, // Stacked, is fine.
|
||||||
|
new HitCircle { StartTime = 1500, Position = new Vector2(100, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCirclesStacking()
|
||||||
|
{
|
||||||
|
assertWarning(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||||
|
new HitCircle { StartTime = 1000, Position = new Vector2(50, 0), StackHeight = 1 },
|
||||||
|
new HitCircle { StartTime = 1500, Position = new Vector2(50, 0), StackHeight = 2 },
|
||||||
|
new HitCircle { StartTime = 2000, Position = new Vector2(50, 0), StackHeight = 3 },
|
||||||
|
new HitCircle { StartTime = 2500, Position = new Vector2(50, 0), StackHeight = 4 }, // Ends up far from (50; 0), causing irregular spacing.
|
||||||
|
new HitCircle { StartTime = 3000, Position = new Vector2(100, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCirclesHalfStack()
|
||||||
|
{
|
||||||
|
assertOk(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||||
|
new HitCircle { StartTime = 1000, Position = new Vector2(55, 0) }, // Basically stacked, so is fine.
|
||||||
|
new HitCircle { StartTime = 1500, Position = new Vector2(105, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCirclesPartialOverlap()
|
||||||
|
{
|
||||||
|
assertProblem(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||||
|
new HitCircle { StartTime = 1000, Position = new Vector2(65, 0) }, // Really low distance compared to previous.
|
||||||
|
new HitCircle { StartTime = 1500, Position = new Vector2(115, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCirclesSlightlyDifferent()
|
||||||
|
{
|
||||||
|
assertOk(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
// Does not need to be perfect, as long as the distance is approximately correct it's sight-readable.
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 500, Position = new Vector2(52, 0) },
|
||||||
|
new HitCircle { StartTime = 1000, Position = new Vector2(97, 0) },
|
||||||
|
new HitCircle { StartTime = 1500, Position = new Vector2(165, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCirclesSlowlyChanging()
|
||||||
|
{
|
||||||
|
const float multiplier = 1.2f;
|
||||||
|
|
||||||
|
assertOk(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||||
|
new HitCircle { StartTime = 1000, Position = new Vector2(50 + 50 * multiplier, 0) },
|
||||||
|
// This gap would be a warning if it weren't for the previous pushing the average spacing up.
|
||||||
|
new HitCircle { StartTime = 1500, Position = new Vector2(50 + 50 * multiplier + 50 * multiplier * multiplier, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCirclesQuicklyChanging()
|
||||||
|
{
|
||||||
|
const float multiplier = 1.6f;
|
||||||
|
|
||||||
|
var beatmap = new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||||
|
new HitCircle { StartTime = 1000, Position = new Vector2(50 + 50 * multiplier, 0) }, // Warning
|
||||||
|
new HitCircle { StartTime = 1500, Position = new Vector2(50 + 50 * multiplier + 50 * multiplier * multiplier, 0) } // Problem
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy);
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(2));
|
||||||
|
Assert.That(issues.First().Template is CheckTimeDistanceEquality.IssueTemplateIrregularSpacingWarning);
|
||||||
|
Assert.That(issues.Last().Template is CheckTimeDistanceEquality.IssueTemplateIrregularSpacingProblem);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCirclesTooFarApart()
|
||||||
|
{
|
||||||
|
assertOk(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||||
|
new HitCircle { StartTime = 4000, Position = new Vector2(200, 0) }, // 2 seconds apart from previous, so can start from wherever.
|
||||||
|
new HitCircle { StartTime = 4500, Position = new Vector2(250, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCirclesOneOffExpert()
|
||||||
|
{
|
||||||
|
assertOk(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||||
|
new HitCircle { StartTime = 1000, Position = new Vector2(150, 0) }, // Jumps are allowed in higher difficulties.
|
||||||
|
new HitCircle { StartTime = 1500, Position = new Vector2(100, 0) }
|
||||||
|
}
|
||||||
|
}, DifficultyRating.Expert);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSpinner()
|
||||||
|
{
|
||||||
|
assertOk(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||||
|
new Spinner { StartTime = 500, EndTime = 1000 }, // Distance to and from the spinner should be ignored. If it isn't this should give a problem.
|
||||||
|
new HitCircle { StartTime = 1500, Position = new Vector2(100, 0) },
|
||||||
|
new HitCircle { StartTime = 2000, Position = new Vector2(150, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSliders()
|
||||||
|
{
|
||||||
|
assertOk(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||||
|
getSliderMock(startTime: 1000, endTime: 1500, startPosition: new Vector2(100, 0), endPosition: new Vector2(150, 0)).Object,
|
||||||
|
getSliderMock(startTime: 2000, endTime: 2500, startPosition: new Vector2(200, 0), endPosition: new Vector2(250, 0)).Object,
|
||||||
|
new HitCircle { StartTime = 2500, Position = new Vector2(300, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSlidersOneOff()
|
||||||
|
{
|
||||||
|
assertProblem(new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new HitCircle { StartTime = 0, Position = new Vector2(0) },
|
||||||
|
new HitCircle { StartTime = 500, Position = new Vector2(50, 0) },
|
||||||
|
getSliderMock(startTime: 1000, endTime: 1500, startPosition: new Vector2(100, 0), endPosition: new Vector2(150, 0)).Object,
|
||||||
|
getSliderMock(startTime: 2000, endTime: 2500, startPosition: new Vector2(250, 0), endPosition: new Vector2(300, 0)).Object, // Twice the spacing.
|
||||||
|
new HitCircle { StartTime = 2500, Position = new Vector2(300, 0) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mock<Slider> getSliderMock(double startTime, double endTime, Vector2 startPosition, Vector2 endPosition)
|
||||||
|
{
|
||||||
|
var mockSlider = new Mock<Slider>();
|
||||||
|
mockSlider.SetupGet(s => s.StartTime).Returns(startTime);
|
||||||
|
mockSlider.SetupGet(s => s.Position).Returns(startPosition);
|
||||||
|
mockSlider.SetupGet(s => s.EndPosition).Returns(endPosition);
|
||||||
|
mockSlider.As<IHasDuration>().Setup(d => d.EndTime).Returns(endTime);
|
||||||
|
|
||||||
|
return mockSlider;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertOk(IBeatmap beatmap, DifficultyRating difficultyRating = DifficultyRating.Easy)
|
||||||
|
{
|
||||||
|
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating);
|
||||||
|
Assert.That(check.Run(context), Is.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertWarning(IBeatmap beatmap, int count = 1)
|
||||||
|
{
|
||||||
|
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy);
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(count));
|
||||||
|
Assert.That(issues.All(issue => issue.Template is CheckTimeDistanceEquality.IssueTemplateIrregularSpacingWarning));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertProblem(IBeatmap beatmap, int count = 1)
|
||||||
|
{
|
||||||
|
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), DifficultyRating.Easy);
|
||||||
|
var issues = check.Run(context).ToList();
|
||||||
|
|
||||||
|
Assert.That(issues, Has.Count.EqualTo(count));
|
||||||
|
Assert.That(issues.All(issue => issue.Template is CheckTimeDistanceEquality.IssueTemplateIrregularSpacingProblem));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@
|
|||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
|
||||||
|
<PackageReference Include="Moq" Version="4.16.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||||
|
109
osu.Game.Rulesets.Osu/Edit/Checks/CheckLowDiffOverlaps.cs
Normal file
109
osu.Game.Rulesets.Osu/Edit/Checks/CheckLowDiffOverlaps.cs
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
// 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 osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Edit.Checks
|
||||||
|
{
|
||||||
|
public class CheckLowDiffOverlaps : ICheck
|
||||||
|
{
|
||||||
|
// For the lowest difficulties, the osu! Ranking Criteria encourages overlapping ~180 BPM 1/2, but discourages ~180 BPM 1/1.
|
||||||
|
private const double should_overlap_threshold = 150; // 200 BPM 1/2
|
||||||
|
private const double should_probably_overlap_threshold = 175; // 170 BPM 1/2
|
||||||
|
private const double should_not_overlap_threshold = 250; // 120 BPM 1/2 = 240 BPM 1/1
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Objects need to overlap this much before being treated as an overlap, else it may just be the borders slightly touching.
|
||||||
|
/// </summary>
|
||||||
|
private const double overlap_leniency = 5;
|
||||||
|
|
||||||
|
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Spread, "Missing or unexpected overlaps");
|
||||||
|
|
||||||
|
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
|
||||||
|
{
|
||||||
|
new IssueTemplateShouldOverlap(this),
|
||||||
|
new IssueTemplateShouldProbablyOverlap(this),
|
||||||
|
new IssueTemplateShouldNotOverlap(this)
|
||||||
|
};
|
||||||
|
|
||||||
|
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||||
|
{
|
||||||
|
// TODO: This should also apply to *lowest difficulty* Normals - they are skipped for now.
|
||||||
|
if (context.InterpretedDifficulty > DifficultyRating.Easy)
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
var hitObjects = context.Beatmap.HitObjects;
|
||||||
|
|
||||||
|
for (int i = 0; i < hitObjects.Count - 1; ++i)
|
||||||
|
{
|
||||||
|
if (!(hitObjects[i] is OsuHitObject hitObject) || hitObject is Spinner)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!(hitObjects[i + 1] is OsuHitObject nextHitObject) || nextHitObject is Spinner)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var deltaTime = nextHitObject.StartTime - hitObject.GetEndTime();
|
||||||
|
if (deltaTime >= hitObject.TimeFadeIn + hitObject.TimePreempt)
|
||||||
|
// The objects are not visible at the same time (without mods), hence skipping.
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var distanceSq = (hitObject.StackedEndPosition - nextHitObject.StackedPosition).LengthSquared;
|
||||||
|
var diameter = (hitObject.Radius - overlap_leniency) * 2;
|
||||||
|
var diameterSq = diameter * diameter;
|
||||||
|
|
||||||
|
bool areOverlapping = distanceSq < diameterSq;
|
||||||
|
|
||||||
|
// Slider ends do not need to be overlapped because of slider leniency.
|
||||||
|
if (!areOverlapping && !(hitObject is Slider))
|
||||||
|
{
|
||||||
|
if (deltaTime < should_overlap_threshold)
|
||||||
|
yield return new IssueTemplateShouldOverlap(this).Create(deltaTime, hitObject, nextHitObject);
|
||||||
|
else if (deltaTime < should_probably_overlap_threshold)
|
||||||
|
yield return new IssueTemplateShouldProbablyOverlap(this).Create(deltaTime, hitObject, nextHitObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areOverlapping && deltaTime > should_not_overlap_threshold)
|
||||||
|
yield return new IssueTemplateShouldNotOverlap(this).Create(deltaTime, hitObject, nextHitObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class IssueTemplateOverlap : IssueTemplate
|
||||||
|
{
|
||||||
|
protected IssueTemplateOverlap(ICheck check, IssueType issueType, string unformattedMessage)
|
||||||
|
: base(check, issueType, unformattedMessage)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Issue Create(double deltaTime, params HitObject[] hitObjects) => new Issue(hitObjects, this, deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IssueTemplateShouldOverlap : IssueTemplateOverlap
|
||||||
|
{
|
||||||
|
public IssueTemplateShouldOverlap(ICheck check)
|
||||||
|
: base(check, IssueType.Problem, "These are {0} ms apart and so should be overlapping.")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IssueTemplateShouldProbablyOverlap : IssueTemplateOverlap
|
||||||
|
{
|
||||||
|
public IssueTemplateShouldProbablyOverlap(ICheck check)
|
||||||
|
: base(check, IssueType.Warning, "These are {0} ms apart and so should probably be overlapping.")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IssueTemplateShouldNotOverlap : IssueTemplateOverlap
|
||||||
|
{
|
||||||
|
public IssueTemplateShouldNotOverlap(ICheck check)
|
||||||
|
: base(check, IssueType.Problem, "These are {0} ms apart and so should NOT be overlapping.")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
179
osu.Game.Rulesets.Osu/Edit/Checks/CheckTimeDistanceEquality.cs
Normal file
179
osu.Game.Rulesets.Osu/Edit/Checks/CheckTimeDistanceEquality.cs
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
// 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 System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Edit.Checks
|
||||||
|
{
|
||||||
|
public class CheckTimeDistanceEquality : ICheck
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Two objects this many ms apart or more are skipped. (200 BPM 2/1)
|
||||||
|
/// </summary>
|
||||||
|
private const double pattern_lifetime = 600;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Two objects this distance apart or less are skipped.
|
||||||
|
/// </summary>
|
||||||
|
private const double stack_leniency = 12;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How long an observation is relevant for comparison. (120 BPM 8/1)
|
||||||
|
/// </summary>
|
||||||
|
private const double observation_lifetime = 4000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How different two delta times can be to still be compared. (240 BPM 1/16)
|
||||||
|
/// </summary>
|
||||||
|
private const double similar_time_leniency = 16;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How many pixels are subtracted from the difference between current and expected distance.
|
||||||
|
/// </summary>
|
||||||
|
private const double distance_leniency_absolute_warning = 10;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How much of the current distance that the difference can make out.
|
||||||
|
/// </summary>
|
||||||
|
private const double distance_leniency_percent_warning = 0.15;
|
||||||
|
|
||||||
|
private const double distance_leniency_absolute_problem = 20;
|
||||||
|
private const double distance_leniency_percent_problem = 0.3;
|
||||||
|
|
||||||
|
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Spread, "Object too close or far away from previous");
|
||||||
|
|
||||||
|
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
|
||||||
|
{
|
||||||
|
new IssueTemplateIrregularSpacingProblem(this),
|
||||||
|
new IssueTemplateIrregularSpacingWarning(this)
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an observation of the time and distance between two objects.
|
||||||
|
/// </summary>
|
||||||
|
private readonly struct ObservedTimeDistance
|
||||||
|
{
|
||||||
|
public readonly double ObservationTime;
|
||||||
|
public readonly double DeltaTime;
|
||||||
|
public readonly double Distance;
|
||||||
|
|
||||||
|
public ObservedTimeDistance(double observationTime, double deltaTime, double distance)
|
||||||
|
{
|
||||||
|
ObservationTime = observationTime;
|
||||||
|
DeltaTime = deltaTime;
|
||||||
|
Distance = distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||||
|
{
|
||||||
|
if (context.InterpretedDifficulty > DifficultyRating.Normal)
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
var prevObservedTimeDistances = new List<ObservedTimeDistance>();
|
||||||
|
var hitObjects = context.Beatmap.HitObjects;
|
||||||
|
|
||||||
|
for (int i = 0; i < hitObjects.Count - 1; ++i)
|
||||||
|
{
|
||||||
|
if (!(hitObjects[i] is OsuHitObject hitObject) || hitObject is Spinner)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!(hitObjects[i + 1] is OsuHitObject nextHitObject) || nextHitObject is Spinner)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var deltaTime = nextHitObject.StartTime - hitObject.GetEndTime();
|
||||||
|
|
||||||
|
// Ignore objects that are far enough apart in time to not be considered the same pattern.
|
||||||
|
if (deltaTime > pattern_lifetime)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Relying on FastInvSqrt is probably good enough here. We'll be taking the difference between distances later, hence square not being sufficient.
|
||||||
|
var distance = (hitObject.StackedEndPosition - nextHitObject.StackedPosition).LengthFast;
|
||||||
|
|
||||||
|
// Ignore stacks and half-stacks, as these are close enough to where they can't be confused for being time-distanced.
|
||||||
|
if (distance < stack_leniency)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var observedTimeDistance = new ObservedTimeDistance(nextHitObject.StartTime, deltaTime, distance);
|
||||||
|
var expectedDistance = getExpectedDistance(prevObservedTimeDistances, observedTimeDistance);
|
||||||
|
|
||||||
|
if (expectedDistance == 0)
|
||||||
|
{
|
||||||
|
// There was nothing relevant to compare to.
|
||||||
|
prevObservedTimeDistances.Add(observedTimeDistance);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((Math.Abs(expectedDistance - distance) - distance_leniency_absolute_problem) / distance > distance_leniency_percent_problem)
|
||||||
|
yield return new IssueTemplateIrregularSpacingProblem(this).Create(expectedDistance, distance, hitObject, nextHitObject);
|
||||||
|
else if ((Math.Abs(expectedDistance - distance) - distance_leniency_absolute_warning) / distance > distance_leniency_percent_warning)
|
||||||
|
yield return new IssueTemplateIrregularSpacingWarning(this).Create(expectedDistance, distance, hitObject, nextHitObject);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// We use `else` here to prevent issues from cascading; an object spaced too far could cause regular spacing to be considered "too short" otherwise.
|
||||||
|
prevObservedTimeDistances.Add(observedTimeDistance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double getExpectedDistance(IEnumerable<ObservedTimeDistance> prevObservedTimeDistances, ObservedTimeDistance observedTimeDistance)
|
||||||
|
{
|
||||||
|
var observations = prevObservedTimeDistances.Count();
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
double sum = 0;
|
||||||
|
|
||||||
|
// Looping this in reverse allows us to break before going through all elements, as we're only interested in the most recent ones.
|
||||||
|
for (int i = observations - 1; i >= 0; --i)
|
||||||
|
{
|
||||||
|
var prevObservedTimeDistance = prevObservedTimeDistances.ElementAt(i);
|
||||||
|
|
||||||
|
// Only consider observations within the last few seconds - this allows the map to build spacing up/down over time, but prevents it from being too sudden.
|
||||||
|
if (observedTimeDistance.ObservationTime - prevObservedTimeDistance.ObservationTime > observation_lifetime)
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Only consider observations which have a similar time difference - this leniency allows handling of multi-BPM maps which speed up/down slowly.
|
||||||
|
if (Math.Abs(observedTimeDistance.DeltaTime - prevObservedTimeDistance.DeltaTime) > similar_time_leniency)
|
||||||
|
break;
|
||||||
|
|
||||||
|
count += 1;
|
||||||
|
sum += prevObservedTimeDistance.Distance / Math.Max(prevObservedTimeDistance.DeltaTime, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sum / Math.Max(count, 1) * observedTimeDistance.DeltaTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class IssueTemplateIrregularSpacing : IssueTemplate
|
||||||
|
{
|
||||||
|
protected IssueTemplateIrregularSpacing(ICheck check, IssueType issueType)
|
||||||
|
: base(check, issueType, "Expected {0:0} px spacing like previous objects, currently {1:0}.")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Issue Create(double expected, double actual, params HitObject[] hitObjects) => new Issue(hitObjects, this, expected, actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IssueTemplateIrregularSpacingProblem : IssueTemplateIrregularSpacing
|
||||||
|
{
|
||||||
|
public IssueTemplateIrregularSpacingProblem(ICheck check)
|
||||||
|
: base(check, IssueType.Problem)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IssueTemplateIrregularSpacingWarning : IssueTemplateIrregularSpacing
|
||||||
|
{
|
||||||
|
public IssueTemplateIrregularSpacingWarning(ICheck check)
|
||||||
|
: base(check, IssueType.Warning)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
private readonly List<ICheck> checks = new List<ICheck>
|
private readonly List<ICheck> checks = new List<ICheck>
|
||||||
{
|
{
|
||||||
new CheckOffscreenObjects()
|
// Compose
|
||||||
|
new CheckOffscreenObjects(),
|
||||||
|
|
||||||
|
// Spread
|
||||||
|
new CheckTimeDistanceEquality(),
|
||||||
|
new CheckLowDiffOverlaps()
|
||||||
};
|
};
|
||||||
|
|
||||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||||
|
@ -11,6 +11,7 @@ using osu.Game.Rulesets.Objects;
|
|||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Skinning;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Mods
|
namespace osu.Game.Rulesets.Osu.Mods
|
||||||
{
|
{
|
||||||
@ -110,6 +111,9 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
// hide elements we don't care about.
|
// hide elements we don't care about.
|
||||||
// todo: hide background
|
// todo: hide background
|
||||||
|
|
||||||
|
spinner.Body.OnSkinChanged += () => hideSpinnerApproachCircle(spinner);
|
||||||
|
hideSpinnerApproachCircle(spinner);
|
||||||
|
|
||||||
using (spinner.BeginAbsoluteSequence(fadeStartTime))
|
using (spinner.BeginAbsoluteSequence(fadeStartTime))
|
||||||
spinner.FadeOut(fadeDuration);
|
spinner.FadeOut(fadeDuration);
|
||||||
|
|
||||||
@ -160,5 +164,15 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void hideSpinnerApproachCircle(DrawableSpinner spinner)
|
||||||
|
{
|
||||||
|
var approachCircle = (spinner.Body.Drawable as IHasApproachCircle)?.ApproachCircle;
|
||||||
|
if (approachCircle == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using (spinner.BeginAbsoluteSequence(spinner.HitObject.StartTime - spinner.HitObject.TimePreempt))
|
||||||
|
approachCircle.Hide();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ using osu.Framework.Input.Bindings;
|
|||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Judgements;
|
using osu.Game.Rulesets.Osu.Judgements;
|
||||||
|
using osu.Game.Rulesets.Osu.Skinning;
|
||||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
@ -19,7 +20,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||||
{
|
{
|
||||||
public class DrawableHitCircle : DrawableOsuHitObject, IHasMainCirclePiece
|
public class DrawableHitCircle : DrawableOsuHitObject, IHasMainCirclePiece, IHasApproachCircle
|
||||||
{
|
{
|
||||||
public OsuAction? HitAction => HitArea.HitAction;
|
public OsuAction? HitAction => HitArea.HitAction;
|
||||||
protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle;
|
protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle;
|
||||||
@ -28,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
public HitReceptor HitArea { get; private set; }
|
public HitReceptor HitArea { get; private set; }
|
||||||
public SkinnableDrawable CirclePiece { get; private set; }
|
public SkinnableDrawable CirclePiece { get; private set; }
|
||||||
|
|
||||||
|
Drawable IHasApproachCircle.ApproachCircle => ApproachCircle;
|
||||||
|
|
||||||
private Container scaleContainer;
|
private Container scaleContainer;
|
||||||
private InputManager inputManager;
|
private InputManager inputManager;
|
||||||
|
|
||||||
|
@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
|
|
||||||
public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result;
|
public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result;
|
||||||
|
|
||||||
|
public SkinnableDrawable Body { get; private set; }
|
||||||
|
|
||||||
public SpinnerRotationTracker RotationTracker { get; private set; }
|
public SpinnerRotationTracker RotationTracker { get; private set; }
|
||||||
|
|
||||||
private SpinnerSpmCalculator spmCalculator;
|
private SpinnerSpmCalculator spmCalculator;
|
||||||
@ -86,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
RelativeSizeAxes = Axes.Y,
|
RelativeSizeAxes = Axes.Y,
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinner()),
|
Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinner()),
|
||||||
RotationTracker = new SpinnerRotationTracker(this)
|
RotationTracker = new SpinnerRotationTracker(this)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -10,7 +10,6 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
Cursor,
|
Cursor,
|
||||||
CursorTrail,
|
CursorTrail,
|
||||||
SliderScorePoint,
|
SliderScorePoint,
|
||||||
ApproachCircle,
|
|
||||||
ReverseArrow,
|
ReverseArrow,
|
||||||
HitCircleText,
|
HitCircleText,
|
||||||
SliderHeadHitCircle,
|
SliderHeadHitCircle,
|
||||||
|
18
osu.Game.Rulesets.Osu/Skinning/IHasApproachCircle.cs
Normal file
18
osu.Game.Rulesets.Osu/Skinning/IHasApproachCircle.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Skinning
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A common interface between implementations which provide an approach circle.
|
||||||
|
/// </summary>
|
||||||
|
public interface IHasApproachCircle
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The approach circle drawable.
|
||||||
|
/// </summary>
|
||||||
|
Drawable ApproachCircle { get; }
|
||||||
|
}
|
||||||
|
}
|
@ -55,28 +55,40 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Texture = source.GetTexture("spinner-bottom")
|
Texture = source.GetTexture("spinner-bottom"),
|
||||||
},
|
},
|
||||||
discTop = new Sprite
|
discTop = new Sprite
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Texture = source.GetTexture("spinner-top")
|
Texture = source.GetTexture("spinner-top"),
|
||||||
},
|
},
|
||||||
fixedMiddle = new Sprite
|
fixedMiddle = new Sprite
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Texture = source.GetTexture("spinner-middle")
|
Texture = source.GetTexture("spinner-middle"),
|
||||||
},
|
},
|
||||||
spinningMiddle = new Sprite
|
spinningMiddle = new Sprite
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Texture = source.GetTexture("spinner-middle2")
|
Texture = source.GetTexture("spinner-middle2"),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!(source.FindProvider(s => s.GetTexture("spinner-top") != null) is DefaultLegacySkin))
|
||||||
|
{
|
||||||
|
AddInternal(ApproachCircle = new Sprite
|
||||||
|
{
|
||||||
|
Anchor = Anchor.TopCentre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Texture = source.GetTexture("spinner-approachcircle"),
|
||||||
|
Scale = new Vector2(SPRITE_SCALE * 1.86f),
|
||||||
|
Y = SPINNER_Y_CENTRE,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void UpdateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
|
protected override void UpdateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
|
||||||
|
@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
spinnerBlink = source.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.SpinnerNoBlink)?.Value != true;
|
spinnerBlink = source.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.SpinnerNoBlink)?.Value != true;
|
||||||
|
|
||||||
AddRangeInternal(new Drawable[]
|
AddRangeInternal(new[]
|
||||||
{
|
{
|
||||||
new Sprite
|
new Sprite
|
||||||
{
|
{
|
||||||
@ -68,6 +68,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
Origin = Anchor.TopLeft,
|
Origin = Anchor.TopLeft,
|
||||||
Scale = new Vector2(SPRITE_SCALE)
|
Scale = new Vector2(SPRITE_SCALE)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
ApproachCircle = new Sprite
|
||||||
|
{
|
||||||
|
Anchor = Anchor.TopCentre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Texture = source.GetTexture("spinner-approachcircle"),
|
||||||
|
Scale = new Vector2(SPRITE_SCALE * 1.86f),
|
||||||
|
Y = SPINNER_Y_CENTRE,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -15,8 +15,10 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||||
{
|
{
|
||||||
public abstract class LegacySpinner : CompositeDrawable
|
public abstract class LegacySpinner : CompositeDrawable, IHasApproachCircle
|
||||||
{
|
{
|
||||||
|
public const float SPRITE_SCALE = 0.625f;
|
||||||
|
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// All constants are in osu!stable's gamefield space, which is shifted 16px downwards.
|
/// All constants are in osu!stable's gamefield space, which is shifted 16px downwards.
|
||||||
/// This offset is negated in both osu!stable and osu!lazer to bring all constants into window-space.
|
/// This offset is negated in both osu!stable and osu!lazer to bring all constants into window-space.
|
||||||
@ -26,12 +28,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
|
|
||||||
protected const float SPINNER_Y_CENTRE = SPINNER_TOP_OFFSET + 219f;
|
protected const float SPINNER_Y_CENTRE = SPINNER_TOP_OFFSET + 219f;
|
||||||
|
|
||||||
protected const float SPRITE_SCALE = 0.625f;
|
|
||||||
|
|
||||||
private const float spm_hide_offset = 50f;
|
private const float spm_hide_offset = 50f;
|
||||||
|
|
||||||
protected DrawableSpinner DrawableSpinner { get; private set; }
|
protected DrawableSpinner DrawableSpinner { get; private set; }
|
||||||
|
|
||||||
|
public Drawable ApproachCircle { get; protected set; }
|
||||||
|
|
||||||
private Sprite spin;
|
private Sprite spin;
|
||||||
private Sprite clear;
|
private Sprite clear;
|
||||||
|
|
||||||
@ -175,6 +177,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out);
|
spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
using (BeginAbsoluteSequence(d.HitObject.StartTime))
|
||||||
|
ApproachCircle?.ScaleTo(SPRITE_SCALE * 0.1f, d.HitObject.Duration);
|
||||||
|
|
||||||
double spinFadeOutLength = Math.Min(400, d.HitObject.Duration);
|
double spinFadeOutLength = Math.Min(400, d.HitObject.Duration);
|
||||||
|
|
||||||
using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true))
|
using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - spinFadeOutLength, true))
|
||||||
|
@ -28,6 +28,8 @@ namespace osu.Game.Tests.Chat
|
|||||||
[TestCase(LinkAction.OpenBeatmapSet, "123", "https://dev.ppy.sh/beatmapsets/123")]
|
[TestCase(LinkAction.OpenBeatmapSet, "123", "https://dev.ppy.sh/beatmapsets/123")]
|
||||||
[TestCase(LinkAction.OpenBeatmapSet, "123", "https://dev.ppy.sh/beatmapsets/123/whatever")]
|
[TestCase(LinkAction.OpenBeatmapSet, "123", "https://dev.ppy.sh/beatmapsets/123/whatever")]
|
||||||
[TestCase(LinkAction.External, "https://dev.ppy.sh/beatmapsets/abc", "https://dev.ppy.sh/beatmapsets/abc")]
|
[TestCase(LinkAction.External, "https://dev.ppy.sh/beatmapsets/abc", "https://dev.ppy.sh/beatmapsets/abc")]
|
||||||
|
[TestCase(LinkAction.External, "https://dev.ppy.sh/beatmapsets/discussions", "https://dev.ppy.sh/beatmapsets/discussions")]
|
||||||
|
[TestCase(LinkAction.External, "https://dev.ppy.sh/beatmapsets/discussions/123", "https://dev.ppy.sh/beatmapsets/discussions/123")]
|
||||||
public void TestBeatmapLinks(LinkAction expectedAction, string expectedArg, string link)
|
public void TestBeatmapLinks(LinkAction expectedAction, string expectedArg, string link)
|
||||||
{
|
{
|
||||||
MessageFormatter.WebsiteRootUrl = "dev.ppy.sh";
|
MessageFormatter.WebsiteRootUrl = "dev.ppy.sh";
|
||||||
|
@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD;
|
||||||
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Extensions
|
namespace osu.Game.Extensions
|
||||||
@ -57,6 +58,9 @@ namespace osu.Game.Extensions
|
|||||||
component.Anchor = info.Anchor;
|
component.Anchor = info.Anchor;
|
||||||
component.Origin = info.Origin;
|
component.Origin = info.Origin;
|
||||||
|
|
||||||
|
if (component is ISkinnableDrawable skinnable)
|
||||||
|
skinnable.UsesFixedAnchor = info.UsesFixedAnchor;
|
||||||
|
|
||||||
if (component is Container container)
|
if (component is Container container)
|
||||||
{
|
{
|
||||||
foreach (var child in info.Children)
|
foreach (var child in info.Children)
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
||||||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
|
|
||||||
<xs:element name="Weavers">
|
|
||||||
<xs:complexType>
|
|
||||||
<xs:all>
|
|
||||||
<xs:element name="Realm" minOccurs="0" maxOccurs="1">
|
|
||||||
<xs:complexType>
|
|
||||||
<xs:attribute name="DisableAnalytics" type="xs:boolean">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>Disables anonymized usage information from being sent on build. Read more about what data is being collected and why here: https://github.com/realm/realm-dotnet/blob/master/Realm/Realm.Fody/Common/Analytics.cs</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:attribute>
|
|
||||||
</xs:complexType>
|
|
||||||
</xs:element>
|
|
||||||
</xs:all>
|
|
||||||
<xs:attribute name="VerifyAssembly" type="xs:boolean">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:attribute>
|
|
||||||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:attribute>
|
|
||||||
<xs:attribute name="GenerateXsd" type="xs:boolean">
|
|
||||||
<xs:annotation>
|
|
||||||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
|
|
||||||
</xs:annotation>
|
|
||||||
</xs:attribute>
|
|
||||||
</xs:complexType>
|
|
||||||
</xs:element>
|
|
||||||
</xs:schema>
|
|
@ -154,6 +154,10 @@ namespace osu.Game.Online.Chat
|
|||||||
case "beatmapsets":
|
case "beatmapsets":
|
||||||
case "d":
|
case "d":
|
||||||
{
|
{
|
||||||
|
if (mainArg == "discussions")
|
||||||
|
// handle discussion links externally for now
|
||||||
|
return new LinkDetails(LinkAction.External, url);
|
||||||
|
|
||||||
if (args.Length > 4 && int.TryParse(args[4], out var id))
|
if (args.Length > 4 && int.TryParse(args[4], out var id))
|
||||||
// https://osu.ppy.sh/beatmapsets/1154158#osu/2768184
|
// https://osu.ppy.sh/beatmapsets/1154158#osu/2768184
|
||||||
return new LinkDetails(LinkAction.OpenBeatmap, id.ToString());
|
return new LinkDetails(LinkAction.OpenBeatmap, id.ToString());
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Runtime;
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -11,7 +10,6 @@ namespace osu.Game.Performance
|
|||||||
public class HighPerformanceSession : Component
|
public class HighPerformanceSession : Component
|
||||||
{
|
{
|
||||||
private readonly IBindable<bool> localUserPlaying = new Bindable<bool>();
|
private readonly IBindable<bool> localUserPlaying = new Bindable<bool>();
|
||||||
private GCLatencyMode originalGCMode;
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuGame game)
|
private void load(OsuGame game)
|
||||||
@ -34,14 +32,10 @@ namespace osu.Game.Performance
|
|||||||
|
|
||||||
protected virtual void EnableHighPerformanceSession()
|
protected virtual void EnableHighPerformanceSession()
|
||||||
{
|
{
|
||||||
originalGCMode = GCSettings.LatencyMode;
|
|
||||||
GCSettings.LatencyMode = GCLatencyMode.LowLatency;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void DisableHighPerformanceSession()
|
protected virtual void DisableHighPerformanceSession()
|
||||||
{
|
{
|
||||||
if (GCSettings.LatencyMode == GCLatencyMode.LowLatency)
|
|
||||||
GCSettings.LatencyMode = originalGCMode;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,8 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
private HUDOverlay hud { get; set; }
|
private HUDOverlay hud { get; set; }
|
||||||
|
|
||||||
|
public bool UsesFixedAnchor { get; set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours)
|
private void load(OsuColour colours)
|
||||||
{
|
{
|
||||||
|
@ -17,6 +17,8 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
private HUDOverlay hud { get; set; }
|
private HUDOverlay hud { get; set; }
|
||||||
|
|
||||||
|
public bool UsesFixedAnchor { get; set; }
|
||||||
|
|
||||||
public DefaultComboCounter()
|
public DefaultComboCounter()
|
||||||
{
|
{
|
||||||
Current.Value = DisplayedCount = 0;
|
Current.Value = DisplayedCount = 0;
|
||||||
|
@ -72,6 +72,8 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool UsesFixedAnchor { get; set; }
|
||||||
|
|
||||||
public DefaultHealthDisplay()
|
public DefaultHealthDisplay()
|
||||||
{
|
{
|
||||||
Size = new Vector2(1, 5);
|
Size = new Vector2(1, 5);
|
||||||
|
@ -20,6 +20,8 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
private HUDOverlay hud { get; set; }
|
private HUDOverlay hud { get; set; }
|
||||||
|
|
||||||
|
public bool UsesFixedAnchor { get; set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours)
|
private void load(OsuColour colours)
|
||||||
{
|
{
|
||||||
|
@ -22,6 +22,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuColour colours { get; set; }
|
private OsuColour colours { get; set; }
|
||||||
|
|
||||||
|
public bool UsesFixedAnchor { get; set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
private void load(DrawableRuleset drawableRuleset)
|
private void load(DrawableRuleset drawableRuleset)
|
||||||
{
|
{
|
||||||
|
@ -59,6 +59,8 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
set => counterContainer.Alpha = value ? 1 : 0;
|
set => counterContainer.Alpha = value ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool UsesFixedAnchor { get; set; }
|
||||||
|
|
||||||
public LegacyComboCounter()
|
public LegacyComboCounter()
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Both;
|
AutoSizeAxes = Axes.Both;
|
||||||
|
@ -32,6 +32,9 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
|
|
||||||
public Anchor Origin { get; set; }
|
public Anchor Origin { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc cref="ISkinnableDrawable.UsesFixedAnchor"/>
|
||||||
|
public bool UsesFixedAnchor { get; set; }
|
||||||
|
|
||||||
public List<SkinnableInfo> Children { get; } = new List<SkinnableInfo>();
|
public List<SkinnableInfo> Children { get; } = new List<SkinnableInfo>();
|
||||||
|
|
||||||
[JsonConstructor]
|
[JsonConstructor]
|
||||||
@ -53,6 +56,9 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
Anchor = component.Anchor;
|
Anchor = component.Anchor;
|
||||||
Origin = component.Origin;
|
Origin = component.Origin;
|
||||||
|
|
||||||
|
if (component is ISkinnableDrawable skinnable)
|
||||||
|
UsesFixedAnchor = skinnable.UsesFixedAnchor;
|
||||||
|
|
||||||
if (component is Container<Drawable> container)
|
if (component is Container<Drawable> container)
|
||||||
{
|
{
|
||||||
foreach (var child in container.OfType<ISkinnableDrawable>().OfType<Drawable>())
|
foreach (var child in container.OfType<ISkinnableDrawable>().OfType<Drawable>())
|
||||||
|
@ -78,6 +78,8 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
private IClock referenceClock;
|
private IClock referenceClock;
|
||||||
|
|
||||||
|
public bool UsesFixedAnchor { get; set; }
|
||||||
|
|
||||||
public SongProgress()
|
public SongProgress()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
|
@ -149,13 +149,21 @@ namespace osu.Game.Skinning.Editor
|
|||||||
{
|
{
|
||||||
foreach (var c in SelectedBlueprints)
|
foreach (var c in SelectedBlueprints)
|
||||||
{
|
{
|
||||||
Drawable drawable = (Drawable)c.Item;
|
var item = c.Item;
|
||||||
|
Drawable drawable = (Drawable)item;
|
||||||
|
|
||||||
drawable.Position += drawable.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta);
|
drawable.Position += drawable.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta);
|
||||||
|
|
||||||
|
if (item.UsesFixedAnchor) continue;
|
||||||
|
|
||||||
|
applyClosestAnchor(drawable);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void applyClosestAnchor(Drawable drawable) => applyAnchor(drawable, getClosestAnchor(drawable));
|
||||||
|
|
||||||
protected override void OnSelectionChanged()
|
protected override void OnSelectionChanged()
|
||||||
{
|
{
|
||||||
base.OnSelectionChanged();
|
base.OnSelectionChanged();
|
||||||
@ -171,20 +179,27 @@ namespace osu.Game.Skinning.Editor
|
|||||||
|
|
||||||
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<ISkinnableDrawable>> selection)
|
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<ISkinnableDrawable>> selection)
|
||||||
{
|
{
|
||||||
|
var closestItem = new TernaryStateRadioMenuItem("Closest", MenuItemType.Standard, _ => applyClosestAnchors())
|
||||||
|
{
|
||||||
|
State = { Value = GetStateFromSelection(selection, c => !c.Item.UsesFixedAnchor) }
|
||||||
|
};
|
||||||
|
|
||||||
yield return new OsuMenuItem("Anchor")
|
yield return new OsuMenuItem("Anchor")
|
||||||
{
|
{
|
||||||
Items = createAnchorItems(d => d.Anchor, applyAnchor).ToArray()
|
Items = createAnchorItems((d, a) => d.UsesFixedAnchor && ((Drawable)d).Anchor == a, applyFixedAnchors)
|
||||||
|
.Prepend(closestItem)
|
||||||
|
.ToArray()
|
||||||
};
|
};
|
||||||
|
|
||||||
yield return new OsuMenuItem("Origin")
|
yield return new OsuMenuItem("Origin")
|
||||||
{
|
{
|
||||||
Items = createAnchorItems(d => d.Origin, applyOrigin).ToArray()
|
Items = createAnchorItems((d, o) => ((Drawable)d).Origin == o, applyOrigins).ToArray()
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var item in base.GetContextMenuItemsForSelection(selection))
|
foreach (var item in base.GetContextMenuItemsForSelection(selection))
|
||||||
yield return item;
|
yield return item;
|
||||||
|
|
||||||
IEnumerable<TernaryStateMenuItem> createAnchorItems(Func<Drawable, Anchor> checkFunction, Action<Anchor> applyFunction)
|
IEnumerable<TernaryStateMenuItem> createAnchorItems(Func<ISkinnableDrawable, Anchor, bool> checkFunction, Action<Anchor> applyFunction)
|
||||||
{
|
{
|
||||||
var displayableAnchors = new[]
|
var displayableAnchors = new[]
|
||||||
{
|
{
|
||||||
@ -198,12 +213,11 @@ namespace osu.Game.Skinning.Editor
|
|||||||
Anchor.BottomCentre,
|
Anchor.BottomCentre,
|
||||||
Anchor.BottomRight,
|
Anchor.BottomRight,
|
||||||
};
|
};
|
||||||
|
|
||||||
return displayableAnchors.Select(a =>
|
return displayableAnchors.Select(a =>
|
||||||
{
|
{
|
||||||
return new TernaryStateRadioMenuItem(a.ToString(), MenuItemType.Standard, _ => applyFunction(a))
|
return new TernaryStateRadioMenuItem(a.ToString(), MenuItemType.Standard, _ => applyFunction(a))
|
||||||
{
|
{
|
||||||
State = { Value = GetStateFromSelection(selection, c => checkFunction((Drawable)c.Item) == a) }
|
State = { Value = GetStateFromSelection(selection, c => checkFunction(c.Item, a)) }
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -215,15 +229,21 @@ namespace osu.Game.Skinning.Editor
|
|||||||
drawable.Parent.ToLocalSpace(screenSpacePosition) - drawable.AnchorPosition;
|
drawable.Parent.ToLocalSpace(screenSpacePosition) - drawable.AnchorPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyOrigin(Anchor anchor)
|
private void applyOrigins(Anchor origin)
|
||||||
{
|
{
|
||||||
foreach (var item in SelectedItems)
|
foreach (var item in SelectedItems)
|
||||||
{
|
{
|
||||||
var drawable = (Drawable)item;
|
var drawable = (Drawable)item;
|
||||||
|
|
||||||
|
if (origin == drawable.Origin) continue;
|
||||||
|
|
||||||
var previousOrigin = drawable.OriginPosition;
|
var previousOrigin = drawable.OriginPosition;
|
||||||
drawable.Origin = anchor;
|
drawable.Origin = origin;
|
||||||
drawable.Position += drawable.OriginPosition - previousOrigin;
|
drawable.Position += drawable.OriginPosition - previousOrigin;
|
||||||
|
|
||||||
|
if (item.UsesFixedAnchor) continue;
|
||||||
|
|
||||||
|
applyClosestAnchor(drawable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,18 +254,86 @@ namespace osu.Game.Skinning.Editor
|
|||||||
private Quad getSelectionQuad() =>
|
private Quad getSelectionQuad() =>
|
||||||
GetSurroundingQuad(SelectedBlueprints.SelectMany(b => b.Item.ScreenSpaceDrawQuad.GetVertices().ToArray()));
|
GetSurroundingQuad(SelectedBlueprints.SelectMany(b => b.Item.ScreenSpaceDrawQuad.GetVertices().ToArray()));
|
||||||
|
|
||||||
private void applyAnchor(Anchor anchor)
|
private void applyFixedAnchors(Anchor anchor)
|
||||||
{
|
{
|
||||||
foreach (var item in SelectedItems)
|
foreach (var item in SelectedItems)
|
||||||
{
|
{
|
||||||
var drawable = (Drawable)item;
|
var drawable = (Drawable)item;
|
||||||
|
|
||||||
var previousAnchor = drawable.AnchorPosition;
|
item.UsesFixedAnchor = true;
|
||||||
drawable.Anchor = anchor;
|
applyAnchor(drawable, anchor);
|
||||||
drawable.Position -= drawable.AnchorPosition - previousAnchor;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void applyClosestAnchors()
|
||||||
|
{
|
||||||
|
foreach (var item in SelectedItems)
|
||||||
|
{
|
||||||
|
item.UsesFixedAnchor = false;
|
||||||
|
applyClosestAnchor((Drawable)item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Anchor getClosestAnchor(Drawable drawable)
|
||||||
|
{
|
||||||
|
var parent = drawable.Parent;
|
||||||
|
|
||||||
|
if (parent == null)
|
||||||
|
return drawable.Anchor;
|
||||||
|
|
||||||
|
var screenPosition = getScreenPosition();
|
||||||
|
|
||||||
|
var absolutePosition = parent.ToLocalSpace(screenPosition);
|
||||||
|
var factor = parent.RelativeToAbsoluteFactor;
|
||||||
|
|
||||||
|
var result = default(Anchor);
|
||||||
|
|
||||||
|
static Anchor getAnchorFromPosition(float xOrY, Anchor anchor0, Anchor anchor1, Anchor anchor2)
|
||||||
|
{
|
||||||
|
if (xOrY >= 2 / 3f)
|
||||||
|
return anchor2;
|
||||||
|
|
||||||
|
if (xOrY >= 1 / 3f)
|
||||||
|
return anchor1;
|
||||||
|
|
||||||
|
return anchor0;
|
||||||
|
}
|
||||||
|
|
||||||
|
result |= getAnchorFromPosition(absolutePosition.X / factor.X, Anchor.x0, Anchor.x1, Anchor.x2);
|
||||||
|
result |= getAnchorFromPosition(absolutePosition.Y / factor.Y, Anchor.y0, Anchor.y1, Anchor.y2);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
Vector2 getScreenPosition()
|
||||||
|
{
|
||||||
|
var quad = drawable.ScreenSpaceDrawQuad;
|
||||||
|
var origin = drawable.Origin;
|
||||||
|
|
||||||
|
var pos = quad.TopLeft;
|
||||||
|
|
||||||
|
if (origin.HasFlagFast(Anchor.x2))
|
||||||
|
pos.X += quad.Width;
|
||||||
|
else if (origin.HasFlagFast(Anchor.x1))
|
||||||
|
pos.X += quad.Width / 2f;
|
||||||
|
|
||||||
|
if (origin.HasFlagFast(Anchor.y2))
|
||||||
|
pos.Y += quad.Height;
|
||||||
|
else if (origin.HasFlagFast(Anchor.y1))
|
||||||
|
pos.Y += quad.Height / 2f;
|
||||||
|
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applyAnchor(Drawable drawable, Anchor anchor)
|
||||||
|
{
|
||||||
|
if (anchor == drawable.Anchor) return;
|
||||||
|
|
||||||
|
var previousAnchor = drawable.AnchorPosition;
|
||||||
|
drawable.Anchor = anchor;
|
||||||
|
drawable.Position -= drawable.AnchorPosition - previousAnchor;
|
||||||
|
}
|
||||||
|
|
||||||
private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference)
|
private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference)
|
||||||
{
|
{
|
||||||
// cancel out scale in axes we don't care about (based on which drag handle was used).
|
// cancel out scale in axes we don't care about (based on which drag handle was used).
|
||||||
|
@ -14,5 +14,12 @@ namespace osu.Game.Skinning
|
|||||||
/// Whether this component should be editable by an end user.
|
/// Whether this component should be editable by an end user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
bool IsEditable => true;
|
bool IsEditable => true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// In the context of the skin layout editor, whether this <see cref="ISkinnableDrawable"/> has a permanent anchor defined.
|
||||||
|
/// If <see langword="false"/>, this <see cref="ISkinnableDrawable"/>'s <see cref="Drawable.Anchor"/> is automatically determined by proximity,
|
||||||
|
/// If <see langword="true"/>, a fixed anchor point has been defined.
|
||||||
|
/// </summary>
|
||||||
|
bool UsesFixedAnchor { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,8 @@ namespace osu.Game.Skinning
|
|||||||
{
|
{
|
||||||
public class LegacyAccuracyCounter : GameplayAccuracyCounter, ISkinnableDrawable
|
public class LegacyAccuracyCounter : GameplayAccuracyCounter, ISkinnableDrawable
|
||||||
{
|
{
|
||||||
|
public bool UsesFixedAnchor { get; set; }
|
||||||
|
|
||||||
public LegacyAccuracyCounter()
|
public LegacyAccuracyCounter()
|
||||||
{
|
{
|
||||||
Anchor = Anchor.TopRight;
|
Anchor = Anchor.TopRight;
|
||||||
|
@ -27,6 +27,8 @@ namespace osu.Game.Skinning
|
|||||||
|
|
||||||
private bool isNewStyle;
|
private bool isNewStyle;
|
||||||
|
|
||||||
|
public bool UsesFixedAnchor { get; set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(ISkinSource source)
|
private void load(ISkinSource source)
|
||||||
{
|
{
|
||||||
|
@ -13,6 +13,8 @@ namespace osu.Game.Skinning
|
|||||||
protected override double RollingDuration => 1000;
|
protected override double RollingDuration => 1000;
|
||||||
protected override Easing RollingEasing => Easing.Out;
|
protected override Easing RollingEasing => Easing.Out;
|
||||||
|
|
||||||
|
public bool UsesFixedAnchor { get; set; }
|
||||||
|
|
||||||
public LegacyScoreCounter()
|
public LegacyScoreCounter()
|
||||||
: base(6)
|
: base(6)
|
||||||
{
|
{
|
||||||
|
@ -17,6 +17,8 @@ namespace osu.Game.Skinning
|
|||||||
{
|
{
|
||||||
public bool IsEditable => false;
|
public bool IsEditable => false;
|
||||||
|
|
||||||
|
public bool UsesFixedAnchor { get; set; }
|
||||||
|
|
||||||
private readonly Action<Container> applyDefaults;
|
private readonly Action<Container> applyDefaults;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -99,5 +99,6 @@
|
|||||||
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
||||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2021.115.0" ExcludeAssets="all" />
|
<PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2021.115.0" ExcludeAssets="all" />
|
||||||
|
<PackageReference Include="Realm" Version="10.2.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
Reference in New Issue
Block a user