mirror of
https://github.com/osukey/osukey.git
synced 2025-05-09 23:57:18 +09:00
Merge branch 'master' into skin-editor-closest-anchor
This commit is contained in:
commit
1fff9a93b9
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
|
||||||
|
3
FodyWeavers.xml
Normal file
3
FodyWeavers.xml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||||
|
<Realm />
|
||||||
|
</Weavers>
|
@ -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!
|
||||||
|
|
||||||
|
@ -51,7 +51,11 @@
|
|||||||
<Reference Include="Java.Interop" />
|
<Reference Include="Java.Interop" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.614.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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,114 @@
|
|||||||
|
// 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 NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Transforms;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.Edit;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class TestSceneOsuEditorHitAnimations : TestSceneOsuEditor
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private OsuConfigManager config { get; set; }
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHitCircleAnimationDisable()
|
||||||
|
{
|
||||||
|
HitCircle hitCircle = null;
|
||||||
|
DrawableHitCircle drawableHitCircle = null;
|
||||||
|
|
||||||
|
AddStep("retrieve first hit circle", () => hitCircle = getHitCircle(0));
|
||||||
|
toggleAnimations(true);
|
||||||
|
seekSmoothlyTo(() => hitCircle.StartTime + 10);
|
||||||
|
|
||||||
|
AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle));
|
||||||
|
assertFutureTransforms(() => drawableHitCircle.CirclePiece, true);
|
||||||
|
|
||||||
|
AddStep("retrieve second hit circle", () => hitCircle = getHitCircle(1));
|
||||||
|
toggleAnimations(false);
|
||||||
|
seekSmoothlyTo(() => hitCircle.StartTime + 10);
|
||||||
|
|
||||||
|
AddStep("retrieve drawable", () => drawableHitCircle = (DrawableHitCircle)getDrawableObjectFor(hitCircle));
|
||||||
|
assertFutureTransforms(() => drawableHitCircle.CirclePiece, false);
|
||||||
|
AddAssert("hit circle has longer fade-out applied", () =>
|
||||||
|
{
|
||||||
|
var alphaTransform = drawableHitCircle.Transforms.Last(t => t.TargetMember == nameof(Alpha));
|
||||||
|
return alphaTransform.EndTime - alphaTransform.StartTime == DrawableOsuEditorRuleset.EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSliderAnimationDisable()
|
||||||
|
{
|
||||||
|
Slider slider = null;
|
||||||
|
DrawableSlider drawableSlider = null;
|
||||||
|
DrawableSliderRepeat sliderRepeat = null;
|
||||||
|
|
||||||
|
AddStep("retrieve first slider with repeats", () => slider = getSliderWithRepeats(0));
|
||||||
|
toggleAnimations(true);
|
||||||
|
seekSmoothlyTo(() => slider.StartTime + slider.SpanDuration + 10);
|
||||||
|
|
||||||
|
retrieveDrawables();
|
||||||
|
assertFutureTransforms(() => sliderRepeat, true);
|
||||||
|
|
||||||
|
AddStep("retrieve second slider with repeats", () => slider = getSliderWithRepeats(1));
|
||||||
|
toggleAnimations(false);
|
||||||
|
seekSmoothlyTo(() => slider.StartTime + slider.SpanDuration + 10);
|
||||||
|
|
||||||
|
retrieveDrawables();
|
||||||
|
assertFutureTransforms(() => sliderRepeat.Arrow, false);
|
||||||
|
seekSmoothlyTo(() => slider.GetEndTime());
|
||||||
|
AddAssert("slider has longer fade-out applied", () =>
|
||||||
|
{
|
||||||
|
var alphaTransform = drawableSlider.Transforms.Last(t => t.TargetMember == nameof(Alpha));
|
||||||
|
return alphaTransform.EndTime - alphaTransform.StartTime == DrawableOsuEditorRuleset.EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION;
|
||||||
|
});
|
||||||
|
|
||||||
|
void retrieveDrawables() =>
|
||||||
|
AddStep("retrieve drawables", () =>
|
||||||
|
{
|
||||||
|
drawableSlider = (DrawableSlider)getDrawableObjectFor(slider);
|
||||||
|
sliderRepeat = (DrawableSliderRepeat)getDrawableObjectFor(slider.NestedHitObjects.OfType<SliderRepeat>().First());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private HitCircle getHitCircle(int index)
|
||||||
|
=> EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(index);
|
||||||
|
|
||||||
|
private Slider getSliderWithRepeats(int index)
|
||||||
|
=> EditorBeatmap.HitObjects.OfType<Slider>().Where(s => s.RepeatCount >= 1).ElementAt(index);
|
||||||
|
|
||||||
|
private DrawableHitObject getDrawableObjectFor(HitObject hitObject)
|
||||||
|
=> this.ChildrenOfType<DrawableHitObject>().Single(ho => ho.HitObject == hitObject);
|
||||||
|
|
||||||
|
private IEnumerable<Transform> getTransformsRecursively(Drawable drawable)
|
||||||
|
=> drawable.ChildrenOfType<Drawable>().SelectMany(d => d.Transforms);
|
||||||
|
|
||||||
|
private void toggleAnimations(bool enabled)
|
||||||
|
=> AddStep($"toggle animations {(enabled ? "on" : "off")}", () => config.SetValue(OsuSetting.EditorHitAnimations, enabled));
|
||||||
|
|
||||||
|
private void seekSmoothlyTo(Func<double> targetTime)
|
||||||
|
{
|
||||||
|
AddStep("seek smoothly", () => EditorClock.SeekSmoothlyTo(targetTime.Invoke()));
|
||||||
|
AddUntilStep("wait for seek", () => Precision.AlmostEquals(targetTime.Invoke(), EditorClock.CurrentTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertFutureTransforms(Func<Drawable> getDrawable, bool hasFutureTransforms)
|
||||||
|
=> AddAssert($"object {(hasFutureTransforms ? "has" : "has no")} future transforms",
|
||||||
|
() => getTransformsRecursively(getDrawable()).Any(t => t.EndTime >= EditorClock.CurrentTime) == hasFutureTransforms);
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
{
|
{
|
||||||
public class DrawableOsuEditorRuleset : DrawableOsuRuleset
|
public class DrawableOsuEditorRuleset : DrawableOsuRuleset
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay.
|
||||||
|
/// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
|
||||||
|
/// </summary>
|
||||||
|
public const double EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION = 700;
|
||||||
|
|
||||||
public DrawableOsuEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
public DrawableOsuEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||||
: base(ruleset, beatmap, mods)
|
: base(ruleset, beatmap, mods)
|
||||||
{
|
{
|
||||||
@ -46,12 +52,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
d.ApplyCustomUpdateState += updateState;
|
d.ApplyCustomUpdateState += updateState;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay.
|
|
||||||
/// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points.
|
|
||||||
/// </summary>
|
|
||||||
private const double editor_hit_object_fade_out_extension = 700;
|
|
||||||
|
|
||||||
private void updateState(DrawableHitObject hitObject, ArmedState state)
|
private void updateState(DrawableHitObject hitObject, ArmedState state)
|
||||||
{
|
{
|
||||||
if (state == ArmedState.Idle || hitAnimations.Value)
|
if (state == ArmedState.Idle || hitAnimations.Value)
|
||||||
@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
if (hitObject is DrawableHitCircle circle)
|
if (hitObject is DrawableHitCircle circle)
|
||||||
{
|
{
|
||||||
circle.ApproachCircle
|
circle.ApproachCircle
|
||||||
.FadeOutFromOne(editor_hit_object_fade_out_extension * 4)
|
.FadeOutFromOne(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION * 4)
|
||||||
.Expire();
|
.Expire();
|
||||||
|
|
||||||
circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint);
|
circle.ApproachCircle.ScaleTo(1.1f, 300, Easing.OutQuint);
|
||||||
@ -69,14 +69,20 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
if (hitObject is IHasMainCirclePiece mainPieceContainer)
|
if (hitObject is IHasMainCirclePiece mainPieceContainer)
|
||||||
{
|
{
|
||||||
// clear any explode animation logic.
|
// clear any explode animation logic.
|
||||||
mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.HitStateUpdateTime, true);
|
// this is scheduled after children to ensure that the clear happens after invocations of ApplyCustomUpdateState on the circle piece's nested skinnables.
|
||||||
mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.HitStateUpdateTime, true);
|
ScheduleAfterChildren(() =>
|
||||||
|
{
|
||||||
|
if (hitObject.HitObject == null) return;
|
||||||
|
|
||||||
|
mainPieceContainer.CirclePiece.ApplyTransformsAt(hitObject.StateUpdateTime, true);
|
||||||
|
mainPieceContainer.CirclePiece.ClearTransformsAfter(hitObject.StateUpdateTime, true);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hitObject is DrawableSliderRepeat repeat)
|
if (hitObject is DrawableSliderRepeat repeat)
|
||||||
{
|
{
|
||||||
repeat.Arrow.ApplyTransformsAt(hitObject.HitStateUpdateTime, true);
|
repeat.Arrow.ApplyTransformsAt(hitObject.StateUpdateTime, true);
|
||||||
repeat.Arrow.ClearTransformsAfter(hitObject.HitStateUpdateTime, true);
|
repeat.Arrow.ClearTransformsAfter(hitObject.StateUpdateTime, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// adjust the visuals of top-level object types to make them stay on screen for longer than usual.
|
// adjust the visuals of top-level object types to make them stay on screen for longer than usual.
|
||||||
@ -93,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
hitObject.RemoveTransform(existing);
|
hitObject.RemoveTransform(existing);
|
||||||
|
|
||||||
using (hitObject.BeginAbsoluteSequence(hitObject.HitStateUpdateTime))
|
using (hitObject.BeginAbsoluteSequence(hitObject.HitStateUpdateTime))
|
||||||
hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire();
|
hitObject.FadeOut(EDITOR_HIT_OBJECT_FADE_OUT_EXTENSION).Expire();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
12
osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs
Normal file
12
osu.Game.Rulesets.Osu/Mods/IMutateApproachCircles.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Mods
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Any mod which affects the animation or visibility of approach circles. Should be used for incompatibility purposes.
|
||||||
|
/// </summary>
|
||||||
|
public interface IMutateApproachCircles
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
// 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.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
@ -11,7 +12,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Mods
|
namespace osu.Game.Rulesets.Osu.Mods
|
||||||
{
|
{
|
||||||
public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObject
|
public class OsuModApproachDifferent : Mod, IApplicableToDrawableHitObject, IMutateApproachCircles
|
||||||
{
|
{
|
||||||
public override string Name => "Approach Different";
|
public override string Name => "Approach Different";
|
||||||
public override string Acronym => "AD";
|
public override string Acronym => "AD";
|
||||||
@ -19,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override double ScoreMultiplier => 1;
|
public override double ScoreMultiplier => 1;
|
||||||
public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle;
|
public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle;
|
||||||
|
|
||||||
|
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
|
||||||
|
|
||||||
[SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 0)]
|
[SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 0)]
|
||||||
public BindableFloat Scale { get; } = new BindableFloat(4)
|
public BindableFloat Scale { get; } = new BindableFloat(4)
|
||||||
{
|
{
|
||||||
|
@ -11,15 +11,16 @@ 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
|
||||||
{
|
{
|
||||||
public class OsuModHidden : ModHidden
|
public class OsuModHidden : ModHidden, IMutateApproachCircles
|
||||||
{
|
{
|
||||||
public override string Description => @"Play with no approach circles and fading circles/sliders.";
|
public override string Description => @"Play with no approach circles and fading circles/sliders.";
|
||||||
public override double ScoreMultiplier => 1.06;
|
public override double ScoreMultiplier => 1.06;
|
||||||
|
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModTraceable), typeof(OsuModSpinIn) };
|
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
|
||||||
|
|
||||||
private const double fade_in_duration_multiplier = 0.4;
|
private const double fade_in_duration_multiplier = 0.4;
|
||||||
private const double fade_out_duration_multiplier = 0.3;
|
private const double fade_out_duration_multiplier = 0.3;
|
||||||
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adjusts the size of hit objects during their fade in animation.
|
/// Adjusts the size of hit objects during their fade in animation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment
|
public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment, IMutateApproachCircles
|
||||||
{
|
{
|
||||||
public override ModType Type => ModType.Fun;
|
public override ModType Type => ModType.Fun;
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
protected virtual float EndScale => 1;
|
protected virtual float EndScale => 1;
|
||||||
|
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpinIn), typeof(OsuModTraceable) };
|
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
|
||||||
|
|
||||||
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
||||||
{
|
{
|
||||||
|
@ -12,7 +12,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Mods
|
namespace osu.Game.Rulesets.Osu.Mods
|
||||||
{
|
{
|
||||||
public class OsuModSpinIn : ModWithVisibilityAdjustment
|
public class OsuModSpinIn : ModWithVisibilityAdjustment, IMutateApproachCircles
|
||||||
{
|
{
|
||||||
public override string Name => "Spin In";
|
public override string Name => "Spin In";
|
||||||
public override string Acronym => "SI";
|
public override string Acronym => "SI";
|
||||||
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override double ScoreMultiplier => 1;
|
public override double ScoreMultiplier => 1;
|
||||||
|
|
||||||
// todo: this mod should be able to be compatible with hidden with a bit of further implementation.
|
// todo: this mod should be able to be compatible with hidden with a bit of further implementation.
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModObjectScaleTween), typeof(OsuModHidden), typeof(OsuModTraceable) };
|
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
|
||||||
|
|
||||||
private const int rotate_offset = 360;
|
private const int rotate_offset = 360;
|
||||||
private const float rotate_starting_width = 2;
|
private const float rotate_starting_width = 2;
|
||||||
|
@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Skinning.Default;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Mods
|
namespace osu.Game.Rulesets.Osu.Mods
|
||||||
{
|
{
|
||||||
public class OsuModTraceable : ModWithVisibilityAdjustment
|
public class OsuModTraceable : ModWithVisibilityAdjustment, IMutateApproachCircles
|
||||||
{
|
{
|
||||||
public override string Name => "Traceable";
|
public override string Name => "Traceable";
|
||||||
public override string Acronym => "TC";
|
public override string Acronym => "TC";
|
||||||
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override string Description => "Put your faith in the approach circles...";
|
public override string Description => "Put your faith in the approach circles...";
|
||||||
public override double ScoreMultiplier => 1;
|
public override double ScoreMultiplier => 1;
|
||||||
|
|
||||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModHidden), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween) };
|
public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) };
|
||||||
|
|
||||||
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
||||||
{
|
{
|
||||||
|
@ -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";
|
||||||
|
101
osu.Game.Tests/Database/TestRealmKeyBindingStore.cs
Normal file
101
osu.Game.Tests/Database/TestRealmKeyBindingStore.cs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
// 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.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Input.Bindings;
|
||||||
|
using osu.Framework.Platform;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Input;
|
||||||
|
using osu.Game.Input.Bindings;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Database
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class TestRealmKeyBindingStore
|
||||||
|
{
|
||||||
|
private NativeStorage storage;
|
||||||
|
|
||||||
|
private RealmKeyBindingStore keyBindingStore;
|
||||||
|
|
||||||
|
private RealmContextFactory realmContextFactory;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
var directory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()));
|
||||||
|
|
||||||
|
storage = new NativeStorage(directory.FullName);
|
||||||
|
|
||||||
|
realmContextFactory = new RealmContextFactory(storage);
|
||||||
|
keyBindingStore = new RealmKeyBindingStore(realmContextFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDefaultsPopulationAndQuery()
|
||||||
|
{
|
||||||
|
Assert.That(query().Count, Is.EqualTo(0));
|
||||||
|
|
||||||
|
KeyBindingContainer testContainer = new TestKeyBindingContainer();
|
||||||
|
|
||||||
|
keyBindingStore.Register(testContainer);
|
||||||
|
|
||||||
|
Assert.That(query().Count, Is.EqualTo(3));
|
||||||
|
|
||||||
|
Assert.That(query().Where(k => k.ActionInt == (int)GlobalAction.Back).Count, Is.EqualTo(1));
|
||||||
|
Assert.That(query().Where(k => k.ActionInt == (int)GlobalAction.Select).Count, Is.EqualTo(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
private IQueryable<RealmKeyBinding> query() => realmContextFactory.Context.All<RealmKeyBinding>();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestUpdateViaQueriedReference()
|
||||||
|
{
|
||||||
|
KeyBindingContainer testContainer = new TestKeyBindingContainer();
|
||||||
|
|
||||||
|
keyBindingStore.Register(testContainer);
|
||||||
|
|
||||||
|
var backBinding = query().Single(k => k.ActionInt == (int)GlobalAction.Back);
|
||||||
|
|
||||||
|
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape }));
|
||||||
|
|
||||||
|
var tsr = ThreadSafeReference.Create(backBinding);
|
||||||
|
|
||||||
|
using (var usage = realmContextFactory.GetForWrite())
|
||||||
|
{
|
||||||
|
var binding = usage.Realm.ResolveReference(tsr);
|
||||||
|
binding.KeyCombination = new KeyCombination(InputKey.BackSpace);
|
||||||
|
|
||||||
|
usage.Commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
|
||||||
|
|
||||||
|
// check still correct after re-query.
|
||||||
|
backBinding = query().Single(k => k.ActionInt == (int)GlobalAction.Back);
|
||||||
|
Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TearDown]
|
||||||
|
public void TearDown()
|
||||||
|
{
|
||||||
|
realmContextFactory.Dispose();
|
||||||
|
storage.DeleteDirectory(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestKeyBindingContainer : KeyBindingContainer
|
||||||
|
{
|
||||||
|
public override IEnumerable<IKeyBinding> DefaultKeyBindings =>
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new KeyBinding(InputKey.Escape, GlobalAction.Back),
|
||||||
|
new KeyBinding(InputKey.Enter, GlobalAction.Select),
|
||||||
|
new KeyBinding(InputKey.Space, GlobalAction.Select),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -44,11 +44,9 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
{
|
{
|
||||||
TestDrawableHitObject dho = null;
|
TestDrawableHitObject dho = null;
|
||||||
TestLifetimeEntry entry = null;
|
TestLifetimeEntry entry = null;
|
||||||
AddStep("Create DHO", () =>
|
AddStep("Create DHO", () => Child = dho = new TestDrawableHitObject
|
||||||
{
|
{
|
||||||
dho = new TestDrawableHitObject(null);
|
Entry = entry = new TestLifetimeEntry(new HitObject())
|
||||||
dho.Apply(entry = new TestLifetimeEntry(new HitObject()));
|
|
||||||
Child = dho;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("KeepAlive = true", () =>
|
AddStep("KeepAlive = true", () =>
|
||||||
@ -81,12 +79,10 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
AddAssert("Lifetime is updated", () => entry.LifetimeStart == -TestLifetimeEntry.INITIAL_LIFETIME_OFFSET);
|
AddAssert("Lifetime is updated", () => entry.LifetimeStart == -TestLifetimeEntry.INITIAL_LIFETIME_OFFSET);
|
||||||
|
|
||||||
TestDrawableHitObject dho = null;
|
TestDrawableHitObject dho = null;
|
||||||
AddStep("Create DHO", () =>
|
AddStep("Create DHO", () => Child = dho = new TestDrawableHitObject
|
||||||
{
|
{
|
||||||
dho = new TestDrawableHitObject(null);
|
Entry = entry,
|
||||||
dho.Apply(entry);
|
SetLifetimeStartOnApply = true
|
||||||
Child = dho;
|
|
||||||
dho.SetLifetimeStartOnApply = true;
|
|
||||||
});
|
});
|
||||||
AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()));
|
AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()));
|
||||||
AddAssert("Lifetime is correct", () => dho.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY && entry.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY);
|
AddAssert("Lifetime is correct", () => dho.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY && entry.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY);
|
||||||
@ -97,11 +93,9 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
{
|
{
|
||||||
TestDrawableHitObject dho = null;
|
TestDrawableHitObject dho = null;
|
||||||
TestLifetimeEntry entry = null;
|
TestLifetimeEntry entry = null;
|
||||||
AddStep("Create DHO", () =>
|
AddStep("Create DHO", () => Child = dho = new TestDrawableHitObject
|
||||||
{
|
{
|
||||||
dho = new TestDrawableHitObject(null);
|
Entry = entry = new TestLifetimeEntry(new HitObject())
|
||||||
dho.Apply(entry = new TestLifetimeEntry(new HitObject()));
|
|
||||||
Child = dho;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("Set entry lifetime", () =>
|
AddStep("Set entry lifetime", () =>
|
||||||
@ -135,7 +129,7 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
|
|
||||||
public bool SetLifetimeStartOnApply;
|
public bool SetLifetimeStartOnApply;
|
||||||
|
|
||||||
public TestDrawableHitObject(HitObject hitObject)
|
public TestDrawableHitObject(HitObject hitObject = null)
|
||||||
: base(hitObject)
|
: base(hitObject)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,14 @@ namespace osu.Game.Tests.Mods
|
|||||||
Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object }));
|
Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestModIsCompatibleByItselfWithIncompatibleInterface()
|
||||||
|
{
|
||||||
|
var mod = new Mock<CustomMod1>();
|
||||||
|
mod.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(IModCompatibilitySpecification) });
|
||||||
|
Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object }));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestIncompatibleThroughTopLevel()
|
public void TestIncompatibleThroughTopLevel()
|
||||||
{
|
{
|
||||||
@ -34,6 +42,20 @@ namespace osu.Game.Tests.Mods
|
|||||||
Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod2.Object, mod1.Object }), Is.False);
|
Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod2.Object, mod1.Object }), Is.False);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestIncompatibleThroughInterface()
|
||||||
|
{
|
||||||
|
var mod1 = new Mock<CustomMod1>();
|
||||||
|
var mod2 = new Mock<CustomMod2>();
|
||||||
|
|
||||||
|
mod1.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(IModCompatibilitySpecification) });
|
||||||
|
mod2.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(IModCompatibilitySpecification) });
|
||||||
|
|
||||||
|
// Test both orderings.
|
||||||
|
Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod1.Object, mod2.Object }), Is.False);
|
||||||
|
Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod2.Object, mod1.Object }), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestMultiModIncompatibleWithTopLevel()
|
public void TestMultiModIncompatibleWithTopLevel()
|
||||||
{
|
{
|
||||||
@ -149,11 +171,15 @@ namespace osu.Game.Tests.Mods
|
|||||||
Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
|
Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class CustomMod1 : Mod
|
public abstract class CustomMod1 : Mod, IModCompatibilitySpecification
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class CustomMod2 : Mod
|
public abstract class CustomMod2 : Mod, IModCompatibilitySpecification
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IModCompatibilitySpecification
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -142,6 +142,9 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
|
|
||||||
foreach (var file in osuStorage.IgnoreFiles)
|
foreach (var file in osuStorage.IgnoreFiles)
|
||||||
{
|
{
|
||||||
|
// avoid touching realm files which may be a pipe and break everything.
|
||||||
|
// this is also done locally inside OsuStorage via the IgnoreFiles list.
|
||||||
|
if (file.EndsWith(".ini", StringComparison.Ordinal))
|
||||||
Assert.That(File.Exists(Path.Combine(originalDirectory, file)));
|
Assert.That(File.Exists(Path.Combine(originalDirectory, file)));
|
||||||
Assert.That(storage.Exists(file), Is.False);
|
Assert.That(storage.Exists(file), Is.False);
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,6 @@ namespace osu.Game.Tests.Visual
|
|||||||
typeof(FileStore),
|
typeof(FileStore),
|
||||||
typeof(ScoreManager),
|
typeof(ScoreManager),
|
||||||
typeof(BeatmapManager),
|
typeof(BeatmapManager),
|
||||||
typeof(KeyBindingStore),
|
|
||||||
typeof(SettingsStore),
|
typeof(SettingsStore),
|
||||||
typeof(RulesetConfigCache),
|
typeof(RulesetConfigCache),
|
||||||
typeof(OsuColour),
|
typeof(OsuColour),
|
||||||
|
16
osu.Game/Database/IHasGuidPrimaryKey.cs
Normal file
16
osu.Game/Database/IHasGuidPrimaryKey.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// 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 Newtonsoft.Json;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
namespace osu.Game.Database
|
||||||
|
{
|
||||||
|
public interface IHasGuidPrimaryKey
|
||||||
|
{
|
||||||
|
[JsonIgnore]
|
||||||
|
[PrimaryKey]
|
||||||
|
Guid ID { get; set; }
|
||||||
|
}
|
||||||
|
}
|
27
osu.Game/Database/IRealmFactory.cs
Normal file
27
osu.Game/Database/IRealmFactory.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// 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 Realms;
|
||||||
|
|
||||||
|
namespace osu.Game.Database
|
||||||
|
{
|
||||||
|
public interface IRealmFactory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The main realm context, bound to the update thread.
|
||||||
|
/// </summary>
|
||||||
|
Realm Context { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a fresh context for read usage.
|
||||||
|
/// </summary>
|
||||||
|
RealmContextFactory.RealmUsage GetForRead();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request a context for write usage.
|
||||||
|
/// This method may block if a write is already active on a different thread.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A usage containing a usable context.</returns>
|
||||||
|
RealmContextFactory.RealmWriteUsage GetForWrite();
|
||||||
|
}
|
||||||
|
}
|
@ -24,13 +24,15 @@ namespace osu.Game.Database
|
|||||||
public DbSet<BeatmapDifficulty> BeatmapDifficulty { get; set; }
|
public DbSet<BeatmapDifficulty> BeatmapDifficulty { get; set; }
|
||||||
public DbSet<BeatmapMetadata> BeatmapMetadata { get; set; }
|
public DbSet<BeatmapMetadata> BeatmapMetadata { get; set; }
|
||||||
public DbSet<BeatmapSetInfo> BeatmapSetInfo { get; set; }
|
public DbSet<BeatmapSetInfo> BeatmapSetInfo { get; set; }
|
||||||
public DbSet<DatabasedKeyBinding> DatabasedKeyBinding { get; set; }
|
|
||||||
public DbSet<DatabasedSetting> DatabasedSetting { get; set; }
|
public DbSet<DatabasedSetting> DatabasedSetting { get; set; }
|
||||||
public DbSet<FileInfo> FileInfo { get; set; }
|
public DbSet<FileInfo> FileInfo { get; set; }
|
||||||
public DbSet<RulesetInfo> RulesetInfo { get; set; }
|
public DbSet<RulesetInfo> RulesetInfo { get; set; }
|
||||||
public DbSet<SkinInfo> SkinInfo { get; set; }
|
public DbSet<SkinInfo> SkinInfo { get; set; }
|
||||||
public DbSet<ScoreInfo> ScoreInfo { get; set; }
|
public DbSet<ScoreInfo> ScoreInfo { get; set; }
|
||||||
|
|
||||||
|
// migrated to realm
|
||||||
|
public DbSet<DatabasedKeyBinding> DatabasedKeyBinding { get; set; }
|
||||||
|
|
||||||
private readonly string connectionString;
|
private readonly string connectionString;
|
||||||
|
|
||||||
private static readonly Lazy<OsuDbLoggerFactory> logger = new Lazy<OsuDbLoggerFactory>(() => new OsuDbLoggerFactory());
|
private static readonly Lazy<OsuDbLoggerFactory> logger = new Lazy<OsuDbLoggerFactory>(() => new OsuDbLoggerFactory());
|
||||||
|
208
osu.Game/Database/RealmContextFactory.cs
Normal file
208
osu.Game/Database/RealmContextFactory.cs
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
// 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.Threading;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Logging;
|
||||||
|
using osu.Framework.Platform;
|
||||||
|
using osu.Framework.Statistics;
|
||||||
|
using osu.Game.Input.Bindings;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
namespace osu.Game.Database
|
||||||
|
{
|
||||||
|
public class RealmContextFactory : Component, IRealmFactory
|
||||||
|
{
|
||||||
|
private readonly Storage storage;
|
||||||
|
|
||||||
|
private const string database_name = @"client";
|
||||||
|
|
||||||
|
private const int schema_version = 6;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lock object which is held for the duration of a write operation (via <see cref="GetForWrite"/>).
|
||||||
|
/// </summary>
|
||||||
|
private readonly object writeLock = new object();
|
||||||
|
|
||||||
|
private static readonly GlobalStatistic<int> reads = GlobalStatistics.Get<int>("Realm", "Get (Read)");
|
||||||
|
private static readonly GlobalStatistic<int> writes = GlobalStatistics.Get<int>("Realm", "Get (Write)");
|
||||||
|
private static readonly GlobalStatistic<int> refreshes = GlobalStatistics.Get<int>("Realm", "Dirty Refreshes");
|
||||||
|
private static readonly GlobalStatistic<int> contexts_created = GlobalStatistics.Get<int>("Realm", "Contexts (Created)");
|
||||||
|
private static readonly GlobalStatistic<int> pending_writes = GlobalStatistics.Get<int>("Realm", "Pending writes");
|
||||||
|
private static readonly GlobalStatistic<int> active_usages = GlobalStatistics.Get<int>("Realm", "Active usages");
|
||||||
|
|
||||||
|
private readonly ManualResetEventSlim blockingResetEvent = new ManualResetEventSlim(true);
|
||||||
|
|
||||||
|
private Realm context;
|
||||||
|
|
||||||
|
public Realm Context
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (IsDisposed)
|
||||||
|
throw new InvalidOperationException($"Attempted to access {nameof(Context)} on a disposed context factory");
|
||||||
|
|
||||||
|
if (context == null)
|
||||||
|
{
|
||||||
|
context = createContext();
|
||||||
|
Logger.Log($"Opened realm \"{context.Config.DatabasePath}\" at version {context.Config.SchemaVersion}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// creating a context will ensure our schema is up-to-date and migrated.
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealmContextFactory(Storage storage)
|
||||||
|
{
|
||||||
|
this.storage = storage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealmUsage GetForRead()
|
||||||
|
{
|
||||||
|
reads.Value++;
|
||||||
|
return new RealmUsage(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RealmWriteUsage GetForWrite()
|
||||||
|
{
|
||||||
|
writes.Value++;
|
||||||
|
pending_writes.Value++;
|
||||||
|
|
||||||
|
Monitor.Enter(writeLock);
|
||||||
|
|
||||||
|
return new RealmWriteUsage(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
if (context?.Refresh() == true)
|
||||||
|
refreshes.Value++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Realm createContext()
|
||||||
|
{
|
||||||
|
blockingResetEvent.Wait();
|
||||||
|
|
||||||
|
contexts_created.Value++;
|
||||||
|
|
||||||
|
return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true))
|
||||||
|
{
|
||||||
|
SchemaVersion = schema_version,
|
||||||
|
MigrationCallback = onMigration,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onMigration(Migration migration, ulong lastSchemaVersion)
|
||||||
|
{
|
||||||
|
switch (lastSchemaVersion)
|
||||||
|
{
|
||||||
|
case 5:
|
||||||
|
// let's keep things simple. changing the type of the primary key is a bit involved.
|
||||||
|
migration.NewRealm.RemoveAll<RealmKeyBinding>();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
|
BlockAllOperations();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDisposable BlockAllOperations()
|
||||||
|
{
|
||||||
|
blockingResetEvent.Reset();
|
||||||
|
flushContexts();
|
||||||
|
|
||||||
|
return new InvokeOnDisposal<RealmContextFactory>(this, r => endBlockingSection());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void endBlockingSection()
|
||||||
|
{
|
||||||
|
blockingResetEvent.Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void flushContexts()
|
||||||
|
{
|
||||||
|
var previousContext = context;
|
||||||
|
context = null;
|
||||||
|
|
||||||
|
// wait for all threaded usages to finish
|
||||||
|
while (active_usages.Value > 0)
|
||||||
|
Thread.Sleep(50);
|
||||||
|
|
||||||
|
previousContext?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A usage of realm from an arbitrary thread.
|
||||||
|
/// </summary>
|
||||||
|
public class RealmUsage : IDisposable
|
||||||
|
{
|
||||||
|
public readonly Realm Realm;
|
||||||
|
|
||||||
|
protected readonly RealmContextFactory Factory;
|
||||||
|
|
||||||
|
internal RealmUsage(RealmContextFactory factory)
|
||||||
|
{
|
||||||
|
active_usages.Value++;
|
||||||
|
Factory = factory;
|
||||||
|
Realm = factory.createContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disposes this instance, calling the initially captured action.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void Dispose()
|
||||||
|
{
|
||||||
|
Realm?.Dispose();
|
||||||
|
active_usages.Value--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A transaction used for making changes to realm data.
|
||||||
|
/// </summary>
|
||||||
|
public class RealmWriteUsage : RealmUsage
|
||||||
|
{
|
||||||
|
private readonly Transaction transaction;
|
||||||
|
|
||||||
|
internal RealmWriteUsage(RealmContextFactory factory)
|
||||||
|
: base(factory)
|
||||||
|
{
|
||||||
|
transaction = Realm.BeginWrite();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Commit all changes made in this transaction.
|
||||||
|
/// </summary>
|
||||||
|
public void Commit() => transaction.Commit();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Revert all changes made in this transaction.
|
||||||
|
/// </summary>
|
||||||
|
public void Rollback() => transaction.Rollback();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disposes this instance, calling the initially captured action.
|
||||||
|
/// </summary>
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
// rollback if not explicitly committed.
|
||||||
|
transaction?.Dispose();
|
||||||
|
|
||||||
|
base.Dispose();
|
||||||
|
|
||||||
|
Monitor.Exit(Factory.writeLock);
|
||||||
|
pending_writes.Value--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
osu.Game/Database/RealmExtensions.cs
Normal file
51
osu.Game/Database/RealmExtensions.cs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// 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 AutoMapper;
|
||||||
|
using osu.Game.Input.Bindings;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
namespace osu.Game.Database
|
||||||
|
{
|
||||||
|
public static class RealmExtensions
|
||||||
|
{
|
||||||
|
private static readonly IMapper mapper = new MapperConfiguration(c =>
|
||||||
|
{
|
||||||
|
c.ShouldMapField = fi => false;
|
||||||
|
c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic;
|
||||||
|
|
||||||
|
c.CreateMap<RealmKeyBinding, RealmKeyBinding>();
|
||||||
|
}).CreateMapper();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a detached copy of the each item in the collection.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="items">A list of managed <see cref="RealmObject"/>s to detach.</param>
|
||||||
|
/// <typeparam name="T">The type of object.</typeparam>
|
||||||
|
/// <returns>A list containing non-managed copies of provided items.</returns>
|
||||||
|
public static List<T> Detach<T>(this IEnumerable<T> items) where T : RealmObject
|
||||||
|
{
|
||||||
|
var list = new List<T>();
|
||||||
|
|
||||||
|
foreach (var obj in items)
|
||||||
|
list.Add(obj.Detach());
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a detached copy of the item.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">The managed <see cref="RealmObject"/> to detach.</param>
|
||||||
|
/// <typeparam name="T">The type of object.</typeparam>
|
||||||
|
/// <returns>A non-managed copy of provided item. Will return the provided item if already detached.</returns>
|
||||||
|
public static T Detach<T>(this T item) where T : RealmObject
|
||||||
|
{
|
||||||
|
if (!item.IsManaged)
|
||||||
|
return item;
|
||||||
|
|
||||||
|
return mapper.Map<T>(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
|
||||||
namespace osu.Game.Graphics.Containers
|
namespace osu.Game.Graphics.Containers
|
||||||
{
|
{
|
||||||
@ -20,7 +21,8 @@ namespace osu.Game.Graphics.Containers
|
|||||||
|
|
||||||
protected virtual IEnumerable<Drawable> EffectTargets => new[] { Content };
|
protected virtual IEnumerable<Drawable> EffectTargets => new[] { Content };
|
||||||
|
|
||||||
public OsuHoverContainer()
|
public OsuHoverContainer(HoverSampleSet sampleSet = HoverSampleSet.Default)
|
||||||
|
: base(sampleSet)
|
||||||
{
|
{
|
||||||
Enabled.ValueChanged += e =>
|
Enabled.ValueChanged += e =>
|
||||||
{
|
{
|
||||||
|
@ -13,13 +13,13 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
[Description("button")]
|
[Description("button")]
|
||||||
Button,
|
Button,
|
||||||
|
|
||||||
[Description("softer")]
|
|
||||||
Soft,
|
|
||||||
|
|
||||||
[Description("toolbar")]
|
[Description("toolbar")]
|
||||||
Toolbar,
|
Toolbar,
|
||||||
|
|
||||||
[Description("songselect")]
|
[Description("tabselect")]
|
||||||
SongSelect
|
TabSelect,
|
||||||
|
|
||||||
|
[Description("scrolltotop")]
|
||||||
|
ScrollToTop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,8 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Audio.Sample;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
@ -57,6 +59,9 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
{
|
{
|
||||||
public override bool HandleNonPositionalInput => State == MenuState.Open;
|
public override bool HandleNonPositionalInput => State == MenuState.Open;
|
||||||
|
|
||||||
|
private Sample sampleOpen;
|
||||||
|
private Sample sampleClose;
|
||||||
|
|
||||||
// todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring
|
// todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring
|
||||||
public OsuDropdownMenu()
|
public OsuDropdownMenu()
|
||||||
{
|
{
|
||||||
@ -69,9 +74,30 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
ItemsContainer.Padding = new MarginPadding(5);
|
ItemsContainer.Padding = new MarginPadding(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(AudioManager audio)
|
||||||
|
{
|
||||||
|
sampleOpen = audio.Samples.Get(@"UI/dropdown-open");
|
||||||
|
sampleClose = audio.Samples.Get(@"UI/dropdown-close");
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: this shouldn't be required after https://github.com/ppy/osu-framework/issues/4519 is fixed.
|
||||||
|
private bool wasOpened;
|
||||||
|
|
||||||
// todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring
|
// todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring
|
||||||
protected override void AnimateOpen() => this.FadeIn(300, Easing.OutQuint);
|
protected override void AnimateOpen()
|
||||||
protected override void AnimateClose() => this.FadeOut(300, Easing.OutQuint);
|
{
|
||||||
|
wasOpened = true;
|
||||||
|
this.FadeIn(300, Easing.OutQuint);
|
||||||
|
sampleOpen?.Play();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void AnimateClose()
|
||||||
|
{
|
||||||
|
this.FadeOut(300, Easing.OutQuint);
|
||||||
|
if (wasOpened)
|
||||||
|
sampleClose?.Play();
|
||||||
|
}
|
||||||
|
|
||||||
// todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring
|
// todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring
|
||||||
protected override void UpdateSize(Vector2 newSize)
|
protected override void UpdateSize(Vector2 newSize)
|
||||||
@ -155,7 +181,7 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
nonAccentSelectedColour = Color4.Black.Opacity(0.5f);
|
nonAccentSelectedColour = Color4.Black.Opacity(0.5f);
|
||||||
updateColours();
|
updateColours();
|
||||||
|
|
||||||
AddInternal(new HoverClickSounds(HoverSampleSet.Soft));
|
AddInternal(new HoverSounds());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void UpdateForegroundColour()
|
protected override void UpdateForegroundColour()
|
||||||
@ -262,7 +288,7 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
AddInternal(new HoverClickSounds());
|
AddInternal(new HoverSounds());
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
|
@ -172,7 +172,7 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
Origin = Anchor.BottomLeft,
|
Origin = Anchor.BottomLeft,
|
||||||
Anchor = Anchor.BottomLeft,
|
Anchor = Anchor.BottomLeft,
|
||||||
},
|
},
|
||||||
new HoverClickSounds()
|
new HoverClickSounds(HoverSampleSet.TabSelect)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,8 @@
|
|||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Audio.Sample;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
@ -43,6 +45,8 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
private const float transition_length = 500;
|
private const float transition_length = 500;
|
||||||
|
private Sample sampleChecked;
|
||||||
|
private Sample sampleUnchecked;
|
||||||
|
|
||||||
public OsuTabControlCheckbox()
|
public OsuTabControlCheckbox()
|
||||||
{
|
{
|
||||||
@ -77,8 +81,7 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
Colour = Color4.White,
|
Colour = Color4.White,
|
||||||
Origin = Anchor.BottomLeft,
|
Origin = Anchor.BottomLeft,
|
||||||
Anchor = Anchor.BottomLeft,
|
Anchor = Anchor.BottomLeft,
|
||||||
},
|
}
|
||||||
new HoverClickSounds()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Current.ValueChanged += selected =>
|
Current.ValueChanged += selected =>
|
||||||
@ -91,10 +94,13 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours)
|
private void load(OsuColour colours, AudioManager audio)
|
||||||
{
|
{
|
||||||
if (accentColour == null)
|
if (accentColour == null)
|
||||||
AccentColour = colours.Blue;
|
AccentColour = colours.Blue;
|
||||||
|
|
||||||
|
sampleChecked = audio.Samples.Get(@"UI/check-on");
|
||||||
|
sampleUnchecked = audio.Samples.Get(@"UI/check-off");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnHover(HoverEvent e)
|
protected override bool OnHover(HoverEvent e)
|
||||||
@ -111,6 +117,16 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
base.OnHoverLost(e);
|
base.OnHoverLost(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnUserChange(bool value)
|
||||||
|
{
|
||||||
|
base.OnUserChange(value);
|
||||||
|
|
||||||
|
if (value)
|
||||||
|
sampleChecked?.Play();
|
||||||
|
else
|
||||||
|
sampleUnchecked?.Play();
|
||||||
|
}
|
||||||
|
|
||||||
private void updateFade()
|
private void updateFade()
|
||||||
{
|
{
|
||||||
box.FadeTo(Current.Value || IsHovered ? 1 : 0, transition_length, Easing.OutQuint);
|
box.FadeTo(Current.Value || IsHovered ? 1 : 0, transition_length, Easing.OutQuint);
|
||||||
|
@ -76,7 +76,7 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
Origin = Anchor.BottomLeft,
|
Origin = Anchor.BottomLeft,
|
||||||
Anchor = Anchor.BottomLeft,
|
Anchor = Anchor.BottomLeft,
|
||||||
},
|
},
|
||||||
new HoverClickSounds()
|
new HoverClickSounds(HoverSampleSet.TabSelect)
|
||||||
};
|
};
|
||||||
|
|
||||||
Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true);
|
Active.BindValueChanged(active => Text.Font = Text.Font.With(Typeface.Torus, weight: active.NewValue ? FontWeight.Bold : FontWeight.Medium), true);
|
||||||
|
@ -33,12 +33,18 @@ namespace osu.Game.IO
|
|||||||
private readonly StorageConfigManager storageConfig;
|
private readonly StorageConfigManager storageConfig;
|
||||||
private readonly Storage defaultStorage;
|
private readonly Storage defaultStorage;
|
||||||
|
|
||||||
public override string[] IgnoreDirectories => new[] { "cache" };
|
public override string[] IgnoreDirectories => new[]
|
||||||
|
{
|
||||||
|
"cache",
|
||||||
|
"client.realm.management"
|
||||||
|
};
|
||||||
|
|
||||||
public override string[] IgnoreFiles => new[]
|
public override string[] IgnoreFiles => new[]
|
||||||
{
|
{
|
||||||
"framework.ini",
|
"framework.ini",
|
||||||
"storage.ini"
|
"storage.ini",
|
||||||
|
"client.realm.note",
|
||||||
|
"client.realm.lock",
|
||||||
};
|
};
|
||||||
|
|
||||||
public OsuStorage(GameHost host, Storage defaultStorage)
|
public OsuStorage(GameHost host, Storage defaultStorage)
|
||||||
|
@ -8,7 +8,7 @@ using osu.Game.Database;
|
|||||||
namespace osu.Game.Input.Bindings
|
namespace osu.Game.Input.Bindings
|
||||||
{
|
{
|
||||||
[Table("KeyBinding")]
|
[Table("KeyBinding")]
|
||||||
public class DatabasedKeyBinding : KeyBinding, IHasPrimaryKey
|
public class DatabasedKeyBinding : IKeyBinding, IHasPrimaryKey
|
||||||
{
|
{
|
||||||
public int ID { get; set; }
|
public int ID { get; set; }
|
||||||
|
|
||||||
@ -17,17 +17,23 @@ namespace osu.Game.Input.Bindings
|
|||||||
public int? Variant { get; set; }
|
public int? Variant { get; set; }
|
||||||
|
|
||||||
[Column("Keys")]
|
[Column("Keys")]
|
||||||
public string KeysString
|
public string KeysString { get; set; }
|
||||||
{
|
|
||||||
get => KeyCombination.ToString();
|
|
||||||
private set => KeyCombination = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Column("Action")]
|
[Column("Action")]
|
||||||
public int IntAction
|
public int IntAction { get; set; }
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
public KeyCombination KeyCombination
|
||||||
{
|
{
|
||||||
get => (int)Action;
|
get => KeysString;
|
||||||
set => Action = value;
|
set => KeysString = value.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
[NotMapped]
|
||||||
|
public object Action
|
||||||
|
{
|
||||||
|
get => IntAction;
|
||||||
|
set => IntAction = (int)value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,12 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
|
using osu.Game.Database;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using System.Linq;
|
using Realms;
|
||||||
|
|
||||||
namespace osu.Game.Input.Bindings
|
namespace osu.Game.Input.Bindings
|
||||||
{
|
{
|
||||||
@ -21,7 +23,11 @@ namespace osu.Game.Input.Bindings
|
|||||||
|
|
||||||
private readonly int? variant;
|
private readonly int? variant;
|
||||||
|
|
||||||
private KeyBindingStore store;
|
private IDisposable realmSubscription;
|
||||||
|
private IQueryable<RealmKeyBinding> realmKeyBindings;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private RealmContextFactory realmFactory { get; set; }
|
||||||
|
|
||||||
public override IEnumerable<IKeyBinding> DefaultKeyBindings => ruleset.CreateInstance().GetDefaultKeyBindings(variant ?? 0);
|
public override IEnumerable<IKeyBinding> DefaultKeyBindings => ruleset.CreateInstance().GetDefaultKeyBindings(variant ?? 0);
|
||||||
|
|
||||||
@ -42,24 +48,34 @@ namespace osu.Game.Input.Bindings
|
|||||||
throw new InvalidOperationException($"{nameof(variant)} can not be null when a non-null {nameof(ruleset)} is provided.");
|
throw new InvalidOperationException($"{nameof(variant)} can not be null when a non-null {nameof(ruleset)} is provided.");
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load(KeyBindingStore keyBindings)
|
|
||||||
{
|
|
||||||
store = keyBindings;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
|
if (ruleset == null || ruleset.ID.HasValue)
|
||||||
|
{
|
||||||
|
var rulesetId = ruleset?.ID;
|
||||||
|
|
||||||
|
realmKeyBindings = realmFactory.Context.All<RealmKeyBinding>()
|
||||||
|
.Where(b => b.RulesetID == rulesetId && b.Variant == variant);
|
||||||
|
|
||||||
|
realmSubscription = realmKeyBindings
|
||||||
|
.SubscribeForNotifications((sender, changes, error) =>
|
||||||
|
{
|
||||||
|
// first subscription ignored as we are handling this in LoadComplete.
|
||||||
|
if (changes == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ReloadMappings();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
store.KeyBindingChanged += ReloadMappings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
if (store != null)
|
realmSubscription?.Dispose();
|
||||||
store.KeyBindingChanged -= ReloadMappings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void ReloadMappings()
|
protected override void ReloadMappings()
|
||||||
@ -67,17 +83,17 @@ namespace osu.Game.Input.Bindings
|
|||||||
var defaults = DefaultKeyBindings.ToList();
|
var defaults = DefaultKeyBindings.ToList();
|
||||||
|
|
||||||
if (ruleset != null && !ruleset.ID.HasValue)
|
if (ruleset != null && !ruleset.ID.HasValue)
|
||||||
// if the provided ruleset is not stored to the database, we have no way to retrieve custom bindings.
|
// some tests instantiate a ruleset which is not present in the database.
|
||||||
// fallback to defaults instead.
|
// in these cases we still want key bindings to work, but matching to database instances would result in none being present,
|
||||||
|
// so let's populate the defaults directly.
|
||||||
KeyBindings = defaults;
|
KeyBindings = defaults;
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
KeyBindings = store.Query(ruleset?.ID, variant)
|
KeyBindings = realmKeyBindings.Detach()
|
||||||
.OrderBy(b => defaults.FindIndex(d => (int)d.Action == b.IntAction))
|
|
||||||
// this ordering is important to ensure that we read entries from the database in the order
|
// this ordering is important to ensure that we read entries from the database in the order
|
||||||
// enforced by DefaultKeyBindings. allow for song select to handle actions that may otherwise
|
// enforced by DefaultKeyBindings. allow for song select to handle actions that may otherwise
|
||||||
// have been eaten by the music controller due to query order.
|
// have been eaten by the music controller due to query order.
|
||||||
.ToList();
|
.OrderBy(b => defaults.FindIndex(d => (int)d.Action == b.ActionInt)).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
39
osu.Game/Input/Bindings/RealmKeyBinding.cs
Normal file
39
osu.Game/Input/Bindings/RealmKeyBinding.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using osu.Framework.Input.Bindings;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
|
namespace osu.Game.Input.Bindings
|
||||||
|
{
|
||||||
|
[MapTo(nameof(KeyBinding))]
|
||||||
|
public class RealmKeyBinding : RealmObject, IHasGuidPrimaryKey, IKeyBinding
|
||||||
|
{
|
||||||
|
[PrimaryKey]
|
||||||
|
public Guid ID { get; set; } = Guid.NewGuid();
|
||||||
|
|
||||||
|
public int? RulesetID { get; set; }
|
||||||
|
|
||||||
|
public int? Variant { get; set; }
|
||||||
|
|
||||||
|
public KeyCombination KeyCombination
|
||||||
|
{
|
||||||
|
get => KeyCombinationString;
|
||||||
|
set => KeyCombinationString = value.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Action
|
||||||
|
{
|
||||||
|
get => ActionInt;
|
||||||
|
set => ActionInt = (int)value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MapTo(nameof(Action))]
|
||||||
|
public int ActionInt { get; set; }
|
||||||
|
|
||||||
|
[MapTo(nameof(KeyCombination))]
|
||||||
|
public string KeyCombinationString { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -1,133 +0,0 @@
|
|||||||
// 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.Diagnostics;
|
|
||||||
using System.Linq;
|
|
||||||
using osu.Framework.Input.Bindings;
|
|
||||||
using osu.Framework.Platform;
|
|
||||||
using osu.Game.Database;
|
|
||||||
using osu.Game.Input.Bindings;
|
|
||||||
using osu.Game.Rulesets;
|
|
||||||
|
|
||||||
namespace osu.Game.Input
|
|
||||||
{
|
|
||||||
public class KeyBindingStore : DatabaseBackedStore
|
|
||||||
{
|
|
||||||
public event Action KeyBindingChanged;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Keys which should not be allowed for gameplay input purposes.
|
|
||||||
/// </summary>
|
|
||||||
private static readonly IEnumerable<InputKey> banned_keys = new[]
|
|
||||||
{
|
|
||||||
InputKey.MouseWheelDown,
|
|
||||||
InputKey.MouseWheelLeft,
|
|
||||||
InputKey.MouseWheelUp,
|
|
||||||
InputKey.MouseWheelRight
|
|
||||||
};
|
|
||||||
|
|
||||||
public KeyBindingStore(DatabaseContextFactory contextFactory, RulesetStore rulesets, Storage storage = null)
|
|
||||||
: base(contextFactory, storage)
|
|
||||||
{
|
|
||||||
using (ContextFactory.GetForWrite())
|
|
||||||
{
|
|
||||||
foreach (var info in rulesets.AvailableRulesets)
|
|
||||||
{
|
|
||||||
var ruleset = info.CreateInstance();
|
|
||||||
foreach (var variant in ruleset.AvailableVariants)
|
|
||||||
insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Register(KeyBindingContainer manager) => insertDefaults(manager.DefaultKeyBindings);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieve all user-defined key combinations (in a format that can be displayed) for a specific action.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="globalAction">The action to lookup.</param>
|
|
||||||
/// <returns>A set of display strings for all the user's key configuration for the action.</returns>
|
|
||||||
public IEnumerable<string> GetReadableKeyCombinationsFor(GlobalAction globalAction)
|
|
||||||
{
|
|
||||||
foreach (var action in Query().Where(b => (GlobalAction)b.Action == globalAction))
|
|
||||||
{
|
|
||||||
string str = action.KeyCombination.ReadableString();
|
|
||||||
|
|
||||||
// even if found, the readable string may be empty for an unbound action.
|
|
||||||
if (str.Length > 0)
|
|
||||||
yield return str;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void insertDefaults(IEnumerable<IKeyBinding> defaults, int? rulesetId = null, int? variant = null)
|
|
||||||
{
|
|
||||||
using (var usage = ContextFactory.GetForWrite())
|
|
||||||
{
|
|
||||||
// compare counts in database vs defaults
|
|
||||||
foreach (var group in defaults.GroupBy(k => k.Action))
|
|
||||||
{
|
|
||||||
int count = Query(rulesetId, variant).Count(k => (int)k.Action == (int)group.Key);
|
|
||||||
int aimCount = group.Count();
|
|
||||||
|
|
||||||
if (aimCount <= count)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
foreach (var insertable in group.Skip(count).Take(aimCount - count))
|
|
||||||
{
|
|
||||||
// insert any defaults which are missing.
|
|
||||||
usage.Context.DatabasedKeyBinding.Add(new DatabasedKeyBinding
|
|
||||||
{
|
|
||||||
KeyCombination = insertable.KeyCombination,
|
|
||||||
Action = insertable.Action,
|
|
||||||
RulesetID = rulesetId,
|
|
||||||
Variant = variant
|
|
||||||
});
|
|
||||||
|
|
||||||
// required to ensure stable insert order (https://github.com/dotnet/efcore/issues/11686)
|
|
||||||
usage.Context.SaveChanges();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieve <see cref="DatabasedKeyBinding"/>s for a specified ruleset/variant content.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="rulesetId">The ruleset's internal ID.</param>
|
|
||||||
/// <param name="variant">An optional variant.</param>
|
|
||||||
public List<DatabasedKeyBinding> Query(int? rulesetId = null, int? variant = null) =>
|
|
||||||
ContextFactory.Get().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList();
|
|
||||||
|
|
||||||
public void Update(KeyBinding keyBinding)
|
|
||||||
{
|
|
||||||
using (ContextFactory.GetForWrite())
|
|
||||||
{
|
|
||||||
var dbKeyBinding = (DatabasedKeyBinding)keyBinding;
|
|
||||||
|
|
||||||
Debug.Assert(dbKeyBinding.RulesetID == null || CheckValidForGameplay(keyBinding.KeyCombination));
|
|
||||||
|
|
||||||
Refresh(ref dbKeyBinding);
|
|
||||||
|
|
||||||
if (dbKeyBinding.KeyCombination.Equals(keyBinding.KeyCombination))
|
|
||||||
return;
|
|
||||||
|
|
||||||
dbKeyBinding.KeyCombination = keyBinding.KeyCombination;
|
|
||||||
}
|
|
||||||
|
|
||||||
KeyBindingChanged?.Invoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool CheckValidForGameplay(KeyCombination combination)
|
|
||||||
{
|
|
||||||
foreach (var key in banned_keys)
|
|
||||||
{
|
|
||||||
if (combination.Keys.Contains(key))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
117
osu.Game/Input/RealmKeyBindingStore.cs
Normal file
117
osu.Game/Input/RealmKeyBindingStore.cs
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
// 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 osu.Framework.Input.Bindings;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Input.Bindings;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Input
|
||||||
|
{
|
||||||
|
public class RealmKeyBindingStore
|
||||||
|
{
|
||||||
|
private readonly RealmContextFactory realmFactory;
|
||||||
|
|
||||||
|
public RealmKeyBindingStore(RealmContextFactory realmFactory)
|
||||||
|
{
|
||||||
|
this.realmFactory = realmFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieve all user-defined key combinations (in a format that can be displayed) for a specific action.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="globalAction">The action to lookup.</param>
|
||||||
|
/// <returns>A set of display strings for all the user's key configuration for the action.</returns>
|
||||||
|
public IReadOnlyList<string> GetReadableKeyCombinationsFor(GlobalAction globalAction)
|
||||||
|
{
|
||||||
|
List<string> combinations = new List<string>();
|
||||||
|
|
||||||
|
using (var context = realmFactory.GetForRead())
|
||||||
|
{
|
||||||
|
foreach (var action in context.Realm.All<RealmKeyBinding>().Where(b => b.RulesetID == null && (GlobalAction)b.ActionInt == globalAction))
|
||||||
|
{
|
||||||
|
string str = action.KeyCombination.ReadableString();
|
||||||
|
|
||||||
|
// even if found, the readable string may be empty for an unbound action.
|
||||||
|
if (str.Length > 0)
|
||||||
|
combinations.Add(str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return combinations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register a new type of <see cref="KeyBindingContainer{T}"/>, adding default bindings from <see cref="KeyBindingContainer.DefaultKeyBindings"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="container">The container to populate defaults from.</param>
|
||||||
|
public void Register(KeyBindingContainer container) => insertDefaults(container.DefaultKeyBindings);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register a ruleset, adding default bindings for each of its variants.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ruleset">The ruleset to populate defaults from.</param>
|
||||||
|
public void Register(RulesetInfo ruleset)
|
||||||
|
{
|
||||||
|
var instance = ruleset.CreateInstance();
|
||||||
|
|
||||||
|
foreach (var variant in instance.AvailableVariants)
|
||||||
|
insertDefaults(instance.GetDefaultKeyBindings(variant), ruleset.ID, variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void insertDefaults(IEnumerable<IKeyBinding> defaults, int? rulesetId = null, int? variant = null)
|
||||||
|
{
|
||||||
|
using (var usage = realmFactory.GetForWrite())
|
||||||
|
{
|
||||||
|
// compare counts in database vs defaults
|
||||||
|
foreach (var defaultsForAction in defaults.GroupBy(k => k.Action))
|
||||||
|
{
|
||||||
|
int existingCount = usage.Realm.All<RealmKeyBinding>().Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.ActionInt == (int)defaultsForAction.Key);
|
||||||
|
|
||||||
|
if (defaultsForAction.Count() <= existingCount)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach (var k in defaultsForAction.Skip(existingCount))
|
||||||
|
{
|
||||||
|
// insert any defaults which are missing.
|
||||||
|
usage.Realm.Add(new RealmKeyBinding
|
||||||
|
{
|
||||||
|
KeyCombinationString = k.KeyCombination.ToString(),
|
||||||
|
ActionInt = (int)k.Action,
|
||||||
|
RulesetID = rulesetId,
|
||||||
|
Variant = variant
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
usage.Commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Keys which should not be allowed for gameplay input purposes.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly IEnumerable<InputKey> banned_keys = new[]
|
||||||
|
{
|
||||||
|
InputKey.MouseWheelDown,
|
||||||
|
InputKey.MouseWheelLeft,
|
||||||
|
InputKey.MouseWheelUp,
|
||||||
|
InputKey.MouseWheelRight
|
||||||
|
};
|
||||||
|
|
||||||
|
public static bool CheckValidForGameplay(KeyCombination combination)
|
||||||
|
{
|
||||||
|
foreach (var key in banned_keys)
|
||||||
|
{
|
||||||
|
if (combination.Keys.Contains(key))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -14,9 +14,8 @@ namespace osu.Game.Localisation
|
|||||||
// [Description(@"اَلْعَرَبِيَّةُ")]
|
// [Description(@"اَلْعَرَبِيَّةُ")]
|
||||||
// ar,
|
// ar,
|
||||||
|
|
||||||
// TODO: Some accented glyphs are missing. Revisit when adding Inter.
|
[Description(@"Беларуская мова")]
|
||||||
// [Description(@"Беларуская мова")]
|
be,
|
||||||
// be,
|
|
||||||
|
|
||||||
[Description(@"Български")]
|
[Description(@"Български")]
|
||||||
bg,
|
bg,
|
||||||
@ -30,9 +29,8 @@ namespace osu.Game.Localisation
|
|||||||
[Description(@"Deutsch")]
|
[Description(@"Deutsch")]
|
||||||
de,
|
de,
|
||||||
|
|
||||||
// TODO: Some accented glyphs are missing. Revisit when adding Inter.
|
[Description(@"Ελληνικά")]
|
||||||
// [Description(@"Ελληνικά")]
|
el,
|
||||||
// el,
|
|
||||||
|
|
||||||
[Description(@"español")]
|
[Description(@"español")]
|
||||||
es,
|
es,
|
||||||
@ -88,15 +86,16 @@ namespace osu.Game.Localisation
|
|||||||
[Description(@"ไทย")]
|
[Description(@"ไทย")]
|
||||||
th,
|
th,
|
||||||
|
|
||||||
[Description(@"Tagalog")]
|
// Tagalog has no associated localisations yet, and is not supported on Xamarin platforms or Windows versions <10.
|
||||||
tl,
|
// Can be revisited if localisations ever arrive.
|
||||||
|
//[Description(@"Tagalog")]
|
||||||
|
//tl,
|
||||||
|
|
||||||
[Description(@"Türkçe")]
|
[Description(@"Türkçe")]
|
||||||
tr,
|
tr,
|
||||||
|
|
||||||
// TODO: Some accented glyphs are missing. Revisit when adding Inter.
|
[Description(@"Українська мова")]
|
||||||
// [Description(@"Українська мова")]
|
uk,
|
||||||
// uk,
|
|
||||||
|
|
||||||
[Description(@"Tiếng Việt")]
|
[Description(@"Tiếng Việt")]
|
||||||
vi,
|
vi,
|
||||||
|
@ -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());
|
||||||
|
@ -585,8 +585,16 @@ namespace osu.Game
|
|||||||
foreach (var language in Enum.GetValues(typeof(Language)).OfType<Language>())
|
foreach (var language in Enum.GetValues(typeof(Language)).OfType<Language>())
|
||||||
{
|
{
|
||||||
var cultureCode = language.ToCultureCode();
|
var cultureCode = language.ToCultureCode();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
Localisation.AddLanguage(cultureCode, new ResourceManagerLocalisationStore(cultureCode));
|
Localisation.AddLanguage(cultureCode, new ResourceManagerLocalisationStore(cultureCode));
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Error(ex, $"Could not load localisations for language \"{cultureCode}\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The next time this is updated is in UpdateAfterChildren, which occurs too late and results
|
// The next time this is updated is in UpdateAfterChildren, which occurs too late and results
|
||||||
// in the cursor being shown for a few frames during the intro.
|
// in the cursor being shown for a few frames during the intro.
|
||||||
@ -608,9 +616,9 @@ namespace osu.Game
|
|||||||
|
|
||||||
LocalConfig.LookupKeyBindings = l =>
|
LocalConfig.LookupKeyBindings = l =>
|
||||||
{
|
{
|
||||||
var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l).ToArray();
|
var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l);
|
||||||
|
|
||||||
if (combinations.Length == 0)
|
if (combinations.Count == 0)
|
||||||
return "none";
|
return "none";
|
||||||
|
|
||||||
return string.Join(" or ", combinations);
|
return string.Join(" or ", combinations);
|
||||||
|
@ -95,7 +95,7 @@ namespace osu.Game
|
|||||||
|
|
||||||
protected RulesetStore RulesetStore { get; private set; }
|
protected RulesetStore RulesetStore { get; private set; }
|
||||||
|
|
||||||
protected KeyBindingStore KeyBindingStore { get; private set; }
|
protected RealmKeyBindingStore KeyBindingStore { get; private set; }
|
||||||
|
|
||||||
protected MenuCursorContainer MenuCursorContainer { get; private set; }
|
protected MenuCursorContainer MenuCursorContainer { get; private set; }
|
||||||
|
|
||||||
@ -144,6 +144,8 @@ namespace osu.Game
|
|||||||
|
|
||||||
private DatabaseContextFactory contextFactory;
|
private DatabaseContextFactory contextFactory;
|
||||||
|
|
||||||
|
private RealmContextFactory realmFactory;
|
||||||
|
|
||||||
protected override Container<Drawable> Content => content;
|
protected override Container<Drawable> Content => content;
|
||||||
|
|
||||||
private Container content;
|
private Container content;
|
||||||
@ -179,6 +181,9 @@ namespace osu.Game
|
|||||||
|
|
||||||
dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage));
|
dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage));
|
||||||
|
|
||||||
|
dependencies.Cache(realmFactory = new RealmContextFactory(Storage));
|
||||||
|
AddInternal(realmFactory);
|
||||||
|
|
||||||
dependencies.CacheAs(Storage);
|
dependencies.CacheAs(Storage);
|
||||||
|
|
||||||
var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(Resources, @"Textures")));
|
var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(Resources, @"Textures")));
|
||||||
@ -190,20 +195,29 @@ namespace osu.Game
|
|||||||
|
|
||||||
AddFont(Resources, @"Fonts/osuFont");
|
AddFont(Resources, @"Fonts/osuFont");
|
||||||
|
|
||||||
AddFont(Resources, @"Fonts/Torus-Regular");
|
AddFont(Resources, @"Fonts/Torus/Torus-Regular");
|
||||||
AddFont(Resources, @"Fonts/Torus-Light");
|
AddFont(Resources, @"Fonts/Torus/Torus-Light");
|
||||||
AddFont(Resources, @"Fonts/Torus-SemiBold");
|
AddFont(Resources, @"Fonts/Torus/Torus-SemiBold");
|
||||||
AddFont(Resources, @"Fonts/Torus-Bold");
|
AddFont(Resources, @"Fonts/Torus/Torus-Bold");
|
||||||
|
|
||||||
AddFont(Resources, @"Fonts/Noto-Basic");
|
AddFont(Resources, @"Fonts/Inter/Inter-Regular");
|
||||||
AddFont(Resources, @"Fonts/Noto-Hangul");
|
AddFont(Resources, @"Fonts/Inter/Inter-RegularItalic");
|
||||||
AddFont(Resources, @"Fonts/Noto-CJK-Basic");
|
AddFont(Resources, @"Fonts/Inter/Inter-Light");
|
||||||
AddFont(Resources, @"Fonts/Noto-CJK-Compatibility");
|
AddFont(Resources, @"Fonts/Inter/Inter-LightItalic");
|
||||||
AddFont(Resources, @"Fonts/Noto-Thai");
|
AddFont(Resources, @"Fonts/Inter/Inter-SemiBold");
|
||||||
|
AddFont(Resources, @"Fonts/Inter/Inter-SemiBoldItalic");
|
||||||
|
AddFont(Resources, @"Fonts/Inter/Inter-Bold");
|
||||||
|
AddFont(Resources, @"Fonts/Inter/Inter-BoldItalic");
|
||||||
|
|
||||||
AddFont(Resources, @"Fonts/Venera-Light");
|
AddFont(Resources, @"Fonts/Noto/Noto-Basic");
|
||||||
AddFont(Resources, @"Fonts/Venera-Bold");
|
AddFont(Resources, @"Fonts/Noto/Noto-Hangul");
|
||||||
AddFont(Resources, @"Fonts/Venera-Black");
|
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Basic");
|
||||||
|
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Compatibility");
|
||||||
|
AddFont(Resources, @"Fonts/Noto/Noto-Thai");
|
||||||
|
|
||||||
|
AddFont(Resources, @"Fonts/Venera/Venera-Light");
|
||||||
|
AddFont(Resources, @"Fonts/Venera/Venera-Bold");
|
||||||
|
AddFont(Resources, @"Fonts/Venera/Venera-Black");
|
||||||
|
|
||||||
Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY;
|
Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY;
|
||||||
|
|
||||||
@ -275,7 +289,8 @@ namespace osu.Game
|
|||||||
dependencies.Cache(scorePerformanceManager);
|
dependencies.Cache(scorePerformanceManager);
|
||||||
AddInternal(scorePerformanceManager);
|
AddInternal(scorePerformanceManager);
|
||||||
|
|
||||||
dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore));
|
migrateDataToRealm();
|
||||||
|
|
||||||
dependencies.Cache(settingsStore = new SettingsStore(contextFactory));
|
dependencies.Cache(settingsStore = new SettingsStore(contextFactory));
|
||||||
dependencies.Cache(rulesetConfigCache = new RulesetConfigCache(settingsStore));
|
dependencies.Cache(rulesetConfigCache = new RulesetConfigCache(settingsStore));
|
||||||
|
|
||||||
@ -323,7 +338,12 @@ namespace osu.Game
|
|||||||
|
|
||||||
base.Content.Add(CreateScalingContainer().WithChildren(mainContent));
|
base.Content.Add(CreateScalingContainer().WithChildren(mainContent));
|
||||||
|
|
||||||
|
KeyBindingStore = new RealmKeyBindingStore(realmFactory);
|
||||||
KeyBindingStore.Register(globalBindings);
|
KeyBindingStore.Register(globalBindings);
|
||||||
|
|
||||||
|
foreach (var r in RulesetStore.AvailableRulesets)
|
||||||
|
KeyBindingStore.Register(r);
|
||||||
|
|
||||||
dependencies.Cache(globalBindings);
|
dependencies.Cache(globalBindings);
|
||||||
|
|
||||||
PreviewTrackManager previewTrackManager;
|
PreviewTrackManager previewTrackManager;
|
||||||
@ -377,10 +397,13 @@ namespace osu.Game
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void Migrate(string path)
|
public void Migrate(string path)
|
||||||
|
{
|
||||||
|
using (realmFactory.BlockAllOperations())
|
||||||
{
|
{
|
||||||
contextFactory.FlushConnections();
|
contextFactory.FlushConnections();
|
||||||
(Storage as OsuStorage)?.Migrate(Host.GetStorage(path));
|
(Storage as OsuStorage)?.Migrate(Host.GetStorage(path));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager();
|
protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager();
|
||||||
|
|
||||||
@ -390,6 +413,34 @@ namespace osu.Game
|
|||||||
|
|
||||||
protected override Storage CreateStorage(GameHost host, Storage defaultStorage) => new OsuStorage(host, defaultStorage);
|
protected override Storage CreateStorage(GameHost host, Storage defaultStorage) => new OsuStorage(host, defaultStorage);
|
||||||
|
|
||||||
|
private void migrateDataToRealm()
|
||||||
|
{
|
||||||
|
using (var db = contextFactory.GetForWrite())
|
||||||
|
using (var usage = realmFactory.GetForWrite())
|
||||||
|
{
|
||||||
|
var existingBindings = db.Context.DatabasedKeyBinding;
|
||||||
|
|
||||||
|
// only migrate data if the realm database is empty.
|
||||||
|
if (!usage.Realm.All<RealmKeyBinding>().Any())
|
||||||
|
{
|
||||||
|
foreach (var dkb in existingBindings)
|
||||||
|
{
|
||||||
|
usage.Realm.Add(new RealmKeyBinding
|
||||||
|
{
|
||||||
|
KeyCombinationString = dkb.KeyCombination.ToString(),
|
||||||
|
ActionInt = (int)dkb.Action,
|
||||||
|
RulesetID = dkb.RulesetID,
|
||||||
|
Variant = dkb.Variant
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Context.RemoveRange(existingBindings);
|
||||||
|
|
||||||
|
usage.Commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void onRulesetChanged(ValueChangedEvent<RulesetInfo> r)
|
private void onRulesetChanged(ValueChangedEvent<RulesetInfo> r)
|
||||||
{
|
{
|
||||||
var dict = new Dictionary<ModType, IReadOnlyList<Mod>>();
|
var dict = new Dictionary<ModType, IReadOnlyList<Mod>>();
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
// 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 osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Audio.Sample;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -66,6 +68,8 @@ namespace osu.Game.Overlays.Comments
|
|||||||
public readonly BindableBool Checked = new BindableBool();
|
public readonly BindableBool Checked = new BindableBool();
|
||||||
|
|
||||||
private readonly SpriteIcon checkboxIcon;
|
private readonly SpriteIcon checkboxIcon;
|
||||||
|
private Sample sampleChecked;
|
||||||
|
private Sample sampleUnchecked;
|
||||||
|
|
||||||
public ShowDeletedButton()
|
public ShowDeletedButton()
|
||||||
{
|
{
|
||||||
@ -93,6 +97,13 @@ namespace osu.Game.Overlays.Comments
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(AudioManager audio)
|
||||||
|
{
|
||||||
|
sampleChecked = audio.Samples.Get(@"UI/check-on");
|
||||||
|
sampleUnchecked = audio.Samples.Get(@"UI/check-off");
|
||||||
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
Checked.BindValueChanged(isChecked => checkboxIcon.Icon = isChecked.NewValue ? FontAwesome.Solid.CheckSquare : FontAwesome.Regular.Square, true);
|
Checked.BindValueChanged(isChecked => checkboxIcon.Icon = isChecked.NewValue ? FontAwesome.Solid.CheckSquare : FontAwesome.Regular.Square, true);
|
||||||
@ -102,6 +113,12 @@ namespace osu.Game.Overlays.Comments
|
|||||||
protected override bool OnClick(ClickEvent e)
|
protected override bool OnClick(ClickEvent e)
|
||||||
{
|
{
|
||||||
Checked.Value = !Checked.Value;
|
Checked.Value = !Checked.Value;
|
||||||
|
|
||||||
|
if (Checked.Value)
|
||||||
|
sampleChecked?.Play();
|
||||||
|
else
|
||||||
|
sampleUnchecked?.Play();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Graphics.UserInterface;
|
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Comments
|
namespace osu.Game.Overlays.Comments
|
||||||
{
|
{
|
||||||
@ -39,7 +38,6 @@ namespace osu.Game.Overlays.Comments
|
|||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Margin = new MarginPadding { Horizontal = 10 }
|
Margin = new MarginPadding { Horizontal = 10 }
|
||||||
},
|
},
|
||||||
new HoverClickSounds(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// 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 System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -13,6 +14,7 @@ using osu.Framework.Graphics.Effects;
|
|||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Database;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
@ -27,7 +29,7 @@ namespace osu.Game.Overlays.KeyBinding
|
|||||||
public class KeyBindingRow : Container, IFilterable
|
public class KeyBindingRow : Container, IFilterable
|
||||||
{
|
{
|
||||||
private readonly object action;
|
private readonly object action;
|
||||||
private readonly IEnumerable<Framework.Input.Bindings.KeyBinding> bindings;
|
private readonly IEnumerable<RealmKeyBinding> bindings;
|
||||||
|
|
||||||
private const float transition_time = 150;
|
private const float transition_time = 150;
|
||||||
|
|
||||||
@ -62,7 +64,7 @@ namespace osu.Game.Overlays.KeyBinding
|
|||||||
|
|
||||||
public IEnumerable<string> FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend(text.Text.ToString());
|
public IEnumerable<string> FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend(text.Text.ToString());
|
||||||
|
|
||||||
public KeyBindingRow(object action, IEnumerable<Framework.Input.Bindings.KeyBinding> bindings)
|
public KeyBindingRow(object action, List<RealmKeyBinding> bindings)
|
||||||
{
|
{
|
||||||
this.action = action;
|
this.action = action;
|
||||||
this.bindings = bindings;
|
this.bindings = bindings;
|
||||||
@ -72,7 +74,7 @@ namespace osu.Game.Overlays.KeyBinding
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private KeyBindingStore store { get; set; }
|
private RealmContextFactory realmFactory { get; set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours)
|
private void load(OsuColour colours)
|
||||||
@ -153,7 +155,8 @@ namespace osu.Game.Overlays.KeyBinding
|
|||||||
{
|
{
|
||||||
var button = buttons[i++];
|
var button = buttons[i++];
|
||||||
button.UpdateKeyCombination(d);
|
button.UpdateKeyCombination(d);
|
||||||
store.Update(button.KeyBinding);
|
|
||||||
|
updateStoreFromButton(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
isDefault.Value = true;
|
isDefault.Value = true;
|
||||||
@ -314,7 +317,7 @@ namespace osu.Game.Overlays.KeyBinding
|
|||||||
{
|
{
|
||||||
if (bindTarget != null)
|
if (bindTarget != null)
|
||||||
{
|
{
|
||||||
store.Update(bindTarget.KeyBinding);
|
updateStoreFromButton(bindTarget);
|
||||||
|
|
||||||
updateIsDefaultValue();
|
updateIsDefaultValue();
|
||||||
|
|
||||||
@ -361,6 +364,17 @@ namespace osu.Game.Overlays.KeyBinding
|
|||||||
if (bindTarget != null) bindTarget.IsBinding = true;
|
if (bindTarget != null) bindTarget.IsBinding = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateStoreFromButton(KeyButton button)
|
||||||
|
{
|
||||||
|
using (var usage = realmFactory.GetForWrite())
|
||||||
|
{
|
||||||
|
var binding = usage.Realm.Find<RealmKeyBinding>(((IHasGuidPrimaryKey)button.KeyBinding).ID);
|
||||||
|
binding.KeyCombinationString = button.KeyBinding.KeyCombinationString;
|
||||||
|
|
||||||
|
usage.Commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void updateIsDefaultValue()
|
private void updateIsDefaultValue()
|
||||||
{
|
{
|
||||||
isDefault.Value = bindings.Select(b => b.KeyCombination).SequenceEqual(Defaults);
|
isDefault.Value = bindings.Select(b => b.KeyCombination).SequenceEqual(Defaults);
|
||||||
@ -386,7 +400,7 @@ namespace osu.Game.Overlays.KeyBinding
|
|||||||
|
|
||||||
public class KeyButton : Container
|
public class KeyButton : Container
|
||||||
{
|
{
|
||||||
public readonly Framework.Input.Bindings.KeyBinding KeyBinding;
|
public readonly RealmKeyBinding KeyBinding;
|
||||||
|
|
||||||
private readonly Box box;
|
private readonly Box box;
|
||||||
public readonly OsuSpriteText Text;
|
public readonly OsuSpriteText Text;
|
||||||
@ -408,8 +422,11 @@ namespace osu.Game.Overlays.KeyBinding
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public KeyButton(Framework.Input.Bindings.KeyBinding keyBinding)
|
public KeyButton(RealmKeyBinding keyBinding)
|
||||||
{
|
{
|
||||||
|
if (keyBinding.IsManaged)
|
||||||
|
throw new ArgumentException("Key binding should not be attached as we make temporary changes", nameof(keyBinding));
|
||||||
|
|
||||||
KeyBinding = keyBinding;
|
KeyBinding = keyBinding;
|
||||||
|
|
||||||
Margin = new MarginPadding(padding);
|
Margin = new MarginPadding(padding);
|
||||||
@ -478,7 +495,7 @@ namespace osu.Game.Overlays.KeyBinding
|
|||||||
|
|
||||||
public void UpdateKeyCombination(KeyCombination newCombination)
|
public void UpdateKeyCombination(KeyCombination newCombination)
|
||||||
{
|
{
|
||||||
if ((KeyBinding as DatabasedKeyBinding)?.RulesetID != null && !KeyBindingStore.CheckValidForGameplay(newCombination))
|
if (KeyBinding.RulesetID != null && !RealmKeyBindingStore.CheckValidForGameplay(newCombination))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
KeyBinding.KeyCombination = newCombination;
|
KeyBinding.KeyCombination = newCombination;
|
||||||
|
@ -6,8 +6,9 @@ using System.Linq;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Database;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Input;
|
using osu.Game.Input.Bindings;
|
||||||
using osu.Game.Overlays.Settings;
|
using osu.Game.Overlays.Settings;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -31,16 +32,21 @@ namespace osu.Game.Overlays.KeyBinding
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(KeyBindingStore store)
|
private void load(RealmContextFactory realmFactory)
|
||||||
{
|
{
|
||||||
var bindings = store.Query(Ruleset?.ID, variant);
|
var rulesetId = Ruleset?.ID;
|
||||||
|
|
||||||
|
List<RealmKeyBinding> bindings;
|
||||||
|
|
||||||
|
using (var usage = realmFactory.GetForRead())
|
||||||
|
bindings = usage.Realm.All<RealmKeyBinding>().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach();
|
||||||
|
|
||||||
foreach (var defaultGroup in Defaults.GroupBy(d => d.Action))
|
foreach (var defaultGroup in Defaults.GroupBy(d => d.Action))
|
||||||
{
|
{
|
||||||
int intKey = (int)defaultGroup.Key;
|
int intKey = (int)defaultGroup.Key;
|
||||||
|
|
||||||
// one row per valid action.
|
// one row per valid action.
|
||||||
Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => ((int)b.Action).Equals(intKey)))
|
Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.ActionInt.Equals(intKey)).ToList())
|
||||||
{
|
{
|
||||||
AllowMainMouseButtons = Ruleset != null,
|
AllowMainMouseButtons = Ruleset != null,
|
||||||
Defaults = defaultGroup.Select(d => d.KeyCombination)
|
Defaults = defaultGroup.Select(d => d.KeyCombination)
|
||||||
|
@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Mods
|
|||||||
base.OnModSelected(mod);
|
base.OnModSelected(mod);
|
||||||
|
|
||||||
foreach (var section in ModSectionsContainer.Children)
|
foreach (var section in ModSectionsContainer.Children)
|
||||||
section.DeselectTypes(mod.IncompatibleMods, true);
|
section.DeselectTypes(mod.IncompatibleMods, true, mod);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -302,7 +302,7 @@ namespace osu.Game.Overlays.Mods
|
|||||||
Anchor = Anchor.TopCentre,
|
Anchor = Anchor.TopCentre,
|
||||||
Font = OsuFont.GetFont(size: 18)
|
Font = OsuFont.GetFont(size: 18)
|
||||||
},
|
},
|
||||||
new HoverClickSounds(buttons: new[] { MouseButton.Left, MouseButton.Right })
|
new HoverSounds()
|
||||||
};
|
};
|
||||||
|
|
||||||
Mod = mod;
|
Mod = mod;
|
||||||
|
@ -159,12 +159,16 @@ namespace osu.Game.Overlays.Mods
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="modTypes">The types of <see cref="Mod"/>s which should be deselected.</param>
|
/// <param name="modTypes">The types of <see cref="Mod"/>s which should be deselected.</param>
|
||||||
/// <param name="immediate">Whether the deselection should happen immediately. Should only be used when required to ensure correct selection flow.</param>
|
/// <param name="immediate">Whether the deselection should happen immediately. Should only be used when required to ensure correct selection flow.</param>
|
||||||
public void DeselectTypes(IEnumerable<Type> modTypes, bool immediate = false)
|
/// <param name="newSelection">If this deselection is triggered by a user selection, this should contain the newly selected type. This type will never be deselected, even if it matches one provided in <paramref name="modTypes"/>.</param>
|
||||||
|
public void DeselectTypes(IEnumerable<Type> modTypes, bool immediate = false, Mod newSelection = null)
|
||||||
{
|
{
|
||||||
foreach (var button in Buttons)
|
foreach (var button in Buttons)
|
||||||
{
|
{
|
||||||
if (button.SelectedMod == null) continue;
|
if (button.SelectedMod == null) continue;
|
||||||
|
|
||||||
|
if (button.SelectedMod == newSelection)
|
||||||
|
continue;
|
||||||
|
|
||||||
foreach (var type in modTypes)
|
foreach (var type in modTypes)
|
||||||
{
|
{
|
||||||
if (type.IsInstanceOfType(button.SelectedMod))
|
if (type.IsInstanceOfType(button.SelectedMod))
|
||||||
|
@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes;
|
|||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
@ -84,6 +85,7 @@ namespace osu.Game.Overlays
|
|||||||
private readonly Box background;
|
private readonly Box background;
|
||||||
|
|
||||||
public ScrollToTopButton()
|
public ScrollToTopButton()
|
||||||
|
: base(HoverSampleSet.ScrollToTop)
|
||||||
{
|
{
|
||||||
Size = new Vector2(50);
|
Size = new Vector2(50);
|
||||||
Alpha = 0;
|
Alpha = 0;
|
||||||
|
@ -148,6 +148,8 @@ namespace osu.Game.Overlays
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AddInternal(new HoverClickSounds());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
|
@ -99,7 +99,7 @@ namespace osu.Game.Overlays
|
|||||||
ExpandedSize = 5f,
|
ExpandedSize = 5f,
|
||||||
CollapsedSize = 0
|
CollapsedSize = 0
|
||||||
},
|
},
|
||||||
new HoverClickSounds()
|
new HoverClickSounds(HoverSampleSet.TabSelect)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
// 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.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Caching;
|
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Extensions.EnumExtensions;
|
using osu.Framework.Extensions.EnumExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -13,13 +13,13 @@ using osu.Framework.Graphics.Sprites;
|
|||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Game.Database;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Backgrounds;
|
using osu.Game.Graphics.Backgrounds;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Input;
|
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
@ -76,7 +76,7 @@ namespace osu.Game.Overlays.Toolbar
|
|||||||
protected FillFlowContainer Flow;
|
protected FillFlowContainer Flow;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private KeyBindingStore keyBindings { get; set; }
|
private RealmContextFactory realmFactory { get; set; }
|
||||||
|
|
||||||
protected ToolbarButton()
|
protected ToolbarButton()
|
||||||
: base(HoverSampleSet.Toolbar)
|
: base(HoverSampleSet.Toolbar)
|
||||||
@ -159,25 +159,26 @@ namespace osu.Game.Overlays.Toolbar
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly Cached tooltipKeyBinding = new Cached();
|
private RealmKeyBinding realmKeyBinding;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
protected override void LoadComplete()
|
||||||
private void load()
|
|
||||||
{
|
{
|
||||||
keyBindings.KeyBindingChanged += () => tooltipKeyBinding.Invalidate();
|
base.LoadComplete();
|
||||||
|
|
||||||
|
if (Hotkey == null) return;
|
||||||
|
|
||||||
|
realmKeyBinding = realmFactory.Context.All<RealmKeyBinding>().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.ActionInt == (int)Hotkey.Value);
|
||||||
|
|
||||||
|
if (realmKeyBinding != null)
|
||||||
|
{
|
||||||
|
realmKeyBinding.PropertyChanged += (sender, args) =>
|
||||||
|
{
|
||||||
|
if (args.PropertyName == nameof(realmKeyBinding.KeyCombinationString))
|
||||||
updateKeyBindingTooltip();
|
updateKeyBindingTooltip();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateKeyBindingTooltip()
|
updateKeyBindingTooltip();
|
||||||
{
|
|
||||||
if (tooltipKeyBinding.IsValid)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var binding = keyBindings.Query().Find(b => (GlobalAction)b.Action == Hotkey);
|
|
||||||
var keyBindingString = binding?.KeyCombination.ReadableString();
|
|
||||||
keyBindingTooltip.Text = !string.IsNullOrEmpty(keyBindingString) ? $" ({keyBindingString})" : string.Empty;
|
|
||||||
|
|
||||||
tooltipKeyBinding.Validate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnMouseDown(MouseDownEvent e) => true;
|
protected override bool OnMouseDown(MouseDownEvent e) => true;
|
||||||
@ -218,6 +219,17 @@ namespace osu.Game.Overlays.Toolbar
|
|||||||
public void OnReleased(GlobalAction action)
|
public void OnReleased(GlobalAction action)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateKeyBindingTooltip()
|
||||||
|
{
|
||||||
|
if (realmKeyBinding != null)
|
||||||
|
{
|
||||||
|
var keyBindingString = realmKeyBinding.KeyCombination.ReadableString();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(keyBindingString))
|
||||||
|
keyBindingTooltip.Text = $" ({keyBindingString})";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class OpaqueBackground : Container
|
public class OpaqueBackground : Container
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -156,9 +156,10 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
/// If <c>null</c>, a hitobject is expected to be later applied via <see cref="PoolableDrawableWithLifetime{TEntry}.Apply"/> (or automatically via pooling).
|
/// If <c>null</c>, a hitobject is expected to be later applied via <see cref="PoolableDrawableWithLifetime{TEntry}.Apply"/> (or automatically via pooling).
|
||||||
/// </param>
|
/// </param>
|
||||||
protected DrawableHitObject([CanBeNull] HitObject initialHitObject = null)
|
protected DrawableHitObject([CanBeNull] HitObject initialHitObject = null)
|
||||||
: base(initialHitObject != null ? new SyntheticHitObjectEntry(initialHitObject) : null)
|
|
||||||
{
|
{
|
||||||
if (Entry != null)
|
if (initialHitObject == null) return;
|
||||||
|
|
||||||
|
Entry = new SyntheticHitObjectEntry(initialHitObject);
|
||||||
ensureEntryHasResult();
|
ensureEntryHasResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Performance;
|
using osu.Framework.Graphics.Performance;
|
||||||
using osu.Framework.Graphics.Pooling;
|
using osu.Framework.Graphics.Pooling;
|
||||||
|
|
||||||
@ -16,14 +17,32 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
|||||||
/// <typeparam name="TEntry">The <see cref="LifetimeEntry"/> type storing state and controlling this drawable.</typeparam>
|
/// <typeparam name="TEntry">The <see cref="LifetimeEntry"/> type storing state and controlling this drawable.</typeparam>
|
||||||
public abstract class PoolableDrawableWithLifetime<TEntry> : PoolableDrawable where TEntry : LifetimeEntry
|
public abstract class PoolableDrawableWithLifetime<TEntry> : PoolableDrawable where TEntry : LifetimeEntry
|
||||||
{
|
{
|
||||||
|
private TEntry? entry;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The entry holding essential state of this <see cref="PoolableDrawableWithLifetime{TEntry}"/>.
|
/// The entry holding essential state of this <see cref="PoolableDrawableWithLifetime{TEntry}"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TEntry? Entry { get; private set; }
|
/// <remarks>
|
||||||
|
/// If a non-null value is set before loading is started, the entry is applied when the loading is completed.
|
||||||
|
/// It is not valid to set an entry while this <see cref="PoolableDrawableWithLifetime{TEntry}"/> is loading.
|
||||||
|
/// </remarks>
|
||||||
|
public TEntry? Entry
|
||||||
|
{
|
||||||
|
get => entry;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (LoadState == LoadState.NotLoaded)
|
||||||
|
entry = value;
|
||||||
|
else if (value != null)
|
||||||
|
Apply(value);
|
||||||
|
else if (HasEntryApplied)
|
||||||
|
free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether <see cref="Entry"/> is applied to this <see cref="PoolableDrawableWithLifetime{TEntry}"/>.
|
/// Whether <see cref="Entry"/> is applied to this <see cref="PoolableDrawableWithLifetime{TEntry}"/>.
|
||||||
/// When an initial entry is specified in the constructor, <see cref="Entry"/> is set but not applied until loading is completed.
|
/// When an <see cref="Entry"/> is set during initialization, it is not applied until loading is completed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected bool HasEntryApplied { get; private set; }
|
protected bool HasEntryApplied { get; private set; }
|
||||||
|
|
||||||
@ -65,9 +84,9 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
|||||||
{
|
{
|
||||||
base.LoadAsyncComplete();
|
base.LoadAsyncComplete();
|
||||||
|
|
||||||
// Apply the initial entry given in the constructor.
|
// Apply the initial entry.
|
||||||
if (Entry != null && !HasEntryApplied)
|
if (Entry != null && !HasEntryApplied)
|
||||||
Apply(Entry);
|
apply(Entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -76,16 +95,10 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void Apply(TEntry entry)
|
public void Apply(TEntry entry)
|
||||||
{
|
{
|
||||||
if (HasEntryApplied)
|
if (LoadState == LoadState.Loading)
|
||||||
free();
|
throw new InvalidOperationException($"Cannot apply a new {nameof(TEntry)} while currently loading.");
|
||||||
|
|
||||||
Entry = entry;
|
apply(entry);
|
||||||
entry.LifetimeChanged += setLifetimeFromEntry;
|
|
||||||
setLifetimeFromEntry(entry);
|
|
||||||
|
|
||||||
OnApply(entry);
|
|
||||||
|
|
||||||
HasEntryApplied = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected sealed override void FreeAfterUse()
|
protected sealed override void FreeAfterUse()
|
||||||
@ -111,6 +124,20 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void apply(TEntry entry)
|
||||||
|
{
|
||||||
|
if (HasEntryApplied)
|
||||||
|
free();
|
||||||
|
|
||||||
|
this.entry = entry;
|
||||||
|
entry.LifetimeChanged += setLifetimeFromEntry;
|
||||||
|
setLifetimeFromEntry(entry);
|
||||||
|
|
||||||
|
OnApply(entry);
|
||||||
|
|
||||||
|
HasEntryApplied = true;
|
||||||
|
}
|
||||||
|
|
||||||
private void free()
|
private void free()
|
||||||
{
|
{
|
||||||
Debug.Assert(Entry != null && HasEntryApplied);
|
Debug.Assert(Entry != null && HasEntryApplied);
|
||||||
@ -118,7 +145,7 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
|||||||
OnFree(Entry);
|
OnFree(Entry);
|
||||||
|
|
||||||
Entry.LifetimeChanged -= setLifetimeFromEntry;
|
Entry.LifetimeChanged -= setLifetimeFromEntry;
|
||||||
Entry = null;
|
entry = null;
|
||||||
base.LifetimeStart = double.MinValue;
|
base.LifetimeStart = double.MinValue;
|
||||||
base.LifetimeEnd = double.MaxValue;
|
base.LifetimeEnd = double.MaxValue;
|
||||||
|
|
||||||
|
@ -96,14 +96,26 @@ namespace osu.Game.Rulesets
|
|||||||
|
|
||||||
context.SaveChanges();
|
context.SaveChanges();
|
||||||
|
|
||||||
// add any other modes
|
|
||||||
var existingRulesets = context.RulesetInfo.ToList();
|
var existingRulesets = context.RulesetInfo.ToList();
|
||||||
|
|
||||||
|
// add any other rulesets which have assemblies present but are not yet in the database.
|
||||||
foreach (var r in instances.Where(r => !(r is ILegacyRuleset)))
|
foreach (var r in instances.Where(r => !(r is ILegacyRuleset)))
|
||||||
{
|
{
|
||||||
if (existingRulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null)
|
if (existingRulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null)
|
||||||
|
{
|
||||||
|
var existingSameShortName = existingRulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName);
|
||||||
|
|
||||||
|
if (existingSameShortName != null)
|
||||||
|
{
|
||||||
|
// even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName.
|
||||||
|
// this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one.
|
||||||
|
// in such cases, update the instantiation info of the existing entry to point to the new one.
|
||||||
|
existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo;
|
||||||
|
}
|
||||||
|
else
|
||||||
context.RulesetInfo.Add(r.RulesetInfo);
|
context.RulesetInfo.Add(r.RulesetInfo);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
context.SaveChanges();
|
context.SaveChanges();
|
||||||
|
|
||||||
|
@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.UI
|
|||||||
{
|
{
|
||||||
base.ReloadMappings();
|
base.ReloadMappings();
|
||||||
|
|
||||||
KeyBindings = KeyBindings.Where(b => KeyBindingStore.CheckValidForGameplay(b.KeyCombination)).ToList();
|
KeyBindings = KeyBindings.Where(b => RealmKeyBindingStore.CheckValidForGameplay(b.KeyCombination)).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -295,12 +295,12 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
DimmableStoryboard.HasStoryboardEnded.ValueChanged += storyboardEnded =>
|
DimmableStoryboard.HasStoryboardEnded.ValueChanged += storyboardEnded =>
|
||||||
{
|
{
|
||||||
if (storyboardEnded.NewValue && resultsDisplayDelegate == null)
|
if (storyboardEnded.NewValue)
|
||||||
updateCompletionState();
|
progressToResults(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Bind the judgement processors to ourselves
|
// Bind the judgement processors to ourselves
|
||||||
ScoreProcessor.HasCompleted.BindValueChanged(_ => updateCompletionState());
|
ScoreProcessor.HasCompleted.BindValueChanged(scoreCompletionChanged);
|
||||||
HealthProcessor.Failed += onFail;
|
HealthProcessor.Failed += onFail;
|
||||||
|
|
||||||
foreach (var mod in Mods.Value.OfType<IApplicableToScoreProcessor>())
|
foreach (var mod in Mods.Value.OfType<IApplicableToScoreProcessor>())
|
||||||
@ -374,7 +374,7 @@ namespace osu.Game.Screens.Play
|
|||||||
},
|
},
|
||||||
skipOutroOverlay = new SkipOverlay(Beatmap.Value.Storyboard.LatestEventTime ?? 0)
|
skipOutroOverlay = new SkipOverlay(Beatmap.Value.Storyboard.LatestEventTime ?? 0)
|
||||||
{
|
{
|
||||||
RequestSkip = () => updateCompletionState(true),
|
RequestSkip = () => progressToResults(false),
|
||||||
Alpha = 0
|
Alpha = 0
|
||||||
},
|
},
|
||||||
FailOverlay = new FailOverlay
|
FailOverlay = new FailOverlay
|
||||||
@ -643,9 +643,8 @@ namespace osu.Game.Screens.Play
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles changes in player state which may progress the completion of gameplay / this screen's lifetime.
|
/// Handles changes in player state which may progress the completion of gameplay / this screen's lifetime.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="skipStoryboardOutro">If in a state where a storyboard outro is to be played, offers the choice of skipping beyond it.</param>
|
|
||||||
/// <exception cref="InvalidOperationException">Thrown if this method is called more than once without changing state.</exception>
|
/// <exception cref="InvalidOperationException">Thrown if this method is called more than once without changing state.</exception>
|
||||||
private void updateCompletionState(bool skipStoryboardOutro = false)
|
private void scoreCompletionChanged(ValueChangedEvent<bool> completed)
|
||||||
{
|
{
|
||||||
// If this player instance is in the middle of an exit, don't attempt any kind of state update.
|
// If this player instance is in the middle of an exit, don't attempt any kind of state update.
|
||||||
if (!this.IsCurrentScreen())
|
if (!this.IsCurrentScreen())
|
||||||
@ -656,7 +655,7 @@ namespace osu.Game.Screens.Play
|
|||||||
// Currently, even if this scenario is hit, prepareScoreForDisplay has already been queued (and potentially run).
|
// Currently, even if this scenario is hit, prepareScoreForDisplay has already been queued (and potentially run).
|
||||||
// In scenarios where rewinding is possible (replay, spectating) this is a non-issue as no submission/import work is done,
|
// In scenarios where rewinding is possible (replay, spectating) this is a non-issue as no submission/import work is done,
|
||||||
// but it still doesn't feel right that this exists here.
|
// but it still doesn't feel right that this exists here.
|
||||||
if (!ScoreProcessor.HasCompleted.Value)
|
if (!completed.NewValue)
|
||||||
{
|
{
|
||||||
resultsDisplayDelegate?.Cancel();
|
resultsDisplayDelegate?.Cancel();
|
||||||
resultsDisplayDelegate = null;
|
resultsDisplayDelegate = null;
|
||||||
@ -666,9 +665,6 @@ namespace osu.Game.Screens.Play
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resultsDisplayDelegate != null)
|
|
||||||
throw new InvalidOperationException(@$"{nameof(updateCompletionState)} should never be fired more than once.");
|
|
||||||
|
|
||||||
// Only show the completion screen if the player hasn't failed
|
// Only show the completion screen if the player hasn't failed
|
||||||
if (HealthProcessor.HasFailed)
|
if (HealthProcessor.HasFailed)
|
||||||
return;
|
return;
|
||||||
@ -683,27 +679,25 @@ namespace osu.Game.Screens.Play
|
|||||||
if (!Configuration.ShowResults)
|
if (!Configuration.ShowResults)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Asynchronously run score preparation operations (database import, online submission etc.).
|
|
||||||
prepareScoreForDisplayTask ??= Task.Run(prepareScoreForResults);
|
prepareScoreForDisplayTask ??= Task.Run(prepareScoreForResults);
|
||||||
|
|
||||||
if (skipStoryboardOutro)
|
|
||||||
{
|
|
||||||
scheduleCompletion();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value;
|
bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value;
|
||||||
|
|
||||||
if (storyboardHasOutro)
|
if (storyboardHasOutro)
|
||||||
{
|
{
|
||||||
|
// if the current beatmap has a storyboard, the progression to results will be handled by the storyboard ending
|
||||||
|
// or the user pressing the skip outro button.
|
||||||
skipOutroOverlay.Show();
|
skipOutroOverlay.Show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY))
|
progressToResults(true);
|
||||||
scheduleCompletion();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Asynchronously run score preparation operations (database import, online submission etc.).
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The final score.</returns>
|
||||||
private async Task<ScoreInfo> prepareScoreForResults()
|
private async Task<ScoreInfo> prepareScoreForResults()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -727,18 +721,44 @@ namespace osu.Game.Screens.Play
|
|||||||
return Score.ScoreInfo;
|
return Score.ScoreInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void scheduleCompletion() => resultsDisplayDelegate = Schedule(() =>
|
/// <summary>
|
||||||
|
/// Queue the results screen for display.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// A final display will only occur once all work is completed in <see cref="PrepareScoreForResultsAsync"/>. This means that even after calling this method, the results screen will never be shown until <see cref="JudgementProcessor.HasCompleted">ScoreProcessor.HasCompleted</see> becomes <see langword="true"/>.
|
||||||
|
///
|
||||||
|
/// Calling this method multiple times will have no effect.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="withDelay">Whether a minimum delay (<see cref="RESULTS_DISPLAY_DELAY"/>) should be added before the screen is displayed.</param>
|
||||||
|
private void progressToResults(bool withDelay)
|
||||||
{
|
{
|
||||||
if (!prepareScoreForDisplayTask.IsCompleted)
|
if (resultsDisplayDelegate != null)
|
||||||
{
|
// Note that if progressToResults is called one withDelay=true and then withDelay=false, this no-delay timing will not be
|
||||||
scheduleCompletion();
|
// accounted for. shouldn't be a huge concern (a user pressing the skip button after a results progression has already been queued
|
||||||
|
// may take x00 more milliseconds than expected in the very rare edge case).
|
||||||
|
//
|
||||||
|
// If required we can handle this more correctly by rescheduling here.
|
||||||
|
return;
|
||||||
|
|
||||||
|
double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0;
|
||||||
|
|
||||||
|
resultsDisplayDelegate = new ScheduledDelegate(() =>
|
||||||
|
{
|
||||||
|
if (prepareScoreForDisplayTask?.IsCompleted != true)
|
||||||
|
// If the asynchronous preparation has not completed, keep repeating this delegate.
|
||||||
|
return;
|
||||||
|
|
||||||
|
resultsDisplayDelegate?.Cancel();
|
||||||
|
|
||||||
|
if (!this.IsCurrentScreen())
|
||||||
|
// This player instance may already be in the process of exiting.
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
// screen may be in the exiting transition phase.
|
|
||||||
if (this.IsCurrentScreen())
|
|
||||||
this.Push(CreateResults(prepareScoreForDisplayTask.Result));
|
this.Push(CreateResults(prepareScoreForDisplayTask.Result));
|
||||||
});
|
}, Time.Current + delay, 50);
|
||||||
|
|
||||||
|
Scheduler.Add(resultsDisplayDelegate);
|
||||||
|
}
|
||||||
|
|
||||||
protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value;
|
protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value;
|
||||||
|
|
||||||
@ -936,14 +956,6 @@ namespace osu.Game.Screens.Play
|
|||||||
{
|
{
|
||||||
screenSuspension?.Expire();
|
screenSuspension?.Expire();
|
||||||
|
|
||||||
// if the results screen is prepared to be displayed, forcefully show it on an exit request.
|
|
||||||
// usually if a user has completed a play session they do want to see results. and if they don't they can hit the same key a second time.
|
|
||||||
if (resultsDisplayDelegate != null && !resultsDisplayDelegate.Cancelled && !resultsDisplayDelegate.Completed)
|
|
||||||
{
|
|
||||||
resultsDisplayDelegate.RunTask();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous.
|
// EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous.
|
||||||
// To resolve test failures, forcefully end playing synchronously when this screen exits.
|
// To resolve test failures, forcefully end playing synchronously when this screen exits.
|
||||||
// Todo: Replace this with a more permanent solution once osu-framework has a synchronous cleanup method.
|
// Todo: Replace this with a more permanent solution once osu-framework has a synchronous cleanup method.
|
||||||
|
@ -66,7 +66,7 @@ namespace osu.Game.Screens.Select
|
|||||||
private readonly Box light;
|
private readonly Box light;
|
||||||
|
|
||||||
public FooterButton()
|
public FooterButton()
|
||||||
: base(HoverSampleSet.SongSelect)
|
: base(HoverSampleSet.Button)
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Both;
|
AutoSizeAxes = Axes.Both;
|
||||||
Shear = SHEAR;
|
Shear = SHEAR;
|
||||||
|
@ -25,7 +25,7 @@ namespace osu.Game.Storyboards.Drawables
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public IBindable<bool> HasStoryboardEnded => hasStoryboardEnded;
|
public IBindable<bool> HasStoryboardEnded => hasStoryboardEnded;
|
||||||
|
|
||||||
private readonly BindableBool hasStoryboardEnded = new BindableBool();
|
private readonly BindableBool hasStoryboardEnded = new BindableBool(true);
|
||||||
|
|
||||||
protected override Container<DrawableStoryboardLayer> Content { get; }
|
protected override Container<DrawableStoryboardLayer> Content { get; }
|
||||||
|
|
||||||
|
@ -60,6 +60,9 @@ namespace osu.Game.Utils
|
|||||||
{
|
{
|
||||||
foreach (var invalid in combination.Where(m => type.IsInstanceOfType(m)))
|
foreach (var invalid in combination.Where(m => type.IsInstanceOfType(m)))
|
||||||
{
|
{
|
||||||
|
if (invalid == mod)
|
||||||
|
continue;
|
||||||
|
|
||||||
invalidMods ??= new List<Mod>();
|
invalidMods ??= new List<Mod>();
|
||||||
invalidMods.Add(invalid);
|
invalidMods.Add(invalid);
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
|
<PackageReference Include="AutoMapper" Version="10.1.1" />
|
||||||
<PackageReference Include="DiffPlex" Version="1.7.0" />
|
<PackageReference Include="DiffPlex" Version="1.7.0" />
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.33" />
|
<PackageReference Include="HtmlAgilityPack" Version="1.11.33" />
|
||||||
<PackageReference Include="Humanizer" Version="2.10.1" />
|
<PackageReference Include="Humanizer" Version="2.10.1" />
|
||||||
@ -34,8 +35,9 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="Realm" Version="10.2.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2021.616.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2021.616.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.614.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.618.0" />
|
||||||
<PackageReference Include="Sentry" Version="3.4.0" />
|
<PackageReference Include="Sentry" Version="3.4.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.28.2" />
|
<PackageReference Include="SharpCompress" Version="0.28.2" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||||
|
@ -71,7 +71,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.616.0" />
|
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.616.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.614.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.618.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
|
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user