mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 03:15:36 +08:00
Merge branch 'master' into argon-mania-hold-tail-no-sprite
This commit is contained in:
commit
c428565e05
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@ -121,21 +121,12 @@ jobs:
|
||||
|
||||
build-only-ios:
|
||||
name: Build only (iOS)
|
||||
# change to macos-latest once GitHub finishes migrating all repositories to macOS 12.
|
||||
runs-on: macos-12
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# see https://github.com/actions/runner-images/issues/6771#issuecomment-1354713617
|
||||
# remove once all workflow VMs use Xcode 14.1
|
||||
- name: Set Xcode Version
|
||||
shell: bash
|
||||
run: |
|
||||
sudo xcode-select -s "/Applications/Xcode_14.1.app"
|
||||
echo "MD_APPLE_SDK_ROOT=/Applications/Xcode_14.1.app" >> $GITHUB_ENV
|
||||
|
||||
- name: Install .NET 6.0.x
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
|
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.120.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.131.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
@ -113,6 +113,7 @@ namespace osu.Game.Rulesets.Catch
|
||||
new MultiMod(new CatchModDoubleTime(), new CatchModNightcore()),
|
||||
new CatchModHidden(),
|
||||
new CatchModFlashlight(),
|
||||
new ModAccuracyChallenge(),
|
||||
};
|
||||
|
||||
case ModType.Conversion:
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 99 KiB |
@ -14,4 +14,6 @@ Hit200: mania/hit200@2x
|
||||
Hit300: mania/hit300@2x
|
||||
Hit300g: mania/hit300g@2x
|
||||
StageLeft: mania/stage-left
|
||||
StageRight: mania/stage-right
|
||||
StageRight: mania/stage-right
|
||||
NoteImage0L: LongNoteTailWang
|
||||
NoteImage1L: LongNoteTailWang
|
||||
|
@ -245,6 +245,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
new MultiMod(new ManiaModDoubleTime(), new ManiaModNightcore()),
|
||||
new MultiMod(new ManiaModFadeIn(), new ManiaModHidden()),
|
||||
new ManiaModFlashlight(),
|
||||
new ModAccuracyChallenge(),
|
||||
};
|
||||
|
||||
case ModType.Conversion:
|
||||
|
@ -54,6 +54,9 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
float lightScale = GetColumnSkinConfig<float>(skin, LegacyManiaSkinConfigurationLookups.HoldNoteLightScale)?.Value
|
||||
?? 1;
|
||||
|
||||
float minimumColumnWidth = GetColumnSkinConfig<float>(skin, LegacyManiaSkinConfigurationLookups.MinimumColumnWidth)?.Value
|
||||
?? 1;
|
||||
|
||||
// Create a temporary animation to retrieve the number of frames, in an effort to calculate the intended frame length.
|
||||
// This animation is discarded and re-queried with the appropriate frame length afterwards.
|
||||
var tmp = skin.GetAnimation(lightImage, true, false);
|
||||
@ -92,7 +95,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
d.RelativeSizeAxes = Axes.Both;
|
||||
d.Size = Vector2.One;
|
||||
d.FillMode = FillMode.Stretch;
|
||||
// Todo: Wrap
|
||||
d.Height = minimumColumnWidth / d.DrawWidth * 1.6f; // constant matching stable.
|
||||
// Todo: Wrap?
|
||||
});
|
||||
|
||||
if (bodySprite != null)
|
||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
public partial class TestScenePathControlPointVisualiser : OsuManualInputManagerTestScene
|
||||
{
|
||||
private Slider slider;
|
||||
private PathControlPointVisualiser visualiser;
|
||||
private PathControlPointVisualiser<Slider> visualiser;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertControlPointPathType(3, null);
|
||||
}
|
||||
|
||||
private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser(slider, allowSelection)
|
||||
private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser<Slider>(slider, allowSelection)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
|
@ -159,11 +159,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
}
|
||||
|
||||
private void assertSelectionCount(int count) =>
|
||||
AddAssert($"{count} control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece>().Count(piece => piece.IsSelected.Value) == count);
|
||||
AddAssert($"{count} control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece<Slider>>().Count(piece => piece.IsSelected.Value) == count);
|
||||
|
||||
private void assertSelected(int index) =>
|
||||
AddAssert($"{(index + 1).ToOrdinalWords()} control point piece selected",
|
||||
() => this.ChildrenOfType<PathControlPointPiece>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[index]).IsSelected.Value);
|
||||
() => this.ChildrenOfType<PathControlPointPiece<Slider>>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[index]).IsSelected.Value);
|
||||
|
||||
private void moveMouseToRelativePosition(Vector2 relativePosition) =>
|
||||
AddStep($"move mouse to {relativePosition}", () =>
|
||||
@ -202,12 +202,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
moveMouseToControlPoint(2);
|
||||
AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left));
|
||||
|
||||
AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece>().Count(piece => piece.IsSelected.Value) == 3);
|
||||
AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece<Slider>>().Count(piece => piece.IsSelected.Value) == 3);
|
||||
|
||||
addMovementStep(new Vector2(450, 50));
|
||||
AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece>().Count(piece => piece.IsSelected.Value) == 3);
|
||||
AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece<Slider>>().Count(piece => piece.IsSelected.Value) == 3);
|
||||
|
||||
assertControlPointPosition(2, new Vector2(450, 50));
|
||||
assertControlPointType(2, PathType.PerfectCurve);
|
||||
@ -236,12 +236,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
moveMouseToControlPoint(3);
|
||||
AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left));
|
||||
|
||||
AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece>().Count(piece => piece.IsSelected.Value) == 3);
|
||||
AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece<Slider>>().Count(piece => piece.IsSelected.Value) == 3);
|
||||
|
||||
addMovementStep(new Vector2(550, 50));
|
||||
AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece>().Count(piece => piece.IsSelected.Value) == 3);
|
||||
AddAssert("three control point pieces selected", () => this.ChildrenOfType<PathControlPointPiece<Slider>>().Count(piece => piece.IsSelected.Value) == 3);
|
||||
|
||||
// note: if the head is part of the selection being moved, the entire slider is moved.
|
||||
// the unselected nodes will therefore change position relative to the slider head.
|
||||
@ -354,7 +354,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
public new SliderBodyPiece BodyPiece => base.BodyPiece;
|
||||
public new TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay;
|
||||
public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailOverlay;
|
||||
public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser;
|
||||
public new PathControlPointVisualiser<Slider> ControlPointVisualiser => base.ControlPointVisualiser;
|
||||
|
||||
public TestSliderBlueprint(Slider slider)
|
||||
: base(slider)
|
||||
|
@ -199,7 +199,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
public new SliderBodyPiece BodyPiece => base.BodyPiece;
|
||||
public new TestSliderCircleOverlay HeadOverlay => (TestSliderCircleOverlay)base.HeadOverlay;
|
||||
public new TestSliderCircleOverlay TailOverlay => (TestSliderCircleOverlay)base.TailOverlay;
|
||||
public new PathControlPointVisualiser ControlPointVisualiser => base.ControlPointVisualiser;
|
||||
public new PathControlPointVisualiser<Slider> ControlPointVisualiser => base.ControlPointVisualiser;
|
||||
|
||||
public TestSliderBlueprint(Slider slider)
|
||||
: base(slider)
|
||||
|
@ -72,14 +72,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
[Test]
|
||||
public void TestMovingUnsnappedSliderNodesSnaps()
|
||||
{
|
||||
PathControlPointPiece sliderEnd = null;
|
||||
PathControlPointPiece<Slider> sliderEnd = null;
|
||||
|
||||
assertSliderSnapped(false);
|
||||
|
||||
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
|
||||
AddStep("select slider end", () =>
|
||||
{
|
||||
sliderEnd = this.ChildrenOfType<PathControlPointPiece>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints.Last());
|
||||
sliderEnd = this.ChildrenOfType<PathControlPointPiece<Slider>>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints.Last());
|
||||
InputManager.MoveMouseTo(sliderEnd.ScreenSpaceDrawQuad.Centre);
|
||||
});
|
||||
AddStep("move slider end", () =>
|
||||
@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
|
||||
AddStep("move mouse to new point location", () =>
|
||||
{
|
||||
var firstPiece = this.ChildrenOfType<PathControlPointPiece>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[0]);
|
||||
var firstPiece = this.ChildrenOfType<PathControlPointPiece<Slider>>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[0]);
|
||||
var pos = slider.Path.PositionAt(0.25d) + slider.Position;
|
||||
InputManager.MoveMouseTo(firstPiece.Parent.ToScreenSpace(pos));
|
||||
});
|
||||
@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
|
||||
AddStep("move mouse to second control point", () =>
|
||||
{
|
||||
var secondPiece = this.ChildrenOfType<PathControlPointPiece>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[1]);
|
||||
var secondPiece = this.ChildrenOfType<PathControlPointPiece<Slider>>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[1]);
|
||||
InputManager.MoveMouseTo(secondPiece);
|
||||
});
|
||||
AddStep("quick delete", () =>
|
||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
=> Editor.ChildrenOfType<ComposeBlueprintContainer>().First();
|
||||
|
||||
private Slider? slider;
|
||||
private PathControlPointVisualiser? visualiser;
|
||||
private PathControlPointVisualiser<Slider>? visualiser;
|
||||
|
||||
private const double split_gap = 100;
|
||||
|
||||
@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
AddStep("select added slider", () =>
|
||||
{
|
||||
EditorBeatmap.SelectedHitObjects.Add(slider);
|
||||
visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser>().First();
|
||||
visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser<Slider>>().First();
|
||||
});
|
||||
|
||||
moveMouseToControlPoint(2);
|
||||
@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
AddStep("select added slider", () =>
|
||||
{
|
||||
EditorBeatmap.SelectedHitObjects.Add(slider);
|
||||
visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser>().First();
|
||||
visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser<Slider>>().First();
|
||||
});
|
||||
|
||||
moveMouseToControlPoint(2);
|
||||
@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
AddStep("select added slider", () =>
|
||||
{
|
||||
EditorBeatmap.SelectedHitObjects.Add(slider);
|
||||
visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser>().First();
|
||||
visualiser = blueprintContainer.SelectionBlueprints.First(o => o.Item == slider).ChildrenOfType<PathControlPointVisualiser<Slider>>().First();
|
||||
});
|
||||
|
||||
moveMouseToControlPoint(2);
|
||||
|
@ -5,9 +5,11 @@
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
@ -22,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
private int depthIndex;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
||||
[Test]
|
||||
public void TestHits()
|
||||
{
|
||||
@ -56,6 +61,13 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddStep("Hit stream late", () => SetContents(_ => testStream(5, true, 150)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitLighting()
|
||||
{
|
||||
AddToggleStep("toggle hit lighting", v => config.SetValue(OsuSetting.HitLighting, v));
|
||||
AddStep("Hit Big Single", () => SetContents(_ => testSingle(2, true)));
|
||||
}
|
||||
|
||||
private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null)
|
||||
{
|
||||
var playfield = new TestOsuPlayfield();
|
||||
|
@ -4,29 +4,38 @@
|
||||
#nullable disable
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneHitCircleKiai : TestSceneHitCircle
|
||||
public partial class TestSceneHitCircleKiai : TestSceneHitCircle, IBeatSyncProvider
|
||||
{
|
||||
private ControlPointInfo controlPoints { get; set; }
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
var controlPointInfo = new ControlPointInfo();
|
||||
controlPoints = new ControlPointInfo();
|
||||
|
||||
controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
|
||||
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
|
||||
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
|
||||
controlPoints.Add(0, new EffectControlPoint { KiaiMode = true });
|
||||
|
||||
Beatmap.Value = CreateWorkingBeatmap(new Beatmap
|
||||
{
|
||||
ControlPointInfo = controlPointInfo
|
||||
ControlPointInfo = controlPoints
|
||||
});
|
||||
|
||||
// track needs to be playing for BeatSyncedContainer to work.
|
||||
Beatmap.Value.Track.Start();
|
||||
});
|
||||
|
||||
ChannelAmplitudes IHasAmplitudes.CurrentAmplitudes => new ChannelAmplitudes();
|
||||
ControlPointInfo IBeatSyncProvider.ControlPoints => controlPoints;
|
||||
IClock IBeatSyncProvider.Clock => Clock;
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,14 @@ using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Input.States;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
@ -22,7 +29,7 @@ using osuTK.Graphics;
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneTouchInput : OsuManualInputManagerTestScene
|
||||
public partial class TestSceneOsuTouchInput : OsuManualInputManagerTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
@ -33,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
private OsuInputManager osuInputManager = null!;
|
||||
|
||||
private Container mainContent = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
@ -44,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
osuInputManager = new OsuInputManager(new OsuRuleset().RulesetInfo)
|
||||
{
|
||||
Child = new Container
|
||||
Child = mainContent = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@ -54,13 +63,19 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.CentreRight,
|
||||
Depth = float.MinValue,
|
||||
X = -100,
|
||||
},
|
||||
rightKeyCounter = new TestActionKeyCounter(OsuAction.RightButton)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Depth = float.MinValue,
|
||||
X = 100,
|
||||
},
|
||||
new OsuCursorContainer
|
||||
{
|
||||
Depth = float.MinValue,
|
||||
}
|
||||
},
|
||||
}
|
||||
@ -70,6 +85,40 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStreamInputVisual()
|
||||
{
|
||||
addHitCircleAt(TouchSource.Touch1);
|
||||
addHitCircleAt(TouchSource.Touch2);
|
||||
|
||||
beginTouch(TouchSource.Touch1);
|
||||
beginTouch(TouchSource.Touch2);
|
||||
|
||||
endTouch(TouchSource.Touch1);
|
||||
|
||||
int i = 0;
|
||||
|
||||
AddRepeatStep("Alternate", () =>
|
||||
{
|
||||
TouchSource down = i % 2 == 0 ? TouchSource.Touch3 : TouchSource.Touch4;
|
||||
TouchSource up = i % 2 == 0 ? TouchSource.Touch4 : TouchSource.Touch3;
|
||||
|
||||
// sometimes the user will end the previous touch before touching again, sometimes not.
|
||||
if (RNG.NextBool())
|
||||
{
|
||||
InputManager.BeginTouch(new Touch(down, getSanePositionForSource(down)));
|
||||
InputManager.EndTouch(new Touch(up, getSanePositionForSource(up)));
|
||||
}
|
||||
else
|
||||
{
|
||||
InputManager.EndTouch(new Touch(up, getSanePositionForSource(up)));
|
||||
InputManager.BeginTouch(new Touch(down, getSanePositionForSource(down)));
|
||||
}
|
||||
|
||||
i++;
|
||||
}, 100);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSimpleInput()
|
||||
{
|
||||
@ -116,9 +165,224 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
endTouch(TouchSource.Touch2);
|
||||
checkPosition(TouchSource.Touch2);
|
||||
|
||||
// note that touch1 was never ended, but becomes active for tracking again.
|
||||
// note that touch1 was never ended, but is no longer valid for touch input due to touch 2 occurring.
|
||||
beginTouch(TouchSource.Touch1);
|
||||
checkPosition(TouchSource.Touch2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStreamInput()
|
||||
{
|
||||
// In this scenario, the user is tapping on the first object in a stream,
|
||||
// then using one or two fingers in empty space to continue the stream.
|
||||
|
||||
addHitCircleAt(TouchSource.Touch1);
|
||||
beginTouch(TouchSource.Touch1);
|
||||
|
||||
// The first touch is handled as normal.
|
||||
assertKeyCounter(1, 0);
|
||||
checkPressed(OsuAction.LeftButton);
|
||||
checkPosition(TouchSource.Touch1);
|
||||
|
||||
// The second touch should release the first, and also act as a right button.
|
||||
beginTouch(TouchSource.Touch2);
|
||||
|
||||
assertKeyCounter(1, 1);
|
||||
// Importantly, this is different from the simple case because an object was interacted with in the first touch, but not the second touch.
|
||||
// left button is automatically released.
|
||||
checkNotPressed(OsuAction.LeftButton);
|
||||
checkPressed(OsuAction.RightButton);
|
||||
// Also importantly, the positional part of the second touch is ignored.
|
||||
checkPosition(TouchSource.Touch1);
|
||||
|
||||
// In this scenario, a third touch should be allowed, and handled similarly to the second.
|
||||
beginTouch(TouchSource.Touch3);
|
||||
|
||||
assertKeyCounter(2, 1);
|
||||
checkPressed(OsuAction.LeftButton);
|
||||
checkPressed(OsuAction.RightButton);
|
||||
// Position is still ignored.
|
||||
checkPosition(TouchSource.Touch1);
|
||||
|
||||
endTouch(TouchSource.Touch2);
|
||||
|
||||
checkPressed(OsuAction.LeftButton);
|
||||
checkNotPressed(OsuAction.RightButton);
|
||||
// Position is still ignored.
|
||||
checkPosition(TouchSource.Touch1);
|
||||
|
||||
// User continues streaming
|
||||
beginTouch(TouchSource.Touch2);
|
||||
|
||||
assertKeyCounter(2, 2);
|
||||
checkPressed(OsuAction.LeftButton);
|
||||
checkPressed(OsuAction.RightButton);
|
||||
// Position is still ignored.
|
||||
checkPosition(TouchSource.Touch1);
|
||||
|
||||
// In this mode a maximum of three touches should be supported.
|
||||
// A fourth touch should result in no changes anywhere.
|
||||
beginTouch(TouchSource.Touch4);
|
||||
assertKeyCounter(2, 2);
|
||||
checkPressed(OsuAction.LeftButton);
|
||||
checkPressed(OsuAction.RightButton);
|
||||
checkPosition(TouchSource.Touch1);
|
||||
endTouch(TouchSource.Touch4);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStreamInputWithInitialTouchDownLeft()
|
||||
{
|
||||
// In this scenario, the user is wanting to use stream input but we start with one finger still on the screen.
|
||||
// That finger is mapped to a left action.
|
||||
|
||||
addHitCircleAt(TouchSource.Touch2);
|
||||
|
||||
beginTouch(TouchSource.Touch1);
|
||||
assertKeyCounter(1, 0);
|
||||
checkPressed(OsuAction.LeftButton);
|
||||
checkPosition(TouchSource.Touch1);
|
||||
|
||||
// hits circle as right action
|
||||
beginTouch(TouchSource.Touch2);
|
||||
assertKeyCounter(1, 1);
|
||||
checkPressed(OsuAction.LeftButton);
|
||||
checkPressed(OsuAction.RightButton);
|
||||
checkPosition(TouchSource.Touch2);
|
||||
|
||||
endTouch(TouchSource.Touch1);
|
||||
checkNotPressed(OsuAction.LeftButton);
|
||||
|
||||
// stream using other two fingers while touch2 tracks
|
||||
beginTouch(TouchSource.Touch1);
|
||||
assertKeyCounter(2, 1);
|
||||
checkPressed(OsuAction.LeftButton);
|
||||
// right button is automatically released
|
||||
checkNotPressed(OsuAction.RightButton);
|
||||
checkPosition(TouchSource.Touch2);
|
||||
|
||||
beginTouch(TouchSource.Touch3);
|
||||
assertKeyCounter(2, 2);
|
||||
checkPressed(OsuAction.LeftButton);
|
||||
checkPressed(OsuAction.RightButton);
|
||||
checkPosition(TouchSource.Touch2);
|
||||
|
||||
endTouch(TouchSource.Touch1);
|
||||
checkNotPressed(OsuAction.LeftButton);
|
||||
|
||||
beginTouch(TouchSource.Touch1);
|
||||
assertKeyCounter(3, 2);
|
||||
checkPressed(OsuAction.LeftButton);
|
||||
checkPressed(OsuAction.RightButton);
|
||||
checkPosition(TouchSource.Touch2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStreamInputWithInitialTouchDownRight()
|
||||
{
|
||||
// In this scenario, the user is wanting to use stream input but we start with one finger still on the screen.
|
||||
// That finger is mapped to a right action.
|
||||
|
||||
beginTouch(TouchSource.Touch1);
|
||||
beginTouch(TouchSource.Touch2);
|
||||
|
||||
assertKeyCounter(1, 1);
|
||||
checkPressed(OsuAction.LeftButton);
|
||||
checkPressed(OsuAction.RightButton);
|
||||
|
||||
endTouch(TouchSource.Touch1);
|
||||
|
||||
addHitCircleAt(TouchSource.Touch1);
|
||||
|
||||
// hits circle as left action
|
||||
beginTouch(TouchSource.Touch1);
|
||||
assertKeyCounter(2, 1);
|
||||
checkPressed(OsuAction.LeftButton);
|
||||
checkPressed(OsuAction.RightButton);
|
||||
checkPosition(TouchSource.Touch1);
|
||||
|
||||
endTouch(TouchSource.Touch2);
|
||||
|
||||
// stream using other two fingers while touch1 tracks
|
||||
beginTouch(TouchSource.Touch2);
|
||||
assertKeyCounter(2, 2);
|
||||
checkPressed(OsuAction.RightButton);
|
||||
// left button is automatically released
|
||||
checkNotPressed(OsuAction.LeftButton);
|
||||
checkPosition(TouchSource.Touch1);
|
||||
|
||||
beginTouch(TouchSource.Touch3);
|
||||
assertKeyCounter(3, 2);
|
||||
checkPressed(OsuAction.LeftButton);
|
||||
checkPressed(OsuAction.RightButton);
|
||||
checkPosition(TouchSource.Touch1);
|
||||
|
||||
endTouch(TouchSource.Touch2);
|
||||
checkNotPressed(OsuAction.RightButton);
|
||||
|
||||
beginTouch(TouchSource.Touch2);
|
||||
assertKeyCounter(3, 3);
|
||||
checkPressed(OsuAction.LeftButton);
|
||||
checkPressed(OsuAction.RightButton);
|
||||
checkPosition(TouchSource.Touch1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNonStreamOverlappingDirectTouchesWithRelease()
|
||||
{
|
||||
// In this scenario, the user is tapping on three circles directly while correctly releasing the first touch.
|
||||
// All three should be recognised.
|
||||
|
||||
addHitCircleAt(TouchSource.Touch1);
|
||||
addHitCircleAt(TouchSource.Touch2);
|
||||
addHitCircleAt(TouchSource.Touch3);
|
||||
|
||||
beginTouch(TouchSource.Touch1);
|
||||
assertKeyCounter(1, 0);
|
||||
checkPressed(OsuAction.LeftButton);
|
||||
checkPosition(TouchSource.Touch1);
|
||||
|
||||
beginTouch(TouchSource.Touch2);
|
||||
assertKeyCounter(1, 1);
|
||||
checkPressed(OsuAction.LeftButton);
|
||||
checkPressed(OsuAction.RightButton);
|
||||
checkPosition(TouchSource.Touch2);
|
||||
|
||||
endTouch(TouchSource.Touch1);
|
||||
|
||||
beginTouch(TouchSource.Touch3);
|
||||
assertKeyCounter(2, 1);
|
||||
checkPressed(OsuAction.LeftButton);
|
||||
checkPressed(OsuAction.RightButton);
|
||||
checkPosition(TouchSource.Touch3);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNonStreamOverlappingDirectTouchesWithoutRelease()
|
||||
{
|
||||
// In this scenario, the user is tapping on three circles directly without releasing any touches.
|
||||
// The first two should be recognised, but a third should not (as the user already has two fingers down).
|
||||
|
||||
addHitCircleAt(TouchSource.Touch1);
|
||||
addHitCircleAt(TouchSource.Touch2);
|
||||
addHitCircleAt(TouchSource.Touch3);
|
||||
|
||||
beginTouch(TouchSource.Touch1);
|
||||
assertKeyCounter(1, 0);
|
||||
checkPressed(OsuAction.LeftButton);
|
||||
checkPosition(TouchSource.Touch1);
|
||||
|
||||
beginTouch(TouchSource.Touch2);
|
||||
assertKeyCounter(1, 1);
|
||||
checkPressed(OsuAction.LeftButton);
|
||||
checkPressed(OsuAction.RightButton);
|
||||
checkPosition(TouchSource.Touch2);
|
||||
|
||||
beginTouch(TouchSource.Touch3);
|
||||
assertKeyCounter(1, 1);
|
||||
checkPressed(OsuAction.LeftButton);
|
||||
checkPressed(OsuAction.RightButton);
|
||||
checkPosition(TouchSource.Touch3);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -263,6 +527,22 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
assertKeyCounter(1, 1);
|
||||
}
|
||||
|
||||
private void addHitCircleAt(TouchSource source)
|
||||
{
|
||||
AddStep($"Add circle at {source}", () =>
|
||||
{
|
||||
var hitCircle = new HitCircle();
|
||||
|
||||
hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
mainContent.Add(new DrawableHitCircle(hitCircle)
|
||||
{
|
||||
Clock = new FramedClock(new ManualClock()),
|
||||
Position = mainContent.ToLocalSpace(getSanePositionForSource(source)),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void beginTouch(TouchSource source, Vector2? screenSpacePosition = null) =>
|
||||
AddStep($"Begin touch for {source}", () => InputManager.BeginTouch(new Touch(source, screenSpacePosition ??= getSanePositionForSource(source))));
|
||||
|
@ -8,34 +8,36 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Lines;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// A visualisation of the line between two <see cref="PathControlPointPiece"/>s.
|
||||
/// A visualisation of the line between two <see cref="PathControlPointPiece{T}"/>s.
|
||||
/// </summary>
|
||||
public partial class PathControlPointConnectionPiece : CompositeDrawable
|
||||
/// <typeparam name="T">The type of <see cref="OsuHitObject"/> which this <see cref="PathControlPointConnectionPiece{T}"/> visualises.</typeparam>
|
||||
public partial class PathControlPointConnectionPiece<T> : CompositeDrawable where T : OsuHitObject, IHasPath
|
||||
{
|
||||
public readonly PathControlPoint ControlPoint;
|
||||
|
||||
private readonly Path path;
|
||||
private readonly Slider slider;
|
||||
private readonly T hitObject;
|
||||
public int ControlPointIndex { get; set; }
|
||||
|
||||
private IBindable<Vector2> sliderPosition;
|
||||
private IBindable<Vector2> hitObjectPosition;
|
||||
private IBindable<int> pathVersion;
|
||||
|
||||
public PathControlPointConnectionPiece(Slider slider, int controlPointIndex)
|
||||
public PathControlPointConnectionPiece(T hitObject, int controlPointIndex)
|
||||
{
|
||||
this.slider = slider;
|
||||
this.hitObject = hitObject;
|
||||
ControlPointIndex = controlPointIndex;
|
||||
|
||||
Origin = Anchor.Centre;
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
ControlPoint = slider.Path.ControlPoints[controlPointIndex];
|
||||
ControlPoint = hitObject.Path.ControlPoints[controlPointIndex];
|
||||
|
||||
InternalChild = path = new SmoothPath
|
||||
{
|
||||
@ -48,10 +50,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
sliderPosition = slider.PositionBindable.GetBoundCopy();
|
||||
sliderPosition.BindValueChanged(_ => updateConnectingPath());
|
||||
hitObjectPosition = hitObject.PositionBindable.GetBoundCopy();
|
||||
hitObjectPosition.BindValueChanged(_ => updateConnectingPath());
|
||||
|
||||
pathVersion = slider.Path.Version.GetBoundCopy();
|
||||
pathVersion = hitObject.Path.Version.GetBoundCopy();
|
||||
pathVersion.BindValueChanged(_ => updateConnectingPath());
|
||||
|
||||
updateConnectingPath();
|
||||
@ -62,16 +64,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
/// </summary>
|
||||
private void updateConnectingPath()
|
||||
{
|
||||
Position = slider.StackedPosition + ControlPoint.Position;
|
||||
Position = hitObject.StackedPosition + ControlPoint.Position;
|
||||
|
||||
path.ClearVertices();
|
||||
|
||||
int nextIndex = ControlPointIndex + 1;
|
||||
if (nextIndex == 0 || nextIndex >= slider.Path.ControlPoints.Count)
|
||||
if (nextIndex == 0 || nextIndex >= hitObject.Path.ControlPoints.Count)
|
||||
return;
|
||||
|
||||
path.AddVertex(Vector2.Zero);
|
||||
path.AddVertex(slider.Path.ControlPoints[nextIndex].Position - ControlPoint.Position);
|
||||
path.AddVertex(hitObject.Path.ControlPoints[nextIndex].Position - ControlPoint.Position);
|
||||
|
||||
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
|
||||
}
|
||||
|
@ -29,11 +29,13 @@ using osuTK.Input;
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// A visualisation of a single <see cref="PathControlPoint"/> in a <see cref="Slider"/>.
|
||||
/// A visualisation of a single <see cref="PathControlPoint"/> in an osu hit object with a path.
|
||||
/// </summary>
|
||||
public partial class PathControlPointPiece : BlueprintPiece<Slider>, IHasTooltip
|
||||
/// <typeparam name="T">The type of <see cref="OsuHitObject"/> which this <see cref="PathControlPointPiece{T}"/> visualises.</typeparam>
|
||||
public partial class PathControlPointPiece<T> : BlueprintPiece<T>, IHasTooltip
|
||||
where T : OsuHitObject, IHasPath
|
||||
{
|
||||
public Action<PathControlPointPiece, MouseButtonEvent> RequestSelection;
|
||||
public Action<PathControlPointPiece<T>, MouseButtonEvent> RequestSelection;
|
||||
|
||||
public Action<PathControlPoint> DragStarted;
|
||||
public Action<DragEvent> DragInProgress;
|
||||
@ -44,34 +46,34 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
public readonly BindableBool IsSelected = new BindableBool();
|
||||
public readonly PathControlPoint ControlPoint;
|
||||
|
||||
private readonly Slider slider;
|
||||
private readonly T hitObject;
|
||||
private readonly Container marker;
|
||||
private readonly Drawable markerRing;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
private IBindable<Vector2> sliderPosition;
|
||||
private IBindable<float> sliderScale;
|
||||
private IBindable<Vector2> hitObjectPosition;
|
||||
private IBindable<float> hitObjectScale;
|
||||
|
||||
[UsedImplicitly]
|
||||
private readonly IBindable<int> sliderVersion;
|
||||
private readonly IBindable<int> hitObjectVersion;
|
||||
|
||||
public PathControlPointPiece(Slider slider, PathControlPoint controlPoint)
|
||||
public PathControlPointPiece(T hitObject, PathControlPoint controlPoint)
|
||||
{
|
||||
this.slider = slider;
|
||||
this.hitObject = hitObject;
|
||||
ControlPoint = controlPoint;
|
||||
|
||||
// we don't want to run the path type update on construction as it may inadvertently change the slider.
|
||||
cachePoints(slider);
|
||||
// we don't want to run the path type update on construction as it may inadvertently change the hit object.
|
||||
cachePoints(hitObject);
|
||||
|
||||
sliderVersion = slider.Path.Version.GetBoundCopy();
|
||||
hitObjectVersion = hitObject.Path.Version.GetBoundCopy();
|
||||
|
||||
// schedule ensure that updates are only applied after all operations from a single frame are applied.
|
||||
// this avoids inadvertently changing the slider path type for batch operations.
|
||||
sliderVersion.BindValueChanged(_ => Scheduler.AddOnce(() =>
|
||||
// this avoids inadvertently changing the hit object path type for batch operations.
|
||||
hitObjectVersion.BindValueChanged(_ => Scheduler.AddOnce(() =>
|
||||
{
|
||||
cachePoints(slider);
|
||||
cachePoints(hitObject);
|
||||
updatePathType();
|
||||
}));
|
||||
|
||||
@ -120,11 +122,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
sliderPosition = slider.PositionBindable.GetBoundCopy();
|
||||
sliderPosition.BindValueChanged(_ => updateMarkerDisplay());
|
||||
hitObjectPosition = hitObject.PositionBindable.GetBoundCopy();
|
||||
hitObjectPosition.BindValueChanged(_ => updateMarkerDisplay());
|
||||
|
||||
sliderScale = slider.ScaleBindable.GetBoundCopy();
|
||||
sliderScale.BindValueChanged(_ => updateMarkerDisplay());
|
||||
hitObjectScale = hitObject.ScaleBindable.GetBoundCopy();
|
||||
hitObjectScale.BindValueChanged(_ => updateMarkerDisplay());
|
||||
|
||||
IsSelected.BindValueChanged(_ => updateMarkerDisplay());
|
||||
|
||||
@ -212,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e) => DragEnded?.Invoke();
|
||||
|
||||
private void cachePoints(Slider slider) => PointsInSegment = slider.Path.PointsInSegment(ControlPoint);
|
||||
private void cachePoints(T hitObject) => PointsInSegment = hitObject.Path.PointsInSegment(ControlPoint);
|
||||
|
||||
/// <summary>
|
||||
/// Handles correction of invalid path types.
|
||||
@ -239,7 +241,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
/// </summary>
|
||||
private void updateMarkerDisplay()
|
||||
{
|
||||
Position = slider.StackedPosition + ControlPoint.Position;
|
||||
Position = hitObject.StackedPosition + ControlPoint.Position;
|
||||
|
||||
markerRing.Alpha = IsSelected.Value ? 1 : 0;
|
||||
|
||||
@ -249,7 +251,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
colour = colour.Lighten(1);
|
||||
|
||||
marker.Colour = colour;
|
||||
marker.Scale = new Vector2(slider.Scale);
|
||||
marker.Scale = new Vector2(hitObject.Scale);
|
||||
}
|
||||
|
||||
private Color4 getColourFromNodeType()
|
||||
|
@ -29,15 +29,16 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
public partial class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
|
||||
public partial class PathControlPointVisualiser<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
|
||||
where T : OsuHitObject, IHasPath
|
||||
{
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield.
|
||||
|
||||
internal readonly Container<PathControlPointPiece> Pieces;
|
||||
internal readonly Container<PathControlPointConnectionPiece> Connections;
|
||||
internal readonly Container<PathControlPointPiece<T>> Pieces;
|
||||
internal readonly Container<PathControlPointConnectionPiece<T>> Connections;
|
||||
|
||||
private readonly IBindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>();
|
||||
private readonly Slider slider;
|
||||
private readonly T hitObject;
|
||||
private readonly bool allowSelection;
|
||||
|
||||
private InputManager inputManager;
|
||||
@ -48,17 +49,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IDistanceSnapProvider snapProvider { get; set; }
|
||||
|
||||
public PathControlPointVisualiser(Slider slider, bool allowSelection)
|
||||
public PathControlPointVisualiser(T hitObject, bool allowSelection)
|
||||
{
|
||||
this.slider = slider;
|
||||
this.hitObject = hitObject;
|
||||
this.allowSelection = allowSelection;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
Connections = new Container<PathControlPointConnectionPiece> { RelativeSizeAxes = Axes.Both },
|
||||
Pieces = new Container<PathControlPointPiece> { RelativeSizeAxes = Axes.Both }
|
||||
Connections = new Container<PathControlPointConnectionPiece<T>> { RelativeSizeAxes = Axes.Both },
|
||||
Pieces = new Container<PathControlPointPiece<T>> { RelativeSizeAxes = Axes.Both }
|
||||
};
|
||||
}
|
||||
|
||||
@ -69,12 +70,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
inputManager = GetContainingInputManager();
|
||||
|
||||
controlPoints.CollectionChanged += onControlPointsChanged;
|
||||
controlPoints.BindTo(slider.Path.ControlPoints);
|
||||
controlPoints.BindTo(hitObject.Path.ControlPoints);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the <see cref="PathControlPointPiece"/> corresponding to the given <paramref name="pathControlPoint"/>,
|
||||
/// and deselects all other <see cref="PathControlPointPiece"/>s.
|
||||
/// Selects the <see cref="PathControlPointPiece{T}"/> corresponding to the given <paramref name="pathControlPoint"/>,
|
||||
/// and deselects all other <see cref="PathControlPointPiece{T}"/>s.
|
||||
/// </summary>
|
||||
public void SetSelectionTo(PathControlPoint pathControlPoint)
|
||||
{
|
||||
@ -124,8 +125,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool isSplittable(PathControlPointPiece p) =>
|
||||
// A slider can only be split on control points which connect two different slider segments.
|
||||
private bool isSplittable(PathControlPointPiece<T> p) =>
|
||||
// A hit object can only be split on control points which connect two different path segments.
|
||||
p.ControlPoint.Type.HasValue && p != Pieces.FirstOrDefault() && p != Pieces.LastOrDefault();
|
||||
|
||||
private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
@ -150,7 +151,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
var point = (PathControlPoint)e.NewItems[i];
|
||||
|
||||
Pieces.Add(new PathControlPointPiece(slider, point).With(d =>
|
||||
Pieces.Add(new PathControlPointPiece<T>(hitObject, point).With(d =>
|
||||
{
|
||||
if (allowSelection)
|
||||
d.RequestSelection = selectionRequested;
|
||||
@ -160,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
d.DragEnded = dragEnded;
|
||||
}));
|
||||
|
||||
Connections.Add(new PathControlPointConnectionPiece(slider, e.NewStartingIndex + i));
|
||||
Connections.Add(new PathControlPointConnectionPiece<T>(hitObject, e.NewStartingIndex + i));
|
||||
}
|
||||
|
||||
break;
|
||||
@ -219,7 +220,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
}
|
||||
|
||||
private void selectionRequested(PathControlPointPiece piece, MouseButtonEvent e)
|
||||
private void selectionRequested(PathControlPointPiece<T> piece, MouseButtonEvent e)
|
||||
{
|
||||
if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed)
|
||||
piece.IsSelected.Toggle();
|
||||
@ -234,7 +235,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
/// </summary>
|
||||
/// <param name="piece">The control point piece that we want to change the path type of.</param>
|
||||
/// <param name="type">The path type we want to assign to the given control point piece.</param>
|
||||
private void updatePathType(PathControlPointPiece piece, PathType? type)
|
||||
private void updatePathType(PathControlPointPiece<T> piece, PathType? type)
|
||||
{
|
||||
int indexInSegment = piece.PointsInSegment.IndexOf(piece.ControlPoint);
|
||||
|
||||
@ -252,7 +253,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
break;
|
||||
}
|
||||
|
||||
slider.Path.ExpectedDistance.Value = null;
|
||||
hitObject.Path.ExpectedDistance.Value = null;
|
||||
piece.ControlPoint.Type = type;
|
||||
}
|
||||
|
||||
@ -268,9 +269,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
private void dragStarted(PathControlPoint controlPoint)
|
||||
{
|
||||
dragStartPositions = slider.Path.ControlPoints.Select(point => point.Position).ToArray();
|
||||
dragPathTypes = slider.Path.ControlPoints.Select(point => point.Type).ToArray();
|
||||
draggedControlPointIndex = slider.Path.ControlPoints.IndexOf(controlPoint);
|
||||
dragStartPositions = hitObject.Path.ControlPoints.Select(point => point.Position).ToArray();
|
||||
dragPathTypes = hitObject.Path.ControlPoints.Select(point => point.Type).ToArray();
|
||||
draggedControlPointIndex = hitObject.Path.ControlPoints.IndexOf(controlPoint);
|
||||
selectedControlPoints = new HashSet<PathControlPoint>(Pieces.Where(piece => piece.IsSelected.Value).Select(piece => piece.ControlPoint));
|
||||
|
||||
Debug.Assert(draggedControlPointIndex >= 0);
|
||||
@ -280,25 +281,25 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
private void dragInProgress(DragEvent e)
|
||||
{
|
||||
Vector2[] oldControlPoints = slider.Path.ControlPoints.Select(cp => cp.Position).ToArray();
|
||||
var oldPosition = slider.Position;
|
||||
double oldStartTime = slider.StartTime;
|
||||
Vector2[] oldControlPoints = hitObject.Path.ControlPoints.Select(cp => cp.Position).ToArray();
|
||||
var oldPosition = hitObject.Position;
|
||||
double oldStartTime = hitObject.StartTime;
|
||||
|
||||
if (selectedControlPoints.Contains(slider.Path.ControlPoints[0]))
|
||||
if (selectedControlPoints.Contains(hitObject.Path.ControlPoints[0]))
|
||||
{
|
||||
// Special handling for selections containing head control point - the position of the slider changes which means the snapped position and time have to be taken into account
|
||||
// Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account
|
||||
Vector2 newHeadPosition = Parent.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex]));
|
||||
var result = snapProvider?.FindSnappedPositionAndTime(newHeadPosition);
|
||||
|
||||
Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - slider.Position;
|
||||
Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position;
|
||||
|
||||
slider.Position += movementDelta;
|
||||
slider.StartTime = result?.Time ?? slider.StartTime;
|
||||
hitObject.Position += movementDelta;
|
||||
hitObject.StartTime = result?.Time ?? hitObject.StartTime;
|
||||
|
||||
for (int i = 1; i < slider.Path.ControlPoints.Count; i++)
|
||||
for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++)
|
||||
{
|
||||
var controlPoint = slider.Path.ControlPoints[i];
|
||||
// Since control points are relative to the position of the slider, all points that are _not_ selected
|
||||
var controlPoint = hitObject.Path.ControlPoints[i];
|
||||
// Since control points are relative to the position of the hit object, all points that are _not_ selected
|
||||
// need to be offset _back_ by the delta corresponding to the movement of the head point.
|
||||
// All other selected control points (if any) will move together with the head point
|
||||
// (and so they will not move at all, relative to each other).
|
||||
@ -310,7 +311,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
var result = snapProvider?.FindSnappedPositionAndTime(Parent.ToScreenSpace(e.MousePosition));
|
||||
|
||||
Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? Parent.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - slider.Position;
|
||||
Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? Parent.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position;
|
||||
|
||||
for (int i = 0; i < controlPoints.Count; ++i)
|
||||
{
|
||||
@ -321,23 +322,23 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
}
|
||||
|
||||
// Snap the path to the current beat divisor before checking length validity.
|
||||
slider.SnapTo(snapProvider);
|
||||
hitObject.SnapTo(snapProvider);
|
||||
|
||||
if (!slider.Path.HasValidLength)
|
||||
if (!hitObject.Path.HasValidLength)
|
||||
{
|
||||
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||
slider.Path.ControlPoints[i].Position = oldControlPoints[i];
|
||||
for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++)
|
||||
hitObject.Path.ControlPoints[i].Position = oldControlPoints[i];
|
||||
|
||||
slider.Position = oldPosition;
|
||||
slider.StartTime = oldStartTime;
|
||||
hitObject.Position = oldPosition;
|
||||
hitObject.StartTime = oldStartTime;
|
||||
// Snap the path length again to undo the invalid length.
|
||||
slider.SnapTo(snapProvider);
|
||||
hitObject.SnapTo(snapProvider);
|
||||
return;
|
||||
}
|
||||
|
||||
// Maintain the path types in case they got defaulted to bezier at some point during the drag.
|
||||
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||
slider.Path.ControlPoints[i].Type = dragPathTypes[i];
|
||||
for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++)
|
||||
hitObject.Path.ControlPoints[i].Type = dragPathTypes[i];
|
||||
}
|
||||
|
||||
private void dragEnded() => changeHandler?.EndChange();
|
||||
|
@ -22,6 +22,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
/// </summary>
|
||||
public Vector2 PathStartLocation => body.PathOffset;
|
||||
|
||||
/// <summary>
|
||||
/// Offset in absolute (local) coordinates from the end of the curve.
|
||||
/// </summary>
|
||||
public Vector2 PathEndLocation => body.PathEndOffset;
|
||||
|
||||
public SliderBodyPiece()
|
||||
{
|
||||
InternalChild = body = new ManualSliderBody
|
||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
private SliderBodyPiece bodyPiece;
|
||||
private HitCirclePiece headCirclePiece;
|
||||
private HitCirclePiece tailCirclePiece;
|
||||
private PathControlPointVisualiser controlPointVisualiser;
|
||||
private PathControlPointVisualiser<Slider> controlPointVisualiser;
|
||||
|
||||
private InputManager inputManager;
|
||||
|
||||
@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
bodyPiece = new SliderBodyPiece(),
|
||||
headCirclePiece = new HitCirclePiece(),
|
||||
tailCirclePiece = new HitCirclePiece(),
|
||||
controlPointVisualiser = new PathControlPointVisualiser(HitObject, false)
|
||||
controlPointVisualiser = new PathControlPointVisualiser<Slider>(HitObject, false)
|
||||
};
|
||||
|
||||
setState(SliderPlacementState.Initial);
|
||||
|
@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
protected SliderCircleOverlay TailOverlay { get; private set; }
|
||||
|
||||
[CanBeNull]
|
||||
protected PathControlPointVisualiser ControlPointVisualiser { get; private set; }
|
||||
protected PathControlPointVisualiser<Slider> ControlPointVisualiser { get; private set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IDistanceSnapProvider snapProvider { get; set; }
|
||||
@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
if (ControlPointVisualiser == null)
|
||||
{
|
||||
AddInternal(ControlPointVisualiser = new PathControlPointVisualiser(HitObject, true)
|
||||
AddInternal(ControlPointVisualiser = new PathControlPointVisualiser<Slider>(HitObject, true)
|
||||
{
|
||||
RemoveControlPointsRequested = removeControlPoints,
|
||||
SplitControlPointsRequested = splitControlPoints
|
||||
@ -409,6 +409,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathOffset)
|
||||
?? BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation);
|
||||
|
||||
protected override Vector2[] ScreenSpaceAdditionalNodes => new[]
|
||||
{
|
||||
DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathEndOffset) ?? BodyPiece.ToScreenSpace(BodyPiece.PathEndLocation)
|
||||
};
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
|
||||
BodyPiece.ReceivePositionalInputAt(screenSpacePos) || ControlPointVisualiser?.Pieces.Any(p => p.ReceivePositionalInputAt(screenSpacePos)) == true;
|
||||
|
||||
|
14
osu.Game.Rulesets.Osu/Mods/OsuModAccuracyChallenge.cs
Normal file
14
osu.Game.Rulesets.Osu/Mods/OsuModAccuracyChallenge.cs
Normal file
@ -0,0 +1,14 @@
|
||||
// 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.Linq;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModAccuracyChallenge : ModAccuracyChallenge
|
||||
{
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
|
||||
}
|
||||
}
|
@ -9,8 +9,10 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu
|
||||
{
|
||||
@ -40,6 +42,13 @@ namespace osu.Game.Rulesets.Osu
|
||||
protected override KeyBindingContainer<OsuAction> CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
|
||||
=> new OsuKeyBindingContainer(ruleset, variant, unique);
|
||||
|
||||
public bool CheckScreenSpaceActionPressJudgeable(Vector2 screenSpacePosition) =>
|
||||
// This is a very naive but simple approach.
|
||||
//
|
||||
// Based on user feedback of more nuanced scenarios (where touch doesn't behave as expected),
|
||||
// this can be expanded to a more complex implementation, but I'd still want to keep it as simple as we can.
|
||||
NonPositionalInputQueue.OfType<DrawableHitCircle.HitReceptor>().Any(c => c.ReceivePositionalInputAt(screenSpacePosition));
|
||||
|
||||
public OsuInputManager(RulesetInfo ruleset)
|
||||
: base(ruleset, 0, SimultaneousBindingMode.Unique)
|
||||
{
|
||||
|
@ -164,7 +164,8 @@ namespace osu.Game.Rulesets.Osu
|
||||
new MultiMod(new OsuModDoubleTime(), new OsuModNightcore()),
|
||||
new OsuModHidden(),
|
||||
new MultiMod(new OsuModFlashlight(), new OsuModBlinds()),
|
||||
new OsuModStrictTracking()
|
||||
new OsuModStrictTracking(),
|
||||
new OsuModAccuracyChallenge(),
|
||||
};
|
||||
|
||||
case ModType.Conversion:
|
||||
|
@ -10,6 +10,7 @@ using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
@ -43,6 +44,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
|
||||
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
|
||||
private readonly FlashPiece flash;
|
||||
private readonly Container kiaiContainer;
|
||||
|
||||
private Bindable<bool> configHitLighting = null!;
|
||||
|
||||
[Resolved]
|
||||
private DrawableHitObject drawableObject { get; set; } = null!;
|
||||
@ -64,24 +68,32 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
outerGradient = new Circle // renders the outer bright gradient
|
||||
{
|
||||
Size = new Vector2(OUTER_GRADIENT_SIZE),
|
||||
Alpha = 1,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
innerGradient = new Circle // renders the inner bright gradient
|
||||
{
|
||||
Size = new Vector2(INNER_GRADIENT_SIZE),
|
||||
Alpha = 1,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
innerFill = new Circle // renders the inner dark fill
|
||||
{
|
||||
Size = new Vector2(INNER_FILL_SIZE),
|
||||
Alpha = 1,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
kiaiContainer = new CircularContainer
|
||||
{
|
||||
Masking = true,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = Size,
|
||||
Child = new KiaiFlash
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
},
|
||||
number = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.Default.With(size: 52, weight: FontWeight.Bold),
|
||||
@ -96,12 +108,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
var drawableOsuObject = (DrawableOsuHitObject)drawableObject;
|
||||
|
||||
accentColour.BindTo(drawableObject.AccentColour);
|
||||
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
|
||||
|
||||
configHitLighting = config.GetBindable<bool>(OsuSetting.HitLighting);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -117,20 +131,25 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
outerGradient.ClearTransforms(targetMember: nameof(Colour));
|
||||
outerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.1f));
|
||||
|
||||
kiaiContainer.Colour = colour.NewValue;
|
||||
outerFill.Colour = innerFill.Colour = colour.NewValue.Darken(4);
|
||||
innerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue.Darken(0.5f), colour.NewValue.Darken(0.6f));
|
||||
flash.Colour = colour.NewValue;
|
||||
|
||||
// Accent colour may be changed many times during a paused gameplay state.
|
||||
// Schedule the change to avoid transforms piling up.
|
||||
Scheduler.AddOnce(updateStateTransforms);
|
||||
Scheduler.AddOnce(() =>
|
||||
{
|
||||
ApplyTransformsAt(double.MinValue, true);
|
||||
ClearTransformsAfter(double.MinValue, true);
|
||||
|
||||
updateStateTransforms(drawableObject, drawableObject.State.Value);
|
||||
});
|
||||
}, true);
|
||||
|
||||
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
|
||||
}
|
||||
|
||||
private void updateStateTransforms() => updateStateTransforms(drawableObject, drawableObject.State.Value);
|
||||
|
||||
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
|
||||
{
|
||||
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
|
||||
@ -140,7 +159,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
case ArmedState.Hit:
|
||||
// Fade out time is at a maximum of 800. Must match `DrawableHitCircle`'s arbitrary lifetime spec.
|
||||
const double fade_out_time = 800;
|
||||
|
||||
const double flash_in_duration = 150;
|
||||
const double resize_duration = 400;
|
||||
|
||||
@ -171,20 +189,40 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
// gradient layers.
|
||||
border.ResizeTo(Size * shrink_size + new Vector2(border.BorderThickness), resize_duration, Easing.OutElasticHalf);
|
||||
|
||||
// Kiai flash should track the overall size but also be cleaned up quite fast, so we don't get additional
|
||||
// flashes after the hit animation is already in a mostly-completed state.
|
||||
kiaiContainer.ResizeTo(Size * shrink_size, resize_duration, Easing.OutElasticHalf);
|
||||
kiaiContainer.FadeOut(flash_in_duration, Easing.OutQuint);
|
||||
|
||||
// The outer gradient is resize with a slight delay from the border.
|
||||
// This is to give it a bomb-like effect, with the border "triggering" its animation when getting close.
|
||||
using (BeginDelayedSequence(flash_in_duration / 12))
|
||||
{
|
||||
outerGradient.ResizeTo(OUTER_GRADIENT_SIZE * shrink_size, resize_duration, Easing.OutElasticHalf);
|
||||
|
||||
outerGradient
|
||||
.FadeColour(Color4.White, 80)
|
||||
.Then()
|
||||
.FadeOut(flash_in_duration);
|
||||
}
|
||||
|
||||
flash.FadeTo(1, flash_in_duration, Easing.OutQuint);
|
||||
if (configHitLighting.Value)
|
||||
{
|
||||
flash.HitLighting = true;
|
||||
flash.FadeTo(1, flash_in_duration, Easing.OutQuint);
|
||||
|
||||
this.FadeOut(fade_out_time, Easing.OutQuad);
|
||||
}
|
||||
else
|
||||
{
|
||||
flash.HitLighting = false;
|
||||
flash.FadeTo(1, flash_in_duration, Easing.OutQuint)
|
||||
.Then()
|
||||
.FadeOut(flash_in_duration, Easing.OutQuint);
|
||||
|
||||
this.FadeOut(fade_out_time * 0.8f, Easing.OutQuad);
|
||||
}
|
||||
|
||||
this.FadeOut(fade_out_time, Easing.OutQuad);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -215,6 +253,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
Child.AlwaysPresent = true;
|
||||
}
|
||||
|
||||
public bool HitLighting { get; set; }
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
@ -223,7 +263,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Colour = Colour,
|
||||
Radius = OsuHitObject.OBJECT_RADIUS * 1.2f,
|
||||
Radius = OsuHitObject.OBJECT_RADIUS * (HitLighting ? 1.2f : 0.6f),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
/// </summary>
|
||||
public virtual Vector2 PathOffset => path.PositionInBoundingBox(path.Vertices[0]);
|
||||
|
||||
/// <summary>
|
||||
/// Offset in absolute coordinates from the end of the curve.
|
||||
/// </summary>
|
||||
public virtual Vector2 PathEndOffset => path.PositionInBoundingBox(path.Vertices[^1]);
|
||||
|
||||
/// <summary>
|
||||
/// Used to colour the path.
|
||||
/// </summary>
|
||||
|
@ -43,6 +43,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
|
||||
public override Vector2 PathOffset => snakedPathOffset;
|
||||
|
||||
public override Vector2 PathEndOffset => snakedPathEndOffset;
|
||||
|
||||
/// <summary>
|
||||
/// The top-left position of the path when fully snaked.
|
||||
/// </summary>
|
||||
@ -53,6 +55,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
/// </summary>
|
||||
private Vector2 snakedPathOffset;
|
||||
|
||||
/// <summary>
|
||||
/// The offset of the end of path from <see cref="snakedPosition"/> when fully snaked.
|
||||
/// </summary>
|
||||
private Vector2 snakedPathEndOffset;
|
||||
|
||||
private DrawableSlider drawableSlider = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -109,6 +116,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
|
||||
snakedPosition = Path.PositionInBoundingBox(Vector2.Zero);
|
||||
snakedPathOffset = Path.PositionInBoundingBox(Path.Vertices[0]);
|
||||
snakedPathEndOffset = Path.PositionInBoundingBox(Path.Vertices[^1]);
|
||||
|
||||
double lastSnakedStart = SnakedStart ?? 0;
|
||||
double lastSnakedEnd = SnakedEnd ?? 0;
|
||||
|
@ -10,6 +10,7 @@ using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Input.StateChanges;
|
||||
using osu.Game.Configuration;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI
|
||||
{
|
||||
@ -21,6 +22,8 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
/// </summary>
|
||||
private readonly List<TrackedTouch> trackedTouches = new List<TrackedTouch>();
|
||||
|
||||
private TrackedTouch? positionTrackingTouch;
|
||||
|
||||
private readonly OsuInputManager osuInputManager;
|
||||
|
||||
private Bindable<bool> mouseDisabled = null!;
|
||||
@ -38,6 +41,9 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
mouseDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableButtons);
|
||||
}
|
||||
|
||||
// Required to handle touches outside of the playfield when screen scaling is enabled.
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||
|
||||
protected override void OnTouchMove(TouchMoveEvent e)
|
||||
{
|
||||
base.OnTouchMove(e);
|
||||
@ -53,7 +59,15 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
// Ignore any taps which trigger an action which is already handled. But track them for potential positional input in the future.
|
||||
bool shouldResultInAction = osuInputManager.AllowGameplayInputs && !mouseDisabled.Value && trackedTouches.All(t => t.Action != action);
|
||||
|
||||
trackedTouches.Add(new TrackedTouch(e.Touch.Source, shouldResultInAction ? action : null));
|
||||
// If we can actually accept as an action, check whether this tap was on a circle's receptor.
|
||||
// This case gets special handling to allow for empty-space stream tapping.
|
||||
bool isDirectCircleTouch = osuInputManager.CheckScreenSpaceActionPressJudgeable(e.ScreenSpaceTouchDownPosition);
|
||||
|
||||
var newTouch = new TrackedTouch(e.Touch.Source, shouldResultInAction ? action : null, isDirectCircleTouch);
|
||||
|
||||
updatePositionTracking(newTouch);
|
||||
|
||||
trackedTouches.Add(newTouch);
|
||||
|
||||
// Important to update position before triggering the pressed action.
|
||||
handleTouchMovement(e);
|
||||
@ -64,10 +78,47 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a new touch, update the positional tracking state and any related operations.
|
||||
/// </summary>
|
||||
private void updatePositionTracking(TrackedTouch newTouch)
|
||||
{
|
||||
// If the new touch directly interacted with a circle's receptor, it always becomes the current touch for positional tracking.
|
||||
if (newTouch.DirectTouch)
|
||||
{
|
||||
positionTrackingTouch = newTouch;
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, we only want to use the new touch for position tracking if no other touch is tracking position yet..
|
||||
if (positionTrackingTouch == null)
|
||||
{
|
||||
positionTrackingTouch = newTouch;
|
||||
return;
|
||||
}
|
||||
|
||||
// ..or if the current position tracking touch was not a direct touch (this one is debatable and may be change in the future, but it's the simplest way to handle)
|
||||
if (!positionTrackingTouch.DirectTouch)
|
||||
{
|
||||
positionTrackingTouch = newTouch;
|
||||
return;
|
||||
}
|
||||
|
||||
// In the case the new touch was not used for position tracking, we should also check the previous position tracking touch.
|
||||
// If it was a direct touch and still has its action pressed, that action should be released.
|
||||
//
|
||||
// This is done to allow tracking with the initial touch while still having both Left/Right actions available for alternating with two more touches.
|
||||
if (positionTrackingTouch.DirectTouch && positionTrackingTouch.Action is OsuAction directTouchAction)
|
||||
{
|
||||
osuInputManager.KeyBindingContainer.TriggerReleased(directTouchAction);
|
||||
positionTrackingTouch.Action = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleTouchMovement(TouchEvent touchEvent)
|
||||
{
|
||||
// Movement should only be tracked for the most recent touch.
|
||||
if (touchEvent.Touch.Source != trackedTouches.Last().Source)
|
||||
if (touchEvent.Touch.Source != positionTrackingTouch?.Source)
|
||||
return;
|
||||
|
||||
if (!osuInputManager.AllowUserCursorMovement)
|
||||
@ -83,6 +134,9 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
if (tracked.Action is OsuAction action)
|
||||
osuInputManager.KeyBindingContainer.TriggerReleased(action);
|
||||
|
||||
if (positionTrackingTouch == tracked)
|
||||
positionTrackingTouch = null;
|
||||
|
||||
trackedTouches.Remove(tracked);
|
||||
|
||||
base.OnTouchUp(e);
|
||||
@ -92,12 +146,15 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
{
|
||||
public readonly TouchSource Source;
|
||||
|
||||
public readonly OsuAction? Action;
|
||||
public OsuAction? Action;
|
||||
|
||||
public TrackedTouch(TouchSource source, OsuAction? action)
|
||||
public readonly bool DirectTouch;
|
||||
|
||||
public TrackedTouch(TouchSource source, OsuAction? action, bool directTouch)
|
||||
{
|
||||
Source = source;
|
||||
Action = action;
|
||||
DirectTouch = directTouch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
// 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.Diagnostics;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.UI;
|
||||
@ -9,30 +8,15 @@ using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset<TaikoHitObject>, IUpdatableByPlayfield
|
||||
public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset<TaikoHitObject>
|
||||
{
|
||||
private DrawableTaikoRuleset? drawableTaikoRuleset;
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
|
||||
{
|
||||
drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
|
||||
var drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
|
||||
drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false;
|
||||
|
||||
var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
|
||||
playfield.ClassicHitTargetPosition.Value = true;
|
||||
}
|
||||
|
||||
public void Update(Playfield playfield)
|
||||
{
|
||||
Debug.Assert(drawableTaikoRuleset != null);
|
||||
|
||||
// Classic taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened.
|
||||
const float scroll_rate = 10;
|
||||
|
||||
// Since the time range will depend on a positional value, it is referenced to the x480 pixel space.
|
||||
float ratio = drawableTaikoRuleset.DrawHeight / 480;
|
||||
|
||||
drawableTaikoRuleset.TimeRange.Value = (playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -144,6 +144,7 @@ namespace osu.Game.Rulesets.Taiko
|
||||
new MultiMod(new TaikoModDoubleTime(), new TaikoModNightcore()),
|
||||
new TaikoModHidden(),
|
||||
new TaikoModFlashlight(),
|
||||
new ModAccuracyChallenge(),
|
||||
};
|
||||
|
||||
case ModType.Conversion:
|
||||
|
@ -43,7 +43,6 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
: base(ruleset, beatmap, mods)
|
||||
{
|
||||
Direction.Value = ScrollingDirection.Left;
|
||||
TimeRange.Value = 7000;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -60,6 +59,19 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
KeyBindingInputManager.Add(new DrumTouchInputArea());
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// Taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened.
|
||||
const float scroll_rate = 10;
|
||||
|
||||
// Since the time range will depend on a positional value, it is referenced to the x480 pixel space.
|
||||
float ratio = DrawHeight / 480;
|
||||
|
||||
TimeRange.Value = (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
Binary file not shown.
BIN
osu.Game.Tests/Resources/Archives/conflicting-filenames-skin.osk
Normal file
BIN
osu.Game.Tests/Resources/Archives/conflicting-filenames-skin.osk
Normal file
Binary file not shown.
@ -150,6 +150,8 @@ namespace osu.Game.Tests.Rulesets
|
||||
public IBindable<double> AggregateTempo => throw new NotImplementedException();
|
||||
|
||||
public int PlaybackConcurrency { get; set; }
|
||||
|
||||
public void AddExtension(string extension) => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private class TestShaderManager : ShaderManager
|
||||
|
@ -1,12 +1,11 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -20,29 +19,36 @@ namespace osu.Game.Tests.Skins
|
||||
public partial class TestSceneBeatmapSkinResources : OsuTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; }
|
||||
private BeatmapManager beatmaps { get; set; } = null!;
|
||||
|
||||
private IWorkingBeatmap beatmap;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
[Test]
|
||||
public void TestRetrieveOggAudio()
|
||||
{
|
||||
var imported = beatmaps.Import(new ImportTask(TestResources.OpenResource("Archives/ogg-beatmap.osz"), "ogg-beatmap.osz")).GetResultSafely();
|
||||
IWorkingBeatmap beatmap = null!;
|
||||
|
||||
imported?.PerformRead(s =>
|
||||
AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"ogg-beatmap.osz"));
|
||||
AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"sample")) != null);
|
||||
AddAssert("track is non-null", () =>
|
||||
{
|
||||
beatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps[0]);
|
||||
using (var track = beatmap.LoadTrack())
|
||||
return track is not TrackVirtual;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRetrieveOggSample() => AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo("sample")) != null);
|
||||
|
||||
[Test]
|
||||
public void TestRetrieveOggTrack() => AddAssert("track is non-null", () =>
|
||||
public void TestRetrievalWithConflictingFilenames()
|
||||
{
|
||||
using (var track = beatmap.LoadTrack())
|
||||
return track is not TrackVirtual;
|
||||
});
|
||||
IWorkingBeatmap beatmap = null!;
|
||||
|
||||
AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"conflicting-filenames-beatmap.osz"));
|
||||
AddAssert("texture is non-null", () => beatmap.Skin.GetTexture(@"spinner-osu") != null);
|
||||
AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo(@"spinner-osu")) != null);
|
||||
}
|
||||
|
||||
private IWorkingBeatmap importBeatmapFromArchives(string filename)
|
||||
{
|
||||
var imported = beatmaps.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely();
|
||||
return imported.AsNonNull().PerformRead(s => beatmaps.GetWorkingBeatmap(s.Beatmaps[0]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,17 +31,24 @@ namespace osu.Game.Tests.Skins
|
||||
[Resolved]
|
||||
private SkinManager skins { get; set; } = null!;
|
||||
|
||||
private ISkin skin = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
[Test]
|
||||
public void TestRetrieveOggSample()
|
||||
{
|
||||
var imported = skins.Import(new ImportTask(TestResources.OpenResource("Archives/ogg-skin.osk"), "ogg-skin.osk")).GetResultSafely();
|
||||
skin = imported.PerformRead(skinInfo => skins.GetSkin(skinInfo));
|
||||
ISkin skin = null!;
|
||||
|
||||
AddStep("import skin", () => skin = importSkinFromArchives(@"ogg-skin.osk"));
|
||||
AddAssert("sample is non-null", () => skin.GetSample(new SampleInfo(@"sample")) != null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRetrieveOggSample() => AddAssert("sample is non-null", () => skin.GetSample(new SampleInfo("sample")) != null);
|
||||
public void TestRetrievalWithConflictingFilenames()
|
||||
{
|
||||
ISkin skin = null!;
|
||||
|
||||
AddStep("import skin", () => skin = importSkinFromArchives(@"conflicting-filenames-skin.osk"));
|
||||
AddAssert("texture is non-null", () => skin.GetTexture(@"spinner-osu") != null);
|
||||
AddAssert("sample is non-null", () => skin.GetSample(new SampleInfo(@"spinner-osu")) != null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSampleRetrievalOrder()
|
||||
@ -78,6 +85,12 @@ namespace osu.Game.Tests.Skins
|
||||
});
|
||||
}
|
||||
|
||||
private Skin importSkinFromArchives(string filename)
|
||||
{
|
||||
var imported = skins.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely();
|
||||
return imported.PerformRead(skinInfo => skins.GetSkin(skinInfo));
|
||||
}
|
||||
|
||||
private class TestSkin : Skin
|
||||
{
|
||||
public const string SAMPLE_NAME = "test-sample";
|
||||
|
@ -5,6 +5,7 @@ using osu.Game.Graphics.Backgrounds;
|
||||
using osu.Framework.Graphics;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Background
|
||||
{
|
||||
@ -25,7 +26,10 @@ namespace osu.Game.Tests.Visual.Background
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ColourLight = Color4.White,
|
||||
ColourDark = Color4.Gray
|
||||
ColourDark = Color4.Gray,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(0.9f)
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -35,6 +39,8 @@ namespace osu.Game.Tests.Visual.Background
|
||||
base.LoadComplete();
|
||||
|
||||
AddSliderStep("Triangle scale", 0f, 10f, 1f, s => triangles.TriangleScale = s);
|
||||
AddSliderStep("Seed", 0, 1000, 0, s => triangles.Reset(s));
|
||||
AddToggleStep("Masking", m => triangles.Masking = m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
AddStep("move mouse to common point", () =>
|
||||
{
|
||||
var pos = blueprintContainer.ChildrenOfType<PathControlPointPiece>().ElementAt(1).ScreenSpaceDrawQuad.Centre;
|
||||
var pos = blueprintContainer.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1).ScreenSpaceDrawQuad.Centre;
|
||||
InputManager.MoveMouseTo(pos);
|
||||
});
|
||||
AddStep("right click", () => InputManager.Click(MouseButton.Right));
|
||||
|
@ -286,7 +286,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
AddStep("move mouse to controlpoint", () =>
|
||||
{
|
||||
var pos = blueprintContainer.ChildrenOfType<PathControlPointPiece>().ElementAt(1).ScreenSpaceDrawQuad.Centre;
|
||||
var pos = blueprintContainer.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1).ScreenSpaceDrawQuad.Centre;
|
||||
InputManager.MoveMouseTo(pos);
|
||||
});
|
||||
AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
|
||||
|
@ -13,6 +13,7 @@ using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Rulesets;
|
||||
@ -42,6 +43,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[Resolved]
|
||||
private BeatmapManager beatmapManager { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
private Guid currentBeatmapSetID => EditorBeatmap.BeatmapInfo.BeatmapSet?.ID ?? Guid.Empty;
|
||||
|
||||
public override void SetUpSteps()
|
||||
@ -224,7 +228,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
return beatmap != null
|
||||
&& beatmap.DifficultyName == secondDifficultyName
|
||||
&& set != null
|
||||
&& set.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName) && s.Beatmaps.All(b => s.Status == BeatmapOnlineStatus.LocallyModified));
|
||||
&& set.PerformRead(s =>
|
||||
s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName) && s.Beatmaps.All(b => s.Status == BeatmapOnlineStatus.LocallyModified));
|
||||
});
|
||||
}
|
||||
|
||||
@ -327,6 +332,56 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddAssert("old beatmap file not deleted", () => refetchedBeatmapSet.AsNonNull().PerformRead(s => s.Files.Count == 2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCopyDifficultyDoesNotChangeCollections()
|
||||
{
|
||||
string originalDifficultyName = Guid.NewGuid().ToString();
|
||||
|
||||
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = originalDifficultyName);
|
||||
AddStep("save beatmap", () => Editor.Save());
|
||||
|
||||
string originalMd5 = string.Empty;
|
||||
BeatmapCollection collection = null!;
|
||||
|
||||
AddStep("setup a collection with original beatmap", () =>
|
||||
{
|
||||
collection = new BeatmapCollection("test copy");
|
||||
collection.BeatmapMD5Hashes.Add(originalMd5 = EditorBeatmap.BeatmapInfo.MD5Hash);
|
||||
|
||||
realm.Write(r =>
|
||||
{
|
||||
r.Add(collection);
|
||||
});
|
||||
});
|
||||
|
||||
AddAssert("collection contains original beatmap", () =>
|
||||
!string.IsNullOrEmpty(originalMd5) && collection.BeatmapMD5Hashes.Contains(originalMd5));
|
||||
|
||||
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo));
|
||||
|
||||
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog);
|
||||
AddStep("confirm creation as a copy", () => DialogOverlay.CurrentDialog.Buttons.ElementAt(1).TriggerClick());
|
||||
|
||||
AddUntilStep("wait for created", () =>
|
||||
{
|
||||
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
|
||||
return difficultyName != null && difficultyName != originalDifficultyName;
|
||||
});
|
||||
|
||||
AddStep("save without changes", () => Editor.Save());
|
||||
|
||||
AddAssert("collection still points to old beatmap", () => !collection.BeatmapMD5Hashes.Contains(EditorBeatmap.BeatmapInfo.MD5Hash)
|
||||
&& collection.BeatmapMD5Hashes.Contains(originalMd5));
|
||||
|
||||
AddStep("clean up collection", () =>
|
||||
{
|
||||
realm.Write(r =>
|
||||
{
|
||||
r.Remove(collection);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCreateMultipleNewDifficultiesSucceeds()
|
||||
{
|
||||
|
@ -16,6 +16,7 @@ using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
||||
@ -85,6 +86,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
|
||||
|
||||
AddUntilStep("wait for join", () => MultiplayerClient.RoomJoined);
|
||||
AddUntilStep("wait for ongoing operation to complete", () => !(CurrentScreen as OnlinePlayScreen).ChildrenOfType<OngoingOperationTracker>().Single().InProgress.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -15,7 +15,6 @@ using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
||||
using osu.Game.Screens.Play;
|
||||
@ -44,14 +43,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest]
|
||||
/*
|
||||
* TearDown : System.TimeoutException : "wait for ongoing operation to complete" timed out
|
||||
* --TearDown
|
||||
* at osu.Framework.Testing.Drawables.Steps.UntilStepButton.<>c__DisplayClass11_0.<.ctor>b__0()
|
||||
* at osu.Framework.Testing.Drawables.Steps.StepButton.PerformStep(Boolean userTriggered)
|
||||
* at osu.Framework.Testing.TestScene.runNextStep(Action onCompletion, Action`1 onError, Func`2 stopCondition)
|
||||
*/
|
||||
public void TestItemAddedToTheEndOfQueue()
|
||||
{
|
||||
addItem(() => OtherBeatmap);
|
||||
@ -64,7 +55,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest] // See above
|
||||
public void TestNextItemSelectedAfterGameplayFinish()
|
||||
{
|
||||
addItem(() => OtherBeatmap);
|
||||
@ -82,7 +72,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest] // See above
|
||||
public void TestItemsNotClearedWhenSwitchToHostOnlyMode()
|
||||
{
|
||||
addItem(() => OtherBeatmap);
|
||||
@ -98,7 +87,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest] // See above
|
||||
public void TestCorrectItemSelectedAfterNewItemAdded()
|
||||
{
|
||||
addItem(() => OtherBeatmap);
|
||||
@ -106,7 +94,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest] // See above
|
||||
public void TestCorrectRulesetSelectedAfterNewItemAdded()
|
||||
{
|
||||
addItem(() => OtherBeatmap, new CatchRuleset().RulesetInfo);
|
||||
@ -124,7 +111,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest] // See above
|
||||
public void TestCorrectModsSelectedAfterNewItemAdded()
|
||||
{
|
||||
addItem(() => OtherBeatmap, mods: new Mod[] { new OsuModDoubleTime() });
|
||||
@ -153,7 +139,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddUntilStep("wait for song select", () => (songSelect = CurrentSubScreen as Screens.Select.SongSelect) != null);
|
||||
AddUntilStep("wait for loaded", () => songSelect.AsNonNull().BeatmapSetsLoaded);
|
||||
AddUntilStep("wait for ongoing operation to complete", () => !(CurrentScreen as OnlinePlayScreen).ChildrenOfType<OngoingOperationTracker>().Single().InProgress.Value);
|
||||
|
||||
if (ruleset != null)
|
||||
AddStep($"set {ruleset.Name} ruleset", () => songSelect.AsNonNull().Ruleset.Value = ruleset);
|
||||
|
@ -50,14 +50,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest]
|
||||
/*
|
||||
* TearDown : System.TimeoutException : "wait for ongoing operation to complete" timed out
|
||||
* --TearDown
|
||||
* at osu.Framework.Testing.Drawables.Steps.UntilStepButton.<>c__DisplayClass11_0.<.ctor>b__0()
|
||||
* at osu.Framework.Testing.Drawables.Steps.StepButton.PerformStep(Boolean userTriggered)
|
||||
* at osu.Framework.Testing.TestScene.runNextStep(Action onCompletion, Action`1 onError, Func`2 stopCondition)
|
||||
*/
|
||||
public void TestItemStillSelectedAfterChangeToSameBeatmap()
|
||||
{
|
||||
selectNewItem(() => InitialBeatmap);
|
||||
@ -66,7 +58,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest] // See above
|
||||
public void TestItemStillSelectedAfterChangeToOtherBeatmap()
|
||||
{
|
||||
selectNewItem(() => OtherBeatmap);
|
||||
@ -75,7 +66,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest] // See above
|
||||
public void TestOnlyLastItemChangedAfterGameplayFinished()
|
||||
{
|
||||
RunGameplay();
|
||||
@ -90,7 +80,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest] // See above
|
||||
public void TestAddItemsAsHost()
|
||||
{
|
||||
addItem(() => OtherBeatmap);
|
||||
@ -115,7 +104,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded);
|
||||
|
||||
BeatmapInfo otherBeatmap = null;
|
||||
AddUntilStep("wait for ongoing operation to complete", () => !(CurrentScreen as OnlinePlayScreen).ChildrenOfType<OngoingOperationTracker>().Single().InProgress.Value);
|
||||
AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(otherBeatmap = beatmap()));
|
||||
|
||||
AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen);
|
||||
@ -131,7 +119,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
});
|
||||
|
||||
AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded);
|
||||
AddUntilStep("wait for ongoing operation to complete", () => !(CurrentScreen as OnlinePlayScreen).ChildrenOfType<OngoingOperationTracker>().Single().InProgress.Value);
|
||||
AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(beatmap()));
|
||||
AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen);
|
||||
}
|
||||
|
@ -262,7 +262,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddAssert("Nothing happened", () => this.ChildrenOfType<ReportCommentPopover>().Any());
|
||||
AddStep("Set report data", () =>
|
||||
{
|
||||
var field = this.ChildrenOfType<OsuTextBox>().Single();
|
||||
var field = this.ChildrenOfType<ReportCommentPopover>().Single().ChildrenOfType<OsuTextBox>().Single();
|
||||
field.Current.Value = report_text;
|
||||
var reason = this.ChildrenOfType<OsuEnumDropdown<CommentReportReason>>().Single();
|
||||
reason.Current.Value = CommentReportReason.Other;
|
||||
|
54
osu.Game.Tests/Visual/Online/TestSceneLevelBadge.cs
Normal file
54
osu.Game.Tests/Visual/Online/TestSceneLevelBadge.cs
Normal file
@ -0,0 +1,54 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Overlays.Profile.Header.Components;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneLevelBadge : OsuTestScene
|
||||
{
|
||||
public TestSceneLevelBadge()
|
||||
{
|
||||
var levels = new List<UserStatistics.LevelInfo>();
|
||||
|
||||
for (int i = 0; i < 11; i++)
|
||||
{
|
||||
levels.Add(new UserStatistics.LevelInfo
|
||||
{
|
||||
Current = i * 10
|
||||
});
|
||||
}
|
||||
|
||||
levels.Add(new UserStatistics.LevelInfo { Current = 101 });
|
||||
levels.Add(new UserStatistics.LevelInfo { Current = 105 });
|
||||
levels.Add(new UserStatistics.LevelInfo { Current = 110 });
|
||||
levels.Add(new UserStatistics.LevelInfo { Current = 115 });
|
||||
levels.Add(new UserStatistics.LevelInfo { Current = 120 });
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer<LevelBadge>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Full,
|
||||
Spacing = new Vector2(5),
|
||||
ChildrenEnumerable = levels.Select(level => new LevelBadge
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(60),
|
||||
LevelInfo = { Value = level }
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Profile;
|
||||
@ -19,6 +20,9 @@ namespace osu.Game.Tests.Visual.Online
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager configManager { get; set; } = null!;
|
||||
|
||||
private ProfileHeader header = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
@ -33,6 +37,22 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddStep("Show example user", () => header.User.Value = new UserProfileData(TestSceneUserProfileOverlay.TEST_USER, new OsuRuleset().RulesetInfo));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestProfileCoverExpanded()
|
||||
{
|
||||
AddStep("Set cover to expanded", () => configManager.SetValue(OsuSetting.ProfileCoverExpanded, true));
|
||||
AddStep("Show example user", () => header.User.Value = new UserProfileData(TestSceneUserProfileOverlay.TEST_USER, new OsuRuleset().RulesetInfo));
|
||||
AddUntilStep("Cover is expanded", () => header.ChildrenOfType<UserCoverBackground>().Single().Height, () => Is.GreaterThan(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestProfileCoverCollapsed()
|
||||
{
|
||||
AddStep("Set cover to collapsed", () => configManager.SetValue(OsuSetting.ProfileCoverExpanded, false));
|
||||
AddStep("Show example user", () => header.User.Value = new UserProfileData(TestSceneUserProfileOverlay.TEST_USER, new OsuRuleset().RulesetInfo));
|
||||
AddUntilStep("Cover is collapsed", () => header.ChildrenOfType<UserCoverBackground>().Single().Height, () => Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOnlineState()
|
||||
{
|
||||
|
@ -82,13 +82,14 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
Username = @"Somebody",
|
||||
Id = 1,
|
||||
CountryCode = CountryCode.Unknown,
|
||||
CountryCode = CountryCode.JP,
|
||||
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg",
|
||||
JoinDate = DateTimeOffset.Now.AddDays(-1),
|
||||
LastVisit = DateTimeOffset.Now,
|
||||
Groups = new[]
|
||||
{
|
||||
new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
|
||||
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "mania" } },
|
||||
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko" } }
|
||||
},
|
||||
ProfileOrder = new[]
|
||||
@ -142,7 +143,8 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
Available = 10,
|
||||
Total = 50
|
||||
}
|
||||
},
|
||||
SupportLevel = 2,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -123,7 +123,7 @@ needs_cleanup: true
|
||||
AddStep("Add absolute image", () =>
|
||||
{
|
||||
markdownContainer.CurrentPath = "https://dev.ppy.sh";
|
||||
markdownContainer.Text = "![intro](/wiki/Interface/img/intro-screen.jpg)";
|
||||
markdownContainer.Text = "![intro](/wiki/images/Client/Interface/img/intro-screen.jpg)";
|
||||
});
|
||||
}
|
||||
|
||||
@ -133,7 +133,7 @@ needs_cleanup: true
|
||||
AddStep("Add relative image", () =>
|
||||
{
|
||||
markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/";
|
||||
markdownContainer.Text = "![intro](img/intro-screen.jpg)";
|
||||
markdownContainer.Text = "![intro](../images/Client/Interface/img/intro-screen.jpg)";
|
||||
});
|
||||
}
|
||||
|
||||
@ -145,7 +145,7 @@ needs_cleanup: true
|
||||
markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/";
|
||||
markdownContainer.Text = @"Line before image
|
||||
|
||||
![play menu](img/play-menu.jpg ""Main Menu in osu!"")
|
||||
![play menu](../images/Client/Interface/img/play-menu.jpg ""Main Menu in osu!"")
|
||||
|
||||
Line after image";
|
||||
});
|
||||
@ -170,12 +170,12 @@ Line after image";
|
||||
markdownContainer.Text = @"
|
||||
| Image | Name | Effect |
|
||||
| :-: | :-: | :-- |
|
||||
| ![](/wiki/Skinning/Interface/img/hit300.png ""300"") | 300 | A possible score when tapping a hit circle precisely on time, completing a Slider and keeping the cursor over every tick, or completing a Spinner with the Spinner Metre full. A score of 300 appears in an blue score by default. Scoring nothing except 300s in a beatmap will award the player with the SS or SSH grade. |
|
||||
| ![](/wiki/Skinning/Interface/img/hit300g.png ""Geki"") | (激) Geki | A term from Ouendan, called Elite Beat! in EBA. Appears when playing the last element in a combo in which the player has scored only 300s. Getting a Geki will give a sizable boost to the Life Bar. By default, it is blue. |
|
||||
| ![](/wiki/Skinning/Interface/img/hit100.png ""100"") | 100 | A possible score one can get when tapping a Hit Object slightly late or early, completing a Slider and missing a number of ticks, or completing a Spinner with the Spinner Meter almost full. A score of 100 appears in a green score by default. When very skilled players test a beatmap and they get a lot of 100s, this may mean that the beatmap does not have correct timing. |
|
||||
| ![](/wiki/Skinning/Interface/img/hit300k.png ""300 Katu"") ![](/wiki/Skinning/Interface/img/hit100k.png ""100 Katu"") | (喝) Katu or Katsu | A term from Ouendan, called Beat! in EBA. Appears when playing the last element in a combo in which the player has scored at least one 100, but no 50s or misses. Getting a Katu will give a small boost to the Life Bar. By default, it is coloured green or blue depending on whether the Katu itself is a 100 or a 300. |
|
||||
| ![](/wiki/Skinning/Interface/img/hit50.png ""50"") | 50 | A possible score one can get when tapping a hit circle rather early or late but not early or late enough to cause a miss, completing a Slider and missing a lot of ticks, or completing a Spinner with the Spinner Metre close to full. A score of 50 appears in a orange score by default. Scoring a 50 in a combo will prevent the appearance of a Katu or a Geki at the combo's end. |
|
||||
| ![](/wiki/Skinning/Interface/img/hit0.png ""Miss"") | Miss | A possible score one can get when not tapping a hit circle or too early (based on OD and AR, it may *shake* instead), not tapping or holding the Slider at least once, or completing a Spinner with low Spinner Metre fill. Scoring a Miss will reset the current combo to 0 and will prevent the appearance of a Katu or a Geki at the combo's end. |
|
||||
| ![](/wiki/images/shared/judgement/osu!/hit300.png ""300"") | 300 | A possible score when tapping a hit circle precisely on time, completing a Slider and keeping the cursor over every tick, or completing a Spinner with the Spinner Metre full. A score of 300 appears in an blue score by default. Scoring nothing except 300s in a beatmap will award the player with the SS or SSH grade. |
|
||||
| ![](/wiki/images/shared/judgement/osu!/hit300g.png ""Geki"") | (激) Geki | A term from Ouendan, called Elite Beat! in EBA. Appears when playing the last element in a combo in which the player has scored only 300s. Getting a Geki will give a sizable boost to the Life Bar. By default, it is blue. |
|
||||
| ![](/wiki/images/shared/judgement/osu!/hit100.png ""100"") | 100 | A possible score one can get when tapping a Hit Object slightly late or early, completing a Slider and missing a number of ticks, or completing a Spinner with the Spinner Meter almost full. A score of 100 appears in a green score by default. When very skilled players test a beatmap and they get a lot of 100s, this may mean that the beatmap does not have correct timing. |
|
||||
| ![](/wiki/images/shared/judgement/osu!/hit300k.png ""300 Katu"") ![](/wiki/Skinning/Interface/img/hit100k.png ""100 Katu"") | (喝) Katu or Katsu | A term from Ouendan, called Beat! in EBA. Appears when playing the last element in a combo in which the player has scored at least one 100, but no 50s or misses. Getting a Katu will give a small boost to the Life Bar. By default, it is coloured green or blue depending on whether the Katu itself is a 100 or a 300. |
|
||||
| ![](/wiki/images/shared/judgement/osu!/hit50.png ""50"") | 50 | A possible score one can get when tapping a hit circle rather early or late but not early or late enough to cause a miss, completing a Slider and missing a lot of ticks, or completing a Spinner with the Spinner Metre close to full. A score of 50 appears in a orange score by default. Scoring a 50 in a combo will prevent the appearance of a Katu or a Geki at the combo's end. |
|
||||
| ![](/wiki/images/shared/judgement/osu!/hit0.png ""Miss"") | Miss | A possible score one can get when not tapping a hit circle or too early (based on OD and AR, it may *shake* instead), not tapping or holding the Slider at least once, or completing a Spinner with low Spinner Metre fill. Scoring a Miss will reset the current combo to 0 and will prevent the appearance of a Katu or a Geki at the combo's end. |
|
||||
";
|
||||
});
|
||||
}
|
||||
@ -186,7 +186,7 @@ Line after image";
|
||||
AddStep("Add image", () =>
|
||||
{
|
||||
markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/osu!_Program_Files/";
|
||||
markdownContainer.Text = "![](img/file_structure.jpg \"The file structure of osu!'s installation folder, on Windows and macOS\")";
|
||||
markdownContainer.Text = "![](../images/Client/Program_files/img/file_structure.jpg \"The file structure of osu!'s installation folder, on Windows and macOS\")";
|
||||
});
|
||||
|
||||
AddUntilStep("Wait image to load", () => markdownContainer.ChildrenOfType<DelayedLoadWrapper>().First().DelayedLoadCompleted);
|
||||
@ -270,6 +270,30 @@ Phasellus eu nunc nec ligula semper fringilla. Aliquam magna neque, placerat sed
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCodeSyntax()
|
||||
{
|
||||
AddStep("set content", () =>
|
||||
{
|
||||
markdownContainer.Text = @"
|
||||
This is a paragraph containing `inline code` synatax.
|
||||
Oh wow I do love the `WikiMarkdownContainer`, it is very cool!
|
||||
|
||||
This is a line before the fenced code block:
|
||||
```csharp
|
||||
public class WikiMarkdownContainer : MarkdownContainer
|
||||
{
|
||||
public WikiMarkdownContainer()
|
||||
{
|
||||
this.foo = bar;
|
||||
}
|
||||
}
|
||||
```
|
||||
This is a line after the fenced code block!
|
||||
";
|
||||
});
|
||||
}
|
||||
|
||||
private partial class TestMarkdownContainer : WikiMarkdownContainer
|
||||
{
|
||||
public LinkInline Link;
|
||||
|
@ -186,7 +186,7 @@ namespace osu.Game.Beatmaps
|
||||
targetBeatmapSet.Beatmaps.Add(newBeatmap.BeatmapInfo);
|
||||
newBeatmap.BeatmapInfo.BeatmapSet = targetBeatmapSet;
|
||||
|
||||
Save(newBeatmap.BeatmapInfo, newBeatmap, beatmapSkin);
|
||||
save(newBeatmap.BeatmapInfo, newBeatmap, beatmapSkin, transferCollections: false);
|
||||
|
||||
workingBeatmapCache.Invalidate(targetBeatmapSet);
|
||||
return GetWorkingBeatmap(newBeatmap.BeatmapInfo);
|
||||
@ -280,77 +280,16 @@ namespace osu.Game.Beatmaps
|
||||
public IWorkingBeatmap DefaultBeatmap => workingBeatmapCache.DefaultBeatmap;
|
||||
|
||||
/// <summary>
|
||||
/// Saves an <see cref="IBeatmap"/> file against a given <see cref="BeatmapInfo"/>.
|
||||
/// Saves an existing <see cref="IBeatmap"/> file against a given <see cref="BeatmapInfo"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method will also update any user beatmap collection hash references to the new post-saved hash.
|
||||
/// </remarks>
|
||||
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to save the content against. The file referenced by <see cref="BeatmapInfo.Path"/> will be replaced.</param>
|
||||
/// <param name="beatmapContent">The <see cref="IBeatmap"/> content to write.</param>
|
||||
/// <param name="beatmapSkin">The beatmap <see cref="ISkin"/> content to write, null if to be omitted.</param>
|
||||
public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null)
|
||||
{
|
||||
var setInfo = beatmapInfo.BeatmapSet;
|
||||
Debug.Assert(setInfo != null);
|
||||
|
||||
// Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`.
|
||||
// This should hopefully be temporary, assuming said clone is eventually removed.
|
||||
|
||||
// Warning: The directionality here is important. Changes have to be copied *from* beatmapContent (which comes from editor and is being saved)
|
||||
// *to* the beatmapInfo (which is a database model and needs to receive values without the taiko slider velocity multiplier for correct operation).
|
||||
// CopyTo() will undo such adjustments, while CopyFrom() will not.
|
||||
beatmapContent.Difficulty.CopyTo(beatmapInfo.Difficulty);
|
||||
|
||||
// All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.
|
||||
beatmapContent.BeatmapInfo = beatmapInfo;
|
||||
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
||||
new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw);
|
||||
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
// AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity.
|
||||
var existingFileInfo = beatmapInfo.Path != null ? setInfo.GetFile(beatmapInfo.Path) : null;
|
||||
string targetFilename = createBeatmapFilenameFromMetadata(beatmapInfo);
|
||||
|
||||
// ensure that two difficulties from the set don't point at the same beatmap file.
|
||||
if (setInfo.Beatmaps.Any(b => b.ID != beatmapInfo.ID && string.Equals(b.Path, targetFilename, StringComparison.OrdinalIgnoreCase)))
|
||||
throw new InvalidOperationException($"{setInfo.GetDisplayString()} already has a difficulty with the name of '{beatmapInfo.DifficultyName}'.");
|
||||
|
||||
if (existingFileInfo != null)
|
||||
DeleteFile(setInfo, existingFileInfo);
|
||||
|
||||
string oldMd5Hash = beatmapInfo.MD5Hash;
|
||||
|
||||
beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
|
||||
beatmapInfo.Hash = stream.ComputeSHA2Hash();
|
||||
|
||||
beatmapInfo.LastLocalUpdate = DateTimeOffset.Now;
|
||||
beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;
|
||||
|
||||
AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));
|
||||
|
||||
updateHashAndMarkDirty(setInfo);
|
||||
|
||||
Realm.Write(r =>
|
||||
{
|
||||
var liveBeatmapSet = r.Find<BeatmapSetInfo>(setInfo.ID);
|
||||
|
||||
setInfo.CopyChangesToRealm(liveBeatmapSet);
|
||||
|
||||
beatmapInfo.TransferCollectionReferences(r, oldMd5Hash);
|
||||
|
||||
ProcessBeatmap?.Invoke((liveBeatmapSet, false));
|
||||
});
|
||||
}
|
||||
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
|
||||
static string createBeatmapFilenameFromMetadata(BeatmapInfo beatmapInfo)
|
||||
{
|
||||
var metadata = beatmapInfo.Metadata;
|
||||
return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu".GetValidFilename();
|
||||
}
|
||||
}
|
||||
public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null) =>
|
||||
save(beatmapInfo, beatmapContent, beatmapSkin, transferCollections: true);
|
||||
|
||||
public void DeleteAllVideos()
|
||||
{
|
||||
@ -460,6 +399,74 @@ namespace osu.Game.Beatmaps
|
||||
setInfo.Status = BeatmapOnlineStatus.LocallyModified;
|
||||
}
|
||||
|
||||
private void save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin, bool transferCollections)
|
||||
{
|
||||
var setInfo = beatmapInfo.BeatmapSet;
|
||||
Debug.Assert(setInfo != null);
|
||||
|
||||
// Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`.
|
||||
// This should hopefully be temporary, assuming said clone is eventually removed.
|
||||
|
||||
// Warning: The directionality here is important. Changes have to be copied *from* beatmapContent (which comes from editor and is being saved)
|
||||
// *to* the beatmapInfo (which is a database model and needs to receive values without the taiko slider velocity multiplier for correct operation).
|
||||
// CopyTo() will undo such adjustments, while CopyFrom() will not.
|
||||
beatmapContent.Difficulty.CopyTo(beatmapInfo.Difficulty);
|
||||
|
||||
// All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.
|
||||
beatmapContent.BeatmapInfo = beatmapInfo;
|
||||
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
||||
new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw);
|
||||
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
// AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity.
|
||||
var existingFileInfo = beatmapInfo.Path != null ? setInfo.GetFile(beatmapInfo.Path) : null;
|
||||
string targetFilename = createBeatmapFilenameFromMetadata(beatmapInfo);
|
||||
|
||||
// ensure that two difficulties from the set don't point at the same beatmap file.
|
||||
if (setInfo.Beatmaps.Any(b => b.ID != beatmapInfo.ID && string.Equals(b.Path, targetFilename, StringComparison.OrdinalIgnoreCase)))
|
||||
throw new InvalidOperationException($"{setInfo.GetDisplayString()} already has a difficulty with the name of '{beatmapInfo.DifficultyName}'.");
|
||||
|
||||
if (existingFileInfo != null)
|
||||
DeleteFile(setInfo, existingFileInfo);
|
||||
|
||||
string oldMd5Hash = beatmapInfo.MD5Hash;
|
||||
|
||||
beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
|
||||
beatmapInfo.Hash = stream.ComputeSHA2Hash();
|
||||
|
||||
beatmapInfo.LastLocalUpdate = DateTimeOffset.Now;
|
||||
beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;
|
||||
|
||||
AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));
|
||||
|
||||
updateHashAndMarkDirty(setInfo);
|
||||
|
||||
Realm.Write(r =>
|
||||
{
|
||||
var liveBeatmapSet = r.Find<BeatmapSetInfo>(setInfo.ID);
|
||||
|
||||
setInfo.CopyChangesToRealm(liveBeatmapSet);
|
||||
|
||||
if (transferCollections)
|
||||
beatmapInfo.TransferCollectionReferences(r, oldMd5Hash);
|
||||
|
||||
ProcessBeatmap?.Invoke((liveBeatmapSet, false));
|
||||
});
|
||||
}
|
||||
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
|
||||
static string createBeatmapFilenameFromMetadata(BeatmapInfo beatmapInfo)
|
||||
{
|
||||
var metadata = beatmapInfo.Metadata;
|
||||
return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu".GetValidFilename();
|
||||
}
|
||||
}
|
||||
|
||||
#region Implementation of ICanAcceptFiles
|
||||
|
||||
public Task Import(params string[] paths) => beatmapImporter.Import(paths);
|
||||
|
@ -268,7 +268,10 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
Stream storyboardFileStream = null;
|
||||
|
||||
if (BeatmapSetInfo?.Files.FirstOrDefault(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename is string storyboardFilename)
|
||||
string mainStoryboardFilename = getMainStoryboardFilename(BeatmapSetInfo.Metadata);
|
||||
|
||||
if (BeatmapSetInfo?.Files.FirstOrDefault(f => f.Filename.Equals(mainStoryboardFilename, StringComparison.OrdinalIgnoreCase))?.Filename is string
|
||||
storyboardFilename)
|
||||
{
|
||||
string storyboardFileStorePath = BeatmapSetInfo?.GetPathForFile(storyboardFilename);
|
||||
storyboardFileStream = GetStream(storyboardFileStorePath);
|
||||
@ -312,6 +315,33 @@ namespace osu.Game.Beatmaps
|
||||
}
|
||||
|
||||
public override Stream GetStream(string storagePath) => resources.Files.GetStream(storagePath);
|
||||
|
||||
private string getMainStoryboardFilename(IBeatmapMetadataInfo metadata)
|
||||
{
|
||||
// Matches stable implementation, because it's probably simpler than trying to do anything else.
|
||||
// This may need to be reconsidered after we begin storing storyboards in the new editor.
|
||||
return windowsFilenameStrip(
|
||||
(metadata.Artist.Length > 0 ? metadata.Artist + @" - " + metadata.Title : Path.GetFileNameWithoutExtension(metadata.AudioFile))
|
||||
+ (metadata.Author.Username.Length > 0 ? @" (" + metadata.Author.Username + @")" : string.Empty)
|
||||
+ @".osb");
|
||||
|
||||
string windowsFilenameStrip(string entry)
|
||||
{
|
||||
// Inlined from Path.GetInvalidFilenameChars() to ensure the windows characters are used (to match stable).
|
||||
char[] invalidCharacters =
|
||||
{
|
||||
'\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07',
|
||||
'\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12',
|
||||
'\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D',
|
||||
'\x1E', '\x1F', '\x22', '\x3C', '\x3E', '\x7C', ':', '*', '?', '\\', '/'
|
||||
};
|
||||
|
||||
foreach (char c in invalidCharacters)
|
||||
entry = entry.Replace(c.ToString(), string.Empty);
|
||||
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -58,8 +58,12 @@ namespace osu.Game.Configuration
|
||||
|
||||
SetDefault(OsuSetting.BeatmapListingCardSize, BeatmapCardSize.Normal);
|
||||
|
||||
SetDefault(OsuSetting.ProfileCoverExpanded, true);
|
||||
|
||||
SetDefault(OsuSetting.ToolbarClockDisplayMode, ToolbarClockDisplayMode.Full);
|
||||
|
||||
SetDefault(OsuSetting.SongSelectBackgroundBlur, true);
|
||||
|
||||
// Online settings
|
||||
SetDefault(OsuSetting.Username, string.Empty);
|
||||
SetDefault(OsuSetting.Token, string.Empty);
|
||||
@ -339,6 +343,7 @@ namespace osu.Game.Configuration
|
||||
ChatDisplayHeight,
|
||||
BeatmapListingCardSize,
|
||||
ToolbarClockDisplayMode,
|
||||
SongSelectBackgroundBlur,
|
||||
Version,
|
||||
ShowFirstRunSetup,
|
||||
ShowConvertedBeatmaps,
|
||||
@ -375,5 +380,6 @@ namespace osu.Game.Configuration
|
||||
LastProcessedMetadataId,
|
||||
SafeAreaConsiderations,
|
||||
ComboColourNormalisationAmount,
|
||||
ProfileCoverExpanded,
|
||||
}
|
||||
}
|
||||
|
@ -31,12 +31,6 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
/// </summary>
|
||||
private const float equilateral_triangle_ratio = 0.866f;
|
||||
|
||||
/// <summary>
|
||||
/// How many screen-space pixels are smoothed over.
|
||||
/// Same behavior as Sprite's EdgeSmoothness.
|
||||
/// </summary>
|
||||
private const float edge_smoothness = 1;
|
||||
|
||||
private Color4 colourLight = Color4.White;
|
||||
|
||||
public Color4 ColourLight
|
||||
@ -83,6 +77,12 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
set => triangleScale.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If enabled, only the portion of triangles that falls within this <see cref="Drawable"/>'s
|
||||
/// shape is drawn to the screen.
|
||||
/// </summary>
|
||||
public bool Masking { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether we should drop-off alpha values of triangles more quickly to improve
|
||||
/// the visual appearance of fading. This defaults to on as it is generally more
|
||||
@ -115,7 +115,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
private void load(IRenderer renderer, ShaderManager shaders)
|
||||
{
|
||||
texture = renderer.WhitePixel;
|
||||
shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
|
||||
shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder");
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -252,14 +252,18 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
|
||||
private class TrianglesDrawNode : DrawNode
|
||||
{
|
||||
private float fill = 1f;
|
||||
|
||||
protected new Triangles Source => (Triangles)base.Source;
|
||||
|
||||
private IShader shader;
|
||||
private Texture texture;
|
||||
private bool masking;
|
||||
|
||||
private readonly List<TriangleParticle> parts = new List<TriangleParticle>();
|
||||
private Vector2 size;
|
||||
private readonly Vector2 triangleSize = new Vector2(1f, equilateral_triangle_ratio) * triangle_size;
|
||||
|
||||
private Vector2 size;
|
||||
private IVertexBatch<TexturedVertex2D> vertexBatch;
|
||||
|
||||
public TrianglesDrawNode(Triangles source)
|
||||
@ -274,6 +278,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
shader = Source.shader;
|
||||
texture = Source.texture;
|
||||
size = Source.DrawSize;
|
||||
masking = Source.Masking;
|
||||
|
||||
parts.Clear();
|
||||
parts.AddRange(Source.parts);
|
||||
@ -290,34 +295,52 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
}
|
||||
|
||||
shader.Bind();
|
||||
|
||||
Vector2 localInflationAmount = edge_smoothness * DrawInfo.MatrixInverse.ExtractScale().Xy;
|
||||
shader.GetUniform<float>("thickness").UpdateValue(ref fill);
|
||||
|
||||
foreach (TriangleParticle particle in parts)
|
||||
{
|
||||
var offset = triangle_size * new Vector2(particle.Scale * 0.5f, particle.Scale * equilateral_triangle_ratio);
|
||||
Vector2 relativeSize = Vector2.Divide(triangleSize * particle.Scale, size);
|
||||
|
||||
var triangle = new Triangle(
|
||||
Vector2Extensions.Transform(particle.Position * size, DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(particle.Position * size + offset, DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(particle.Position * size + new Vector2(-offset.X, offset.Y), DrawInfo.Matrix)
|
||||
Vector2 topLeft = particle.Position - new Vector2(relativeSize.X * 0.5f, 0f);
|
||||
|
||||
Quad triangleQuad = masking ? clampToDrawable(topLeft, relativeSize) : new Quad(topLeft.X, topLeft.Y, relativeSize.X, relativeSize.Y);
|
||||
|
||||
var drawQuad = new Quad(
|
||||
Vector2Extensions.Transform(triangleQuad.TopLeft * size, DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(triangleQuad.TopRight * size, DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(triangleQuad.BottomLeft * size, DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(triangleQuad.BottomRight * size, DrawInfo.Matrix)
|
||||
);
|
||||
|
||||
ColourInfo colourInfo = DrawColourInfo.Colour;
|
||||
colourInfo.ApplyChild(particle.Colour);
|
||||
|
||||
renderer.DrawTriangle(
|
||||
texture,
|
||||
triangle,
|
||||
colourInfo,
|
||||
null,
|
||||
vertexBatch.AddAction,
|
||||
Vector2.Divide(localInflationAmount, new Vector2(2 * offset.X, offset.Y)));
|
||||
RectangleF textureCoords = new RectangleF(
|
||||
triangleQuad.TopLeft.X - topLeft.X,
|
||||
triangleQuad.TopLeft.Y - topLeft.Y,
|
||||
triangleQuad.Width,
|
||||
triangleQuad.Height
|
||||
) / relativeSize;
|
||||
|
||||
renderer.DrawQuad(texture, drawQuad, colourInfo, new RectangleF(0, 0, 1, 1), vertexBatch.AddAction, textureCoords: textureCoords);
|
||||
}
|
||||
|
||||
shader.Unbind();
|
||||
}
|
||||
|
||||
private static Quad clampToDrawable(Vector2 topLeft, Vector2 size)
|
||||
{
|
||||
float leftClamped = Math.Clamp(topLeft.X, 0f, 1f);
|
||||
float topClamped = Math.Clamp(topLeft.Y, 0f, 1f);
|
||||
|
||||
return new Quad(
|
||||
leftClamped,
|
||||
topClamped,
|
||||
Math.Clamp(topLeft.X + size.X, 0f, 1f) - leftClamped,
|
||||
Math.Clamp(topLeft.Y + size.Y, 0f, 1f) - topClamped
|
||||
);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
@ -42,8 +42,12 @@ namespace osu.Game.Graphics.Containers.Markdown
|
||||
|
||||
protected override void AddFootnoteBacklink(FootnoteLink footnoteBacklink) => AddDrawable(new OsuMarkdownFootnoteBacklink(footnoteBacklink));
|
||||
|
||||
protected override SpriteText CreateEmphasisedSpriteText(bool bold, bool italic)
|
||||
=> CreateSpriteText().With(t => t.Font = t.Font.With(weight: bold ? FontWeight.Bold : FontWeight.Regular, italics: italic));
|
||||
protected override void ApplyEmphasisedCreationParameters(SpriteText spriteText, bool bold, bool italic)
|
||||
{
|
||||
base.ApplyEmphasisedCreationParameters(spriteText, bold, italic);
|
||||
|
||||
spriteText.Font = spriteText.Font.With(weight: bold ? FontWeight.Bold : FontWeight.Regular, italics: italic);
|
||||
}
|
||||
|
||||
protected override void AddCustomComponent(CustomContainerInline inline)
|
||||
{
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
@ -187,6 +188,41 @@ namespace osu.Game.Graphics
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves colour for a <see cref="RankingTier"/>.
|
||||
/// See https://www.figma.com/file/YHWhp9wZ089YXgB7pe6L1k/Tier-Colours
|
||||
/// </summary>
|
||||
public ColourInfo ForRankingTier(RankingTier tier)
|
||||
{
|
||||
switch (tier)
|
||||
{
|
||||
default:
|
||||
case RankingTier.Iron:
|
||||
return Color4Extensions.FromHex(@"BAB3AB");
|
||||
|
||||
case RankingTier.Bronze:
|
||||
return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"B88F7A"), Color4Extensions.FromHex(@"855C47"));
|
||||
|
||||
case RankingTier.Silver:
|
||||
return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"E0E0EB"), Color4Extensions.FromHex(@"A3A3C2"));
|
||||
|
||||
case RankingTier.Gold:
|
||||
return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"F0E4A8"), Color4Extensions.FromHex(@"E0C952"));
|
||||
|
||||
case RankingTier.Platinum:
|
||||
return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"A8F0EF"), Color4Extensions.FromHex(@"52E0DF"));
|
||||
|
||||
case RankingTier.Rhodium:
|
||||
return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"D9F8D3"), Color4Extensions.FromHex(@"A0CF96"));
|
||||
|
||||
case RankingTier.Radiant:
|
||||
return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"97DCFF"), Color4Extensions.FromHex(@"ED82FF"));
|
||||
|
||||
case RankingTier.Lustrous:
|
||||
return ColourInfo.GradientVertical(Color4Extensions.FromHex(@"FFE600"), Color4Extensions.FromHex(@"ED82FF"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a foreground text colour that is supposed to contrast well with
|
||||
/// the supplied <paramref name="backgroundColour"/>.
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
|
||||
namespace osu.Game.Graphics
|
||||
@ -115,6 +116,8 @@ namespace osu.Game.Graphics
|
||||
{
|
||||
Venera,
|
||||
Torus,
|
||||
|
||||
[Description("Torus (alternate)")]
|
||||
TorusAlternate,
|
||||
Inter,
|
||||
}
|
||||
|
@ -250,13 +250,16 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
protected override void OnFocus(FocusEvent e)
|
||||
{
|
||||
BorderThickness = 3;
|
||||
if (Masking)
|
||||
BorderThickness = 3;
|
||||
|
||||
base.OnFocus(e);
|
||||
}
|
||||
|
||||
protected override void OnFocusLost(FocusLostEvent e)
|
||||
{
|
||||
BorderThickness = 0;
|
||||
if (Masking)
|
||||
BorderThickness = 0;
|
||||
|
||||
base.OnFocusLost(e);
|
||||
}
|
||||
|
@ -152,22 +152,6 @@ namespace osu.Game.Graphics.UserInterface
|
||||
segments.Sort();
|
||||
}
|
||||
|
||||
private ColourInfo getSegmentColour(SegmentInfo segment)
|
||||
{
|
||||
var segmentColour = new ColourInfo
|
||||
{
|
||||
TopLeft = DrawColourInfo.Colour.Interpolate(new Vector2(segment.Start, 0f)),
|
||||
TopRight = DrawColourInfo.Colour.Interpolate(new Vector2(segment.End, 0f)),
|
||||
BottomLeft = DrawColourInfo.Colour.Interpolate(new Vector2(segment.Start, 1f)),
|
||||
BottomRight = DrawColourInfo.Colour.Interpolate(new Vector2(segment.End, 1f))
|
||||
};
|
||||
|
||||
var tierColour = segment.Tier >= 0 ? tierColours[segment.Tier] : new Colour4(0, 0, 0, 0);
|
||||
segmentColour.ApplyChild(tierColour);
|
||||
|
||||
return segmentColour;
|
||||
}
|
||||
|
||||
protected override DrawNode CreateDrawNode() => new SegmentedGraphDrawNode(this);
|
||||
|
||||
protected struct SegmentInfo
|
||||
@ -215,6 +199,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
private IShader shader = null!;
|
||||
private readonly List<SegmentInfo> segments = new List<SegmentInfo>();
|
||||
private Vector2 drawSize;
|
||||
private readonly List<Colour4> tierColours = new List<Colour4>();
|
||||
|
||||
public SegmentedGraphDrawNode(SegmentedGraph<T> source)
|
||||
: base(source)
|
||||
@ -228,8 +213,12 @@ namespace osu.Game.Graphics.UserInterface
|
||||
texture = Source.texture;
|
||||
shader = Source.shader;
|
||||
drawSize = Source.DrawSize;
|
||||
|
||||
segments.Clear();
|
||||
segments.AddRange(Source.segments.Where(s => s.Length * drawSize.X > 1));
|
||||
|
||||
tierColours.Clear();
|
||||
tierColours.AddRange(Source.tierColours);
|
||||
}
|
||||
|
||||
public override void Draw(IRenderer renderer)
|
||||
@ -252,11 +241,27 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Vector2Extensions.Transform(topRight, DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(bottomLeft, DrawInfo.Matrix),
|
||||
Vector2Extensions.Transform(bottomRight, DrawInfo.Matrix)),
|
||||
Source.getSegmentColour(segment));
|
||||
getSegmentColour(segment));
|
||||
}
|
||||
|
||||
shader.Unbind();
|
||||
}
|
||||
|
||||
private ColourInfo getSegmentColour(SegmentInfo segment)
|
||||
{
|
||||
var segmentColour = new ColourInfo
|
||||
{
|
||||
TopLeft = DrawColourInfo.Colour.Interpolate(new Vector2(segment.Start, 0f)),
|
||||
TopRight = DrawColourInfo.Colour.Interpolate(new Vector2(segment.End, 0f)),
|
||||
BottomLeft = DrawColourInfo.Colour.Interpolate(new Vector2(segment.Start, 1f)),
|
||||
BottomRight = DrawColourInfo.Colour.Interpolate(new Vector2(segment.End, 1f))
|
||||
};
|
||||
|
||||
var tierColour = segment.Tier >= 0 ? tierColours[segment.Tier] : new Colour4(0, 0, 0, 0);
|
||||
segmentColour.ApplyChild(tierColour);
|
||||
|
||||
return segmentColour;
|
||||
}
|
||||
}
|
||||
|
||||
protected class SegmentManager : IEnumerable<SegmentInfo>
|
||||
|
@ -160,9 +160,12 @@ namespace osu.Game
|
||||
|
||||
protected Bindable<WorkingBeatmap> Beatmap { get; private set; } // cached via load() method
|
||||
|
||||
/// <summary>
|
||||
/// The current ruleset selection for the local user.
|
||||
/// </summary>
|
||||
[Cached]
|
||||
[Cached(typeof(IBindable<RulesetInfo>))]
|
||||
protected readonly Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
|
||||
protected internal readonly Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
|
||||
|
||||
/// <summary>
|
||||
/// The current mod selection for the local user.
|
||||
@ -553,8 +556,8 @@ namespace osu.Game
|
||||
case JoystickHandler jh:
|
||||
return new JoystickSettings(jh);
|
||||
|
||||
case TouchHandler:
|
||||
return new InputSection.HandlerSection(handler);
|
||||
case TouchHandler th:
|
||||
return new TouchSettings(th);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,12 +5,14 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Online.API.Requests;
|
||||
@ -22,8 +24,6 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
public partial class ChangelogOverlay : OnlineOverlay<ChangelogHeader>
|
||||
{
|
||||
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
|
||||
|
||||
public readonly Bindable<APIChangelogBuild> Current = new Bindable<APIChangelogBuild>();
|
||||
|
||||
private List<APIChangelogBuild> builds;
|
||||
@ -81,6 +81,8 @@ namespace osu.Game.Overlays
|
||||
ArgumentNullException.ThrowIfNull(updateStream);
|
||||
ArgumentNullException.ThrowIfNull(version);
|
||||
|
||||
Show();
|
||||
|
||||
performAfterFetch(() =>
|
||||
{
|
||||
var build = builds.Find(b => b.Version == version && b.UpdateStream.Name == updateStream)
|
||||
@ -89,8 +91,6 @@ namespace osu.Game.Overlays
|
||||
if (build != null)
|
||||
ShowBuild(build);
|
||||
});
|
||||
|
||||
Show();
|
||||
}
|
||||
|
||||
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
@ -127,11 +127,16 @@ namespace osu.Game.Overlays
|
||||
|
||||
private Task initialFetchTask;
|
||||
|
||||
private void performAfterFetch(Action action) => Schedule(() =>
|
||||
private void performAfterFetch(Action action)
|
||||
{
|
||||
fetchListing()?.ContinueWith(_ =>
|
||||
Schedule(action), TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||
});
|
||||
Debug.Assert(State.Value == Visibility.Visible);
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
fetchListing()?.ContinueWith(_ =>
|
||||
Schedule(action), TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||
});
|
||||
}
|
||||
|
||||
private Task fetchListing()
|
||||
{
|
||||
|
@ -147,7 +147,7 @@ namespace osu.Game.Overlays.Comments
|
||||
private void updateCommitButtonState() =>
|
||||
commitButton.Enabled.Value = loadingSpinner.State.Value == Visibility.Hidden && !string.IsNullOrEmpty(Current.Value);
|
||||
|
||||
private partial class EditorTextBox : BasicTextBox
|
||||
private partial class EditorTextBox : OsuTextBox
|
||||
{
|
||||
protected override float LeftRightPadding => side_padding;
|
||||
|
||||
@ -173,12 +173,6 @@ namespace osu.Game.Overlays.Comments
|
||||
{
|
||||
Font = OsuFont.GetFont(weight: FontWeight.Regular),
|
||||
};
|
||||
|
||||
protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Child = new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: CalculatedTextSize) }
|
||||
};
|
||||
}
|
||||
|
||||
protected partial class EditorButton : RoundedButton
|
||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
{
|
||||
public partial class GroupBadge : Container, IHasTooltip
|
||||
{
|
||||
public LocalisableString TooltipText { get; }
|
||||
public LocalisableString TooltipText { get; private set; }
|
||||
|
||||
public int TextSize { get; set; } = 12;
|
||||
|
||||
@ -78,6 +78,11 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
icon.Size = new Vector2(TextSize - 1);
|
||||
})).ToList()
|
||||
);
|
||||
|
||||
var badgeModesList = group.Playmodes.Select(p => rulesets.GetRuleset(p)?.Name).ToList();
|
||||
|
||||
string modesDisplay = string.Join(", ", badgeModesList);
|
||||
TooltipText += $" ({modesDisplay})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
@ -12,6 +13,7 @@ using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Overlays.Profile.Header.Components
|
||||
@ -23,6 +25,10 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
public LocalisableString TooltipText { get; private set; }
|
||||
|
||||
private OsuSpriteText levelText = null!;
|
||||
private Sprite sprite = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour osuColour { get; set; } = null!;
|
||||
|
||||
public LevelBadge()
|
||||
{
|
||||
@ -34,7 +40,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Sprite
|
||||
sprite = new Sprite
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Texture = textures.Get("Profile/levelbadge"),
|
||||
@ -58,9 +64,34 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
|
||||
private void updateLevel(UserStatistics.LevelInfo? levelInfo)
|
||||
{
|
||||
string level = levelInfo?.Current.ToString() ?? "0";
|
||||
levelText.Text = level;
|
||||
TooltipText = UsersStrings.ShowStatsLevel(level);
|
||||
int level = levelInfo?.Current ?? 0;
|
||||
|
||||
levelText.Text = level.ToString();
|
||||
TooltipText = UsersStrings.ShowStatsLevel(level.ToString());
|
||||
|
||||
sprite.Colour = mapLevelToTierColour(level);
|
||||
}
|
||||
|
||||
private ColourInfo mapLevelToTierColour(int level)
|
||||
{
|
||||
var tier = RankingTier.Iron;
|
||||
|
||||
if (level > 0)
|
||||
{
|
||||
tier = (RankingTier)(level / 20);
|
||||
}
|
||||
|
||||
if (level >= 105)
|
||||
{
|
||||
tier = RankingTier.Radiant;
|
||||
}
|
||||
|
||||
if (level >= 110)
|
||||
{
|
||||
tier = RankingTier.Lustrous;
|
||||
}
|
||||
|
||||
return osuColour.ForRankingTier(tier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
@ -15,11 +14,11 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Profile.Header.Components
|
||||
{
|
||||
public partial class ExpandDetailsButton : ProfileHeaderButton
|
||||
public partial class ToggleCoverButton : ProfileHeaderButton
|
||||
{
|
||||
public readonly BindableBool DetailsVisible = new BindableBool();
|
||||
public readonly BindableBool CoverExpanded = new BindableBool(true);
|
||||
|
||||
public override LocalisableString TooltipText => DetailsVisible.Value ? CommonStrings.ButtonsCollapse : CommonStrings.ButtonsExpand;
|
||||
public override LocalisableString TooltipText => CoverExpanded.Value ? UsersStrings.ShowCoverTo0 : UsersStrings.ShowCoverTo1;
|
||||
|
||||
private SpriteIcon icon = null!;
|
||||
private Sample? sampleOpen;
|
||||
@ -27,12 +26,12 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
|
||||
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds();
|
||||
|
||||
public ExpandDetailsButton()
|
||||
public ToggleCoverButton()
|
||||
{
|
||||
Action = () =>
|
||||
{
|
||||
DetailsVisible.Toggle();
|
||||
(DetailsVisible.Value ? sampleOpen : sampleClose)?.Play();
|
||||
CoverExpanded.Toggle();
|
||||
(CoverExpanded.Value ? sampleOpen : sampleClose)?.Play();
|
||||
};
|
||||
}
|
||||
|
||||
@ -40,19 +39,21 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
private void load(OverlayColourProvider colourProvider, AudioManager audio)
|
||||
{
|
||||
IdleColour = colourProvider.Background2;
|
||||
HoverColour = colourProvider.Background2.Lighten(0.2f);
|
||||
HoverColour = colourProvider.Background1;
|
||||
|
||||
sampleOpen = audio.Samples.Get(@"UI/dropdown-open");
|
||||
sampleClose = audio.Samples.Get(@"UI/dropdown-close");
|
||||
|
||||
AutoSizeAxes = Axes.None;
|
||||
Size = new Vector2(30);
|
||||
Child = icon = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(20, 12)
|
||||
Size = new Vector2(10.5f, 12)
|
||||
};
|
||||
|
||||
DetailsVisible.BindValueChanged(visible => updateState(visible.NewValue), true);
|
||||
CoverExpanded.BindValueChanged(visible => updateState(visible.NewValue), true);
|
||||
}
|
||||
|
||||
private void updateState(bool detailsVisible) => icon.Icon = detailsVisible ? FontAwesome.Solid.ChevronUp : FontAwesome.Solid.ChevronDown;
|
@ -7,13 +7,16 @@ using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Overlays.Profile.Header.Components;
|
||||
using osu.Game.Users;
|
||||
using osu.Game.Users.Drawables;
|
||||
using osuTK;
|
||||
|
||||
@ -21,13 +24,15 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
{
|
||||
public partial class TopHeaderContainer : CompositeDrawable
|
||||
{
|
||||
private const float avatar_size = 110;
|
||||
private const float content_height = 65;
|
||||
private const float vertical_padding = 10;
|
||||
|
||||
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
private UserCoverBackground cover = null!;
|
||||
private SupporterIcon supporterTag = null!;
|
||||
private UpdateableAvatar avatar = null!;
|
||||
private OsuSpriteText usernameText = null!;
|
||||
@ -36,11 +41,19 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
private UpdateableFlag userFlag = null!;
|
||||
private OsuSpriteText userCountryText = null!;
|
||||
private GroupBadgeFlow groupBadgeFlow = null!;
|
||||
private ToggleCoverButton coverToggle = null!;
|
||||
|
||||
private Bindable<bool> coverExpanded = null!;
|
||||
|
||||
private FillFlowContainer flow = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
private void load(OverlayColourProvider colourProvider, OsuConfigManager configManager)
|
||||
{
|
||||
Height = 150;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
coverExpanded = configManager.GetBindable<bool>(OsuSetting.ProfileCoverExpanded);
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
@ -51,124 +64,147 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Horizontal,
|
||||
Margin = new MarginPadding { Left = UserProfileOverlay.CONTENT_X_MARGIN },
|
||||
Height = avatar_size,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
avatar = new UpdateableAvatar(isInteractive: false, showGuestOnNull: false)
|
||||
cover = new ProfileCoverBackground
|
||||
{
|
||||
Size = new Vector2(avatar_size),
|
||||
Masking = true,
|
||||
CornerRadius = avatar_size * 0.25f,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
new OsuContextMenuContainer
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Child = new Container
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Padding = new MarginPadding { Left = 10 },
|
||||
Children = new Drawable[]
|
||||
flow = new FillFlowContainer
|
||||
{
|
||||
new FillFlowContainer
|
||||
Direction = FillDirection.Horizontal,
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(5),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
usernameText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Regular)
|
||||
},
|
||||
openUserExternally = new ExternalLinkButton
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
groupBadgeFlow = new GroupBadgeFlow
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
}
|
||||
}
|
||||
},
|
||||
titleText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 18, weight: FontWeight.Regular)
|
||||
},
|
||||
}
|
||||
Left = UserProfileOverlay.CONTENT_X_MARGIN,
|
||||
Vertical = vertical_padding
|
||||
},
|
||||
new FillFlowContainer
|
||||
Height = content_height + 2 * vertical_padding,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Direction = FillDirection.Vertical,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
avatar = new UpdateableAvatar(isInteractive: false, showGuestOnNull: false)
|
||||
{
|
||||
supporterTag = new SupporterIcon
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Masking = true,
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Height = 20,
|
||||
Margin = new MarginPadding { Top = 5 }
|
||||
},
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 1.5f,
|
||||
Margin = new MarginPadding { Top = 10 },
|
||||
Colour = colourProvider.Light1,
|
||||
},
|
||||
new FillFlowContainer
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Offset = new Vector2(0, 1),
|
||||
Radius = 3,
|
||||
Colour = Colour4.Black.Opacity(0.25f),
|
||||
}
|
||||
},
|
||||
new OsuContextMenuContainer
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Margin = new MarginPadding { Top = 5 },
|
||||
Direction = FillDirection.Horizontal,
|
||||
Direction = FillDirection.Vertical,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
userFlag = new UpdateableFlag
|
||||
new FillFlowContainer
|
||||
{
|
||||
Size = new Vector2(28, 20),
|
||||
ShowPlaceholderOnUnknown = false,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(5, 0),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
usernameText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Regular)
|
||||
},
|
||||
supporterTag = new SupporterIcon
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Height = 15,
|
||||
},
|
||||
openUserExternally = new ExternalLinkButton
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
groupBadgeFlow = new GroupBadgeFlow
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
}
|
||||
},
|
||||
userCountryText = new OsuSpriteText
|
||||
titleText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 17.5f, weight: FontWeight.Regular),
|
||||
Margin = new MarginPadding { Left = 10 },
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Colour = colourProvider.Light1,
|
||||
}
|
||||
Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular),
|
||||
Margin = new MarginPadding { Bottom = 5 }
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
userFlag = new UpdateableFlag
|
||||
{
|
||||
Size = new Vector2(28, 20),
|
||||
ShowPlaceholderOnUnknown = false,
|
||||
},
|
||||
userCountryText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular),
|
||||
Margin = new MarginPadding { Left = 5 },
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
coverToggle = new ToggleCoverButton
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Margin = new MarginPadding { Right = 10 },
|
||||
CoverExpanded = { BindTarget = coverExpanded }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
User.BindValueChanged(user => updateUser(user.NewValue));
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
User.BindValueChanged(user => updateUser(user.NewValue), true);
|
||||
coverExpanded.BindValueChanged(_ => updateCoverState(), true);
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
private void updateUser(UserProfileData? data)
|
||||
{
|
||||
var user = data?.User;
|
||||
|
||||
cover.User = user;
|
||||
avatar.User = user;
|
||||
usernameText.Text = user?.Username ?? string.Empty;
|
||||
openUserExternally.Link = $@"{api.WebsiteRootUrl}/users/{user?.Id ?? 0}";
|
||||
@ -179,5 +215,27 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
titleText.Colour = Color4Extensions.FromHex(user?.Colour ?? "fff");
|
||||
groupBadgeFlow.User.Value = user;
|
||||
}
|
||||
|
||||
private void updateCoverState()
|
||||
{
|
||||
const float transition_duration = 500;
|
||||
|
||||
bool expanded = coverToggle.CoverExpanded.Value;
|
||||
|
||||
cover.ResizeHeightTo(expanded ? 250 : 0, transition_duration, Easing.OutQuint);
|
||||
avatar.ResizeTo(new Vector2(expanded ? 120 : content_height), transition_duration, Easing.OutQuint);
|
||||
avatar.TransformTo(nameof(avatar.CornerRadius), expanded ? 40f : 20f, transition_duration, Easing.OutQuint);
|
||||
flow.TransformTo(nameof(flow.Spacing), new Vector2(expanded ? 20f : 10f), transition_duration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private partial class ProfileCoverBackground : UserCoverBackground
|
||||
{
|
||||
protected override double LoadDelay => 0;
|
||||
|
||||
public ProfileCoverBackground()
|
||||
{
|
||||
Masking = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,23 +3,17 @@
|
||||
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Overlays.Profile.Header;
|
||||
using osu.Game.Overlays.Profile.Header.Components;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Overlays.Profile
|
||||
{
|
||||
public partial class ProfileHeader : TabControlOverlayHeader<LocalisableString>
|
||||
{
|
||||
private UserCoverBackground coverContainer = null!;
|
||||
|
||||
public Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
|
||||
|
||||
private CentreHeaderContainer centreHeaderContainer;
|
||||
@ -29,8 +23,6 @@ namespace osu.Game.Overlays.Profile
|
||||
{
|
||||
ContentSidePadding = UserProfileOverlay.CONTENT_X_MARGIN;
|
||||
|
||||
User.ValueChanged += e => updateDisplay(e.NewValue);
|
||||
|
||||
TabControl.AddItem(LayoutStrings.HeaderUsersShow);
|
||||
|
||||
// todo: pending implementation.
|
||||
@ -41,25 +33,7 @@ namespace osu.Game.Overlays.Profile
|
||||
Debug.Assert(detailHeaderContainer != null);
|
||||
}
|
||||
|
||||
protected override Drawable CreateBackground() =>
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 150,
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
coverContainer = new ProfileCoverBackground
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourInfo.GradientVertical(Color4Extensions.FromHex("222").Opacity(0.8f), Color4Extensions.FromHex("222").Opacity(0.2f))
|
||||
},
|
||||
}
|
||||
};
|
||||
protected override Drawable CreateBackground() => Empty();
|
||||
|
||||
protected override Drawable CreateContent() => new FillFlowContainer
|
||||
{
|
||||
@ -103,8 +77,6 @@ namespace osu.Game.Overlays.Profile
|
||||
User = { BindTarget = User }
|
||||
};
|
||||
|
||||
private void updateDisplay(UserProfileData? user) => coverContainer.User = user?.User;
|
||||
|
||||
private partial class ProfileHeaderTitle : OverlayTitle
|
||||
{
|
||||
public ProfileHeaderTitle()
|
||||
@ -113,10 +85,5 @@ namespace osu.Game.Overlays.Profile
|
||||
IconTexture = "Icons/Hexacons/profile";
|
||||
}
|
||||
}
|
||||
|
||||
private partial class ProfileCoverBackground : UserCoverBackground
|
||||
{
|
||||
protected override double LoadDelay => 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = SkinSettingsStrings.GameplayCursorDuringTouch,
|
||||
Keywords = new[] { @"touchscreen" },
|
||||
Current = config.GetBindable<bool>(OsuSetting.GameplayCursorDuringTouch)
|
||||
},
|
||||
};
|
||||
|
@ -70,6 +70,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
|
||||
Add(new SettingsButton
|
||||
{
|
||||
Text = GeneralSettingsStrings.OpenOsuFolder,
|
||||
Keywords = new[] { @"logs", @"files", @"access", "directory" },
|
||||
Action = () => storage.PresentExternally(),
|
||||
});
|
||||
|
||||
|
@ -34,6 +34,7 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
new SettingsButton
|
||||
{
|
||||
Text = GeneralSettingsStrings.RunSetupWizard,
|
||||
Keywords = new[] { @"first run", @"initial", @"getting started" },
|
||||
TooltipText = FirstRunSetupOverlayStrings.FirstRunSetupDescription,
|
||||
Action = () => firstRunSetupOverlay?.Show(),
|
||||
},
|
||||
|
@ -133,6 +133,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
new SettingsSlider<float>
|
||||
{
|
||||
LabelText = GraphicsSettingsStrings.HorizontalPosition,
|
||||
Keywords = new[] { "screen", "scaling" },
|
||||
Current = scalingPositionX,
|
||||
KeyboardStep = 0.01f,
|
||||
DisplayAsPercentage = true
|
||||
@ -140,6 +141,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
new SettingsSlider<float>
|
||||
{
|
||||
LabelText = GraphicsSettingsStrings.VerticalPosition,
|
||||
Keywords = new[] { "screen", "scaling" },
|
||||
Current = scalingPositionY,
|
||||
KeyboardStep = 0.01f,
|
||||
DisplayAsPercentage = true
|
||||
@ -147,6 +149,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
new SettingsSlider<float>
|
||||
{
|
||||
LabelText = GraphicsSettingsStrings.HorizontalScale,
|
||||
Keywords = new[] { "screen", "scaling" },
|
||||
Current = scalingSizeX,
|
||||
KeyboardStep = 0.01f,
|
||||
DisplayAsPercentage = true
|
||||
@ -154,6 +157,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
new SettingsSlider<float>
|
||||
{
|
||||
LabelText = GraphicsSettingsStrings.VerticalScale,
|
||||
Keywords = new[] { "screen", "scaling" },
|
||||
Current = scalingSizeY,
|
||||
KeyboardStep = 0.01f,
|
||||
DisplayAsPercentage = true
|
||||
|
@ -15,7 +15,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
{
|
||||
protected override LocalisableString Header => BindingSettingsStrings.ShortcutAndGameplayBindings;
|
||||
|
||||
public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "keybindings" });
|
||||
public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { @"keybindings", @"controls", @"keyboard", @"keys" });
|
||||
|
||||
public BindingSettings(KeyBindingPanel keyConfig)
|
||||
{
|
||||
|
40
osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs
Normal file
40
osu.Game/Overlays/Settings/Sections/Input/TouchSettings.cs
Normal file
@ -0,0 +1,40 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Handlers.Touch;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
{
|
||||
public partial class TouchSettings : SettingsSubsection
|
||||
{
|
||||
private readonly TouchHandler handler;
|
||||
|
||||
public TouchSettings(TouchHandler handler)
|
||||
{
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = CommonStrings.Enabled,
|
||||
Current = handler.Enabled
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { @"touchscreen" });
|
||||
|
||||
protected override LocalisableString Header => handler.Description;
|
||||
}
|
||||
}
|
@ -47,7 +47,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
var directoryInfos = target.GetDirectories();
|
||||
var fileInfos = target.GetFiles();
|
||||
|
||||
if (directoryInfos.Length > 0 || fileInfos.Length > 0)
|
||||
if (directoryInfos.Length > 0 || fileInfos.Length > 0 || target.Parent == null)
|
||||
{
|
||||
// Quick test for whether there's already an osu! install at the target path.
|
||||
if (fileInfos.Any(f => f.Name == OsuGameBase.CLIENT_DATABASE_FILENAME))
|
||||
@ -65,7 +65,9 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
return;
|
||||
}
|
||||
|
||||
target = target.CreateSubdirectory("osu-lazer");
|
||||
// Not using CreateSubDirectory as it throws unexpectedly when attempting to create a directory when already at the root of a disk.
|
||||
// See https://cs.github.com/dotnet/runtime/blob/f1bdd5a6182f43f3928b389b03f7bc26f826c8bc/src/libraries/System.Private.CoreLib/src/System/IO/DirectoryInfo.cs#L88-L94
|
||||
target = Directory.CreateDirectory(Path.Combine(target.FullName, @"osu-lazer"));
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
|
@ -42,6 +42,12 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
|
||||
LabelText = UserInterfaceStrings.ModSelectHotkeyStyle,
|
||||
Current = config.GetBindable<ModSelectHotkeyStyle>(OsuSetting.ModSelectHotkeyStyle),
|
||||
ClassicDefault = ModSelectHotkeyStyle.Classic
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = GameplaySettingsStrings.BackgroundBlur,
|
||||
Current = config.GetBindable<bool>(OsuSetting.SongSelectBackgroundBlur),
|
||||
ClassicDefault = false,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -22,6 +22,8 @@ namespace osu.Game.Overlays.Settings
|
||||
|
||||
public LocalisableString TooltipText { get; set; }
|
||||
|
||||
public IEnumerable<string> Keywords { get; set; } = Array.Empty<string>();
|
||||
|
||||
public BindableBool CanBeShown { get; } = new BindableBool(true);
|
||||
IBindable<bool> IConditionalFilterable.CanBeShown => CanBeShown;
|
||||
|
||||
@ -30,9 +32,13 @@ namespace osu.Game.Overlays.Settings
|
||||
get
|
||||
{
|
||||
if (TooltipText != default)
|
||||
return base.FilterTerms.Append(TooltipText);
|
||||
yield return TooltipText;
|
||||
|
||||
return base.FilterTerms;
|
||||
foreach (string s in Keywords)
|
||||
yield return s;
|
||||
|
||||
foreach (LocalisableString s in base.FilterTerms)
|
||||
yield return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ namespace osu.Game.Overlays.Settings
|
||||
Text = game.Name,
|
||||
Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold),
|
||||
},
|
||||
new BuildDisplay(game.Version, DebugUtils.IsDebugBuild)
|
||||
new BuildDisplay(game.Version)
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
@ -81,15 +81,13 @@ namespace osu.Game.Overlays.Settings
|
||||
private partial class BuildDisplay : OsuAnimatedButton
|
||||
{
|
||||
private readonly string version;
|
||||
private readonly bool isDebug;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
public BuildDisplay(string version, bool isDebug)
|
||||
public BuildDisplay(string version)
|
||||
{
|
||||
this.version = version;
|
||||
this.isDebug = isDebug;
|
||||
|
||||
Content.RelativeSizeAxes = Axes.Y;
|
||||
Content.AutoSizeAxes = AutoSizeAxes = Axes.X;
|
||||
@ -99,8 +97,7 @@ namespace osu.Game.Overlays.Settings
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(ChangelogOverlay changelog)
|
||||
{
|
||||
if (!isDebug)
|
||||
Action = () => changelog?.ShowBuild(OsuGameBase.CLIENT_STREAM_NAME, version);
|
||||
Action = () => changelog?.ShowBuild(OsuGameBase.CLIENT_STREAM_NAME, version);
|
||||
|
||||
Add(new OsuSpriteText
|
||||
{
|
||||
@ -110,7 +107,7 @@ namespace osu.Game.Overlays.Settings
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Padding = new MarginPadding(5),
|
||||
Colour = isDebug ? colours.Red : Color4.White,
|
||||
Colour = DebugUtils.IsDebugBuild ? colours.Red : Color4.White,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -125,10 +126,21 @@ namespace osu.Game.Rulesets.Edit
|
||||
public virtual MenuItem[] ContextMenuItems => Array.Empty<MenuItem>();
|
||||
|
||||
/// <summary>
|
||||
/// The screen-space point that causes this <see cref="HitObjectSelectionBlueprint"/> to be selected via a drag.
|
||||
/// The screen-space main point that causes this <see cref="HitObjectSelectionBlueprint"/> to be selected via a drag.
|
||||
/// </summary>
|
||||
public virtual Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.Centre;
|
||||
|
||||
/// <summary>
|
||||
/// Any points that should be used for snapping purposes in addition to <see cref="ScreenSpaceSelectionPoint"/>. Exposed via <see cref="ScreenSpaceSnapPoints"/>.
|
||||
/// </summary>
|
||||
protected virtual Vector2[] ScreenSpaceAdditionalNodes => Array.Empty<Vector2>();
|
||||
|
||||
/// <summary>
|
||||
/// The screen-space collection of base points on this <see cref="HitObjectSelectionBlueprint"/> that other objects can be snapped to.
|
||||
/// The first element of this collection is <see cref="ScreenSpaceSelectionPoint"/>
|
||||
/// </summary>
|
||||
public Vector2[] ScreenSpaceSnapPoints => ScreenSpaceAdditionalNodes.Prepend(ScreenSpaceSelectionPoint).ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// The screen-space quad that outlines this <see cref="HitObjectSelectionBlueprint"/> for selections.
|
||||
/// </summary>
|
||||
|
80
osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs
Normal file
80
osu.Game/Rulesets/Mods/ModAccuracyChallenge.cs
Normal file
@ -0,0 +1,80 @@
|
||||
// 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.Globalization;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public class ModAccuracyChallenge : ModFailCondition, IApplicableToScoreProcessor
|
||||
{
|
||||
public override string Name => "Accuracy Challenge";
|
||||
|
||||
public override string Acronym => "AC";
|
||||
|
||||
public override LocalisableString Description => "Fail if your accuracy drops too low!";
|
||||
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
|
||||
public override double ScoreMultiplier => 1.0;
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(ModEasyWithExtraLives), typeof(ModPerfect) }).ToArray();
|
||||
|
||||
public override bool RequiresConfiguration => false;
|
||||
|
||||
public override string SettingDescription => base.SettingDescription.Replace(MinimumAccuracy.ToString(), MinimumAccuracy.Value.ToString("##%", NumberFormatInfo.InvariantInfo));
|
||||
|
||||
[SettingSource("Minimum accuracy", "Trigger a failure if your accuracy goes below this value.", SettingControlType = typeof(SettingsSlider<double, PercentSlider>))]
|
||||
public BindableNumber<double> MinimumAccuracy { get; } = new BindableDouble
|
||||
{
|
||||
MinValue = 0.60,
|
||||
MaxValue = 0.99,
|
||||
Precision = 0.01,
|
||||
Default = 0.9,
|
||||
Value = 0.9,
|
||||
};
|
||||
|
||||
private ScoreProcessor scoreProcessor = null!;
|
||||
|
||||
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) => this.scoreProcessor = scoreProcessor;
|
||||
|
||||
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
|
||||
|
||||
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
|
||||
{
|
||||
if (!result.Type.AffectsAccuracy())
|
||||
return false;
|
||||
|
||||
return getAccuracyWithImminentResultAdded(result) < MinimumAccuracy.Value;
|
||||
}
|
||||
|
||||
private double getAccuracyWithImminentResultAdded(JudgementResult result)
|
||||
{
|
||||
var score = new ScoreInfo { Ruleset = scoreProcessor.Ruleset.RulesetInfo };
|
||||
|
||||
// This is super ugly, but if we don't do it this way we will not have the most recent result added to the accuracy value.
|
||||
// Hopefully we can improve this in the future.
|
||||
scoreProcessor.PopulateScore(score);
|
||||
score.Statistics[result.Type]++;
|
||||
|
||||
return scoreProcessor.ComputeAccuracy(score);
|
||||
}
|
||||
}
|
||||
|
||||
public partial class PercentSlider : OsuSliderBar<double>
|
||||
{
|
||||
public PercentSlider()
|
||||
{
|
||||
DisplayAsPercentage = true;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Humanizer;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -19,6 +21,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
};
|
||||
|
||||
public override string SettingDescription => Retries.IsDefault ? string.Empty : $"{"lives".ToQuantity(Retries.Value)}";
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAccuracyChallenge)).ToArray();
|
||||
|
||||
private int retries;
|
||||
|
||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override LocalisableString Description => "SS or quit.";
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModSuddenDeath)).ToArray();
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(ModSuddenDeath), typeof(ModAccuracyChallenge) }).ToArray();
|
||||
|
||||
protected ModPerfect()
|
||||
{
|
||||
|
@ -97,7 +97,11 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
protected virtual double ClassicScoreMultiplier => 36;
|
||||
|
||||
private readonly Ruleset ruleset;
|
||||
/// <summary>
|
||||
/// The ruleset this score processor is valid for.
|
||||
/// </summary>
|
||||
public readonly Ruleset Ruleset;
|
||||
|
||||
private readonly double accuracyPortion;
|
||||
private readonly double comboPortion;
|
||||
|
||||
@ -145,7 +149,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
|
||||
public ScoreProcessor(Ruleset ruleset)
|
||||
{
|
||||
this.ruleset = ruleset;
|
||||
Ruleset = ruleset;
|
||||
|
||||
accuracyPortion = DefaultAccuracyPortion;
|
||||
comboPortion = DefaultComboPortion;
|
||||
@ -291,8 +295,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
[Pure]
|
||||
public double ComputeAccuracy(ScoreInfo scoreInfo)
|
||||
{
|
||||
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
|
||||
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
|
||||
if (!Ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
|
||||
throw new ArgumentException($"Unexpected score ruleset. Expected \"{Ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
|
||||
|
||||
// We only extract scoring values from the score's statistics. This is because accuracy is always relative to the point of pass or fail rather than relative to the whole beatmap.
|
||||
extractScoringValues(scoreInfo.Statistics, out var current, out var maximum);
|
||||
@ -312,8 +316,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
[Pure]
|
||||
public long ComputeScore(ScoringMode mode, ScoreInfo scoreInfo)
|
||||
{
|
||||
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
|
||||
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
|
||||
if (!Ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
|
||||
throw new ArgumentException($"Unexpected score ruleset. Expected \"{Ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
|
||||
|
||||
extractScoringValues(scoreInfo, out var current, out var maximum);
|
||||
|
||||
@ -552,7 +556,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
break;
|
||||
|
||||
default:
|
||||
maxResult = maxBasicResult ??= ruleset.GetHitResults().MaxBy(kvp => Judgement.ToNumericResult(kvp.result)).result;
|
||||
maxResult = maxBasicResult ??= Ruleset.GetHitResults().MaxBy(kvp => Judgement.ToNumericResult(kvp.result)).result;
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -157,6 +157,8 @@ namespace osu.Game.Rulesets.UI
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void AddExtension(string extension) => throw new NotSupportedException();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (primary.IsNotNull()) primary.Dispose();
|
||||
|
17
osu.Game/Scoring/RankingTier.cs
Normal file
17
osu.Game/Scoring/RankingTier.cs
Normal file
@ -0,0 +1,17 @@
|
||||
// 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.Scoring
|
||||
{
|
||||
public enum RankingTier
|
||||
{
|
||||
Iron,
|
||||
Bronze,
|
||||
Silver,
|
||||
Gold,
|
||||
Platinum,
|
||||
Rhodium,
|
||||
Radiant,
|
||||
Lustrous
|
||||
}
|
||||
}
|
@ -439,7 +439,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
#region Selection Movement
|
||||
|
||||
private Vector2[] movementBlueprintOriginalPositions;
|
||||
private Vector2[][] movementBlueprintsOriginalPositions;
|
||||
private SelectionBlueprint<T>[] movementBlueprints;
|
||||
private bool isDraggingBlueprint;
|
||||
|
||||
@ -459,7 +459,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
// Movement is tracked from the blueprint of the earliest item, since it only makes sense to distance snap from that item
|
||||
movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).ToArray();
|
||||
movementBlueprintOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSelectionPoint).ToArray();
|
||||
movementBlueprintsOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSnapPoints).ToArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -480,26 +480,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
if (movementBlueprints == null)
|
||||
return false;
|
||||
|
||||
Debug.Assert(movementBlueprintOriginalPositions != null);
|
||||
Debug.Assert(movementBlueprintsOriginalPositions != null);
|
||||
|
||||
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
|
||||
|
||||
if (snapProvider != null)
|
||||
{
|
||||
// check for positional snap for every object in selection (for things like object-object snapping)
|
||||
for (int i = 0; i < movementBlueprintOriginalPositions.Length; i++)
|
||||
for (int i = 0; i < movementBlueprints.Length; i++)
|
||||
{
|
||||
Vector2 originalPosition = movementBlueprintOriginalPositions[i];
|
||||
var testPosition = originalPosition + distanceTravelled;
|
||||
|
||||
var positionalResult = snapProvider.FindSnappedPositionAndTime(testPosition, SnapType.NearbyObjects);
|
||||
|
||||
if (positionalResult.ScreenSpacePosition == testPosition) continue;
|
||||
|
||||
var delta = positionalResult.ScreenSpacePosition - movementBlueprints[i].ScreenSpaceSelectionPoint;
|
||||
|
||||
// attempt to move the objects, and abort any time based snapping if we can.
|
||||
if (SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(movementBlueprints[i], delta)))
|
||||
if (checkSnappingBlueprintToNearbyObjects(movementBlueprints[i], distanceTravelled, movementBlueprintsOriginalPositions[i]))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -508,7 +497,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
// item in the selection.
|
||||
|
||||
// The final movement position, relative to movementBlueprintOriginalPosition.
|
||||
Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled;
|
||||
Vector2 movePosition = movementBlueprintsOriginalPositions.First().First() + distanceTravelled;
|
||||
|
||||
// Retrieve a snapped position.
|
||||
var result = snapProvider?.FindSnappedPositionAndTime(movePosition, ~SnapType.NearbyObjects);
|
||||
@ -521,6 +510,36 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
return ApplySnapResult(movementBlueprints, result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check for positional snap for given blueprint.
|
||||
/// </summary>
|
||||
/// <param name="blueprint">The blueprint to check for snapping.</param>
|
||||
/// <param name="distanceTravelled">Distance travelled since start of dragging action.</param>
|
||||
/// <param name="originalPositions">The snap positions of blueprint before start of dragging action.</param>
|
||||
/// <returns>Whether an object to snap to was found.</returns>
|
||||
private bool checkSnappingBlueprintToNearbyObjects(SelectionBlueprint<T> blueprint, Vector2 distanceTravelled, Vector2[] originalPositions)
|
||||
{
|
||||
var currentPositions = blueprint.ScreenSpaceSnapPoints;
|
||||
|
||||
for (int i = 0; i < originalPositions.Length; i++)
|
||||
{
|
||||
Vector2 originalPosition = originalPositions[i];
|
||||
var testPosition = originalPosition + distanceTravelled;
|
||||
|
||||
var positionalResult = snapProvider.FindSnappedPositionAndTime(testPosition, SnapType.NearbyObjects);
|
||||
|
||||
if (positionalResult.ScreenSpacePosition == testPosition) continue;
|
||||
|
||||
var delta = positionalResult.ScreenSpacePosition - currentPositions[i];
|
||||
|
||||
// attempt to move the objects, and abort any time based snapping if we can.
|
||||
if (SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(blueprint, delta)))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected virtual bool ApplySnapResult(SelectionBlueprint<T>[] blueprints, SnapResult result) =>
|
||||
SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(blueprints.First(), result.ScreenSpacePosition - blueprints.First().ScreenSpaceSelectionPoint));
|
||||
|
||||
@ -533,7 +552,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
if (movementBlueprints == null)
|
||||
return false;
|
||||
|
||||
movementBlueprintOriginalPositions = null;
|
||||
movementBlueprintsOriginalPositions = null;
|
||||
movementBlueprints = null;
|
||||
|
||||
return true;
|
||||
|
@ -51,6 +51,7 @@ namespace osu.Game.Screens.Play
|
||||
private const float duration = 2500;
|
||||
|
||||
private ISample? failSample;
|
||||
private SampleChannel? failSampleChannel;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
@ -119,13 +120,13 @@ namespace osu.Game.Screens.Play
|
||||
this.TransformBindableTo(trackFreq, 0, duration).OnComplete(_ =>
|
||||
{
|
||||
// Don't reset frequency as the pause screen may appear post transform, causing a second frequency sweep.
|
||||
RemoveFilters(false);
|
||||
removeFilters(false);
|
||||
OnComplete?.Invoke();
|
||||
});
|
||||
|
||||
failHighPassFilter.CutoffTo(300);
|
||||
failLowPassFilter.CutoffTo(300, duration, Easing.OutCubic);
|
||||
failSample?.Play();
|
||||
failSampleChannel = failSample?.Play();
|
||||
|
||||
track.AddAdjustment(AdjustableProperty.Frequency, trackFreq);
|
||||
track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment);
|
||||
@ -153,7 +154,16 @@ namespace osu.Game.Screens.Play
|
||||
Background?.FadeColour(OsuColour.Gray(0.3f), 60);
|
||||
}
|
||||
|
||||
public void RemoveFilters(bool resetTrackFrequency = true)
|
||||
/// <summary>
|
||||
/// Stops any and all persistent effects added by the ongoing fail animation.
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
failSampleChannel?.Stop();
|
||||
removeFilters();
|
||||
}
|
||||
|
||||
private void removeFilters(bool resetTrackFrequency = true)
|
||||
{
|
||||
filtersRemoved = true;
|
||||
|
||||
|
@ -78,30 +78,39 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
public DefaultHealthDisplay()
|
||||
{
|
||||
Size = new Vector2(1, 5);
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Margin = new MarginPadding { Top = 20 };
|
||||
const float padding = 20;
|
||||
const float bar_height = 5;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
Size = new Vector2(1, bar_height + padding * 2);
|
||||
RelativeSizeAxes = Axes.X;
|
||||
|
||||
InternalChild = new Container
|
||||
{
|
||||
new Box
|
||||
Padding = new MarginPadding { Vertical = padding },
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black,
|
||||
},
|
||||
fill = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(0, 1),
|
||||
Masking = true,
|
||||
Children = new[]
|
||||
new Box
|
||||
{
|
||||
new Box
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black,
|
||||
},
|
||||
fill = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(0, 1),
|
||||
Masking = true,
|
||||
Children = new[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1072,7 +1072,7 @@ namespace osu.Game.Screens.Play
|
||||
public override bool OnExiting(ScreenExitEvent e)
|
||||
{
|
||||
screenSuspension?.RemoveAndDisposeImmediately();
|
||||
failAnimationLayer?.RemoveFilters();
|
||||
failAnimationLayer?.Stop();
|
||||
|
||||
if (LoadedBeatmapSuccessfully)
|
||||
{
|
||||
|
@ -30,14 +30,16 @@ namespace osu.Game.Screens.Select.Details
|
||||
{
|
||||
public partial class AdvancedStats : Container
|
||||
{
|
||||
[Resolved]
|
||||
private BeatmapDifficultyCache difficultyCache { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IBindable<IReadOnlyList<Mod>> mods { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; }
|
||||
private OsuGameBase game { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private BeatmapDifficultyCache difficultyCache { get; set; }
|
||||
private IBindable<RulesetInfo> gameRuleset;
|
||||
|
||||
protected readonly StatisticRow FirstValue, HpDrain, Accuracy, ApproachRate;
|
||||
private readonly StatisticRow starDifficulty;
|
||||
@ -84,7 +86,13 @@ namespace osu.Game.Screens.Select.Details
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
ruleset.BindValueChanged(_ => updateStatistics());
|
||||
// the cached ruleset bindable might be a decoupled bindable provided by SongSelect,
|
||||
// which we can't rely on in combination with the game-wide selected mods list,
|
||||
// since mods could be updated to the new ruleset instances while the decoupled bindable is held behind,
|
||||
// therefore resulting in performing difficulty calculation with invalid states.
|
||||
gameRuleset = game.Ruleset.GetBoundCopy();
|
||||
gameRuleset.BindValueChanged(_ => updateStatistics());
|
||||
|
||||
mods.BindValueChanged(modsChanged, true);
|
||||
}
|
||||
|
||||
@ -142,7 +150,14 @@ namespace osu.Game.Screens.Select.Details
|
||||
|
||||
private CancellationTokenSource starDifficultyCancellationSource;
|
||||
|
||||
private void updateStarDifficulty()
|
||||
/// <summary>
|
||||
/// Updates the displayed star difficulty statistics with the values provided by the currently-selected beatmap, ruleset, and selected mods.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is scheduled to avoid scenarios wherein a ruleset changes first before selected mods do,
|
||||
/// potentially resulting in failure during difficulty calculation due to incomplete bindable state updates.
|
||||
/// </remarks>
|
||||
private void updateStarDifficulty() => Scheduler.AddOnce(() =>
|
||||
{
|
||||
starDifficultyCancellationSource?.Cancel();
|
||||
|
||||
@ -151,8 +166,8 @@ namespace osu.Game.Screens.Select.Details
|
||||
|
||||
starDifficultyCancellationSource = new CancellationTokenSource();
|
||||
|
||||
var normalStarDifficultyTask = difficultyCache.GetDifficultyAsync(BeatmapInfo, ruleset.Value, null, starDifficultyCancellationSource.Token);
|
||||
var moddedStarDifficultyTask = difficultyCache.GetDifficultyAsync(BeatmapInfo, ruleset.Value, mods.Value, starDifficultyCancellationSource.Token);
|
||||
var normalStarDifficultyTask = difficultyCache.GetDifficultyAsync(BeatmapInfo, gameRuleset.Value, null, starDifficultyCancellationSource.Token);
|
||||
var moddedStarDifficultyTask = difficultyCache.GetDifficultyAsync(BeatmapInfo, gameRuleset.Value, mods.Value, starDifficultyCancellationSource.Token);
|
||||
|
||||
Task.WhenAll(normalStarDifficultyTask, moddedStarDifficultyTask).ContinueWith(_ => Schedule(() =>
|
||||
{
|
||||
@ -164,7 +179,7 @@ namespace osu.Game.Screens.Select.Details
|
||||
|
||||
starDifficulty.Value = ((float)normalDifficulty.Value.Stars, (float)moddedDifficulty.Value.Stars);
|
||||
}), starDifficultyCancellationSource.Token, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Current);
|
||||
}
|
||||
});
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
|
@ -104,6 +104,9 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
|
||||
protected override APIRequest? FetchScores(CancellationToken cancellationToken)
|
||||
{
|
||||
scoreRetrievalRequest?.Cancel();
|
||||
scoreRetrievalRequest = null;
|
||||
|
||||
var fetchBeatmapInfo = BeatmapInfo;
|
||||
|
||||
if (fetchBeatmapInfo == null)
|
||||
@ -152,8 +155,6 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
else if (filterMods)
|
||||
requestMods = mods.Value;
|
||||
|
||||
scoreRetrievalRequest?.Cancel();
|
||||
|
||||
var newRequest = new GetScoresRequest(fetchBeatmapInfo, fetchRuleset, Scope, requestMods);
|
||||
newRequest.Success += response => Schedule(() =>
|
||||
{
|
||||
|
@ -35,6 +35,7 @@ using osu.Game.Collections;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
@ -124,9 +125,20 @@ namespace osu.Game.Screens.Select
|
||||
[Resolved]
|
||||
internal IOverlayManager? OverlayManager { get; private set; }
|
||||
|
||||
private Bindable<bool> configBackgroundBlur { get; set; } = new BindableBool();
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(AudioManager audio, OsuColour colours, ManageCollectionsDialog? manageCollectionsDialog, DifficultyRecommender? recommender)
|
||||
private void load(AudioManager audio, OsuColour colours, ManageCollectionsDialog? manageCollectionsDialog, DifficultyRecommender? recommender, OsuConfigManager config)
|
||||
{
|
||||
configBackgroundBlur = config.GetBindable<bool>(OsuSetting.SongSelectBackgroundBlur);
|
||||
configBackgroundBlur.BindValueChanged(e =>
|
||||
{
|
||||
if (!this.IsCurrentScreen())
|
||||
return;
|
||||
|
||||
ApplyToBackground(b => b.BlurAmount.Value = e.NewValue ? BACKGROUND_BLUR : 0);
|
||||
});
|
||||
|
||||
LoadComponentAsync(Carousel = new BeatmapCarousel
|
||||
{
|
||||
AllowSelection = false, // delay any selection until our bindables are ready to make a good choice.
|
||||
@ -742,7 +754,7 @@ namespace osu.Game.Screens.Select
|
||||
ApplyToBackground(backgroundModeBeatmap =>
|
||||
{
|
||||
backgroundModeBeatmap.Beatmap = beatmap;
|
||||
backgroundModeBeatmap.BlurAmount.Value = BACKGROUND_BLUR;
|
||||
backgroundModeBeatmap.BlurAmount.Value = configBackgroundBlur.Value ? BACKGROUND_BLUR : 0f;
|
||||
backgroundModeBeatmap.FadeColour(Color4.White, 250);
|
||||
});
|
||||
|
||||
|
@ -11,12 +11,11 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
@ -24,10 +23,8 @@ using osu.Game.Resources.Localisation.Web;
|
||||
namespace osu.Game.Skinning.Components
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public partial class BeatmapAttributeText : Container, ISkinnableDrawable
|
||||
public partial class BeatmapAttributeText : FontAdjustableSkinComponent
|
||||
{
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
[SettingSource("Attribute", "The attribute to be displayed.")]
|
||||
public Bindable<BeatmapAttribute> Attribute { get; } = new Bindable<BeatmapAttribute>(BeatmapAttribute.StarRating);
|
||||
|
||||
@ -67,7 +64,6 @@ namespace osu.Game.Skinning.Components
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Font = OsuFont.Default.With(size: 40)
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -122,6 +118,8 @@ namespace osu.Game.Skinning.Components
|
||||
|
||||
text.Text = LocalisableString.Format(numberedTemplate, args);
|
||||
}
|
||||
|
||||
protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40);
|
||||
}
|
||||
|
||||
public enum BeatmapAttribute
|
||||
|
@ -4,7 +4,7 @@
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
@ -12,17 +12,16 @@ using osu.Game.Graphics.Sprites;
|
||||
namespace osu.Game.Skinning.Components
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public partial class TextElement : Container, ISkinnableDrawable
|
||||
public partial class TextElement : FontAdjustableSkinComponent
|
||||
{
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
[SettingSource("Text", "The text to be displayed.")]
|
||||
public Bindable<string> Text { get; } = new Bindable<string>("Circles!");
|
||||
|
||||
private readonly OsuSpriteText text;
|
||||
|
||||
public TextElement()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
OsuSpriteText text;
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
text = new OsuSpriteText
|
||||
@ -34,5 +33,7 @@ namespace osu.Game.Skinning.Components
|
||||
};
|
||||
text.Current.BindTo(Text);
|
||||
}
|
||||
|
||||
protected override void SetFont(FontUsage font) => text.Font = font.With(size: 40);
|
||||
}
|
||||
}
|
||||
|
41
osu.Game/Skinning/FontAdjustableSkinComponent.cs
Normal file
41
osu.Game/Skinning/FontAdjustableSkinComponent.cs
Normal file
@ -0,0 +1,41 @@
|
||||
// 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.Bindables;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// A skin component that contains text and allows the user to choose its font.
|
||||
/// </summary>
|
||||
public abstract partial class FontAdjustableSkinComponent : Container, ISkinnableDrawable
|
||||
{
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
|
||||
[SettingSource("Font", "The font to use.")]
|
||||
public Bindable<Typeface> Font { get; } = new Bindable<Typeface>(Typeface.Torus);
|
||||
|
||||
/// <summary>
|
||||
/// Implement to apply the user font selection to one or more components.
|
||||
/// </summary>
|
||||
protected abstract void SetFont(FontUsage font);
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Font.BindValueChanged(e =>
|
||||
{
|
||||
// We only have bold weight for venera, so let's force that.
|
||||
FontWeight fontWeight = e.NewValue == Typeface.Venera ? FontWeight.Bold : FontWeight.Regular;
|
||||
|
||||
FontUsage f = OsuFont.GetFont(e.NewValue, weight: fontWeight);
|
||||
SetFont(f);
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
}
|
84
osu.Game/Skinning/MaxDimensionLimitedTextureLoaderStore.cs
Normal file
84
osu.Game/Skinning/MaxDimensionLimitedTextureLoaderStore.cs
Normal file
@ -0,0 +1,84 @@
|
||||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.IO.Stores;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
public class MaxDimensionLimitedTextureLoaderStore : IResourceStore<TextureUpload>
|
||||
{
|
||||
private readonly IResourceStore<TextureUpload>? textureStore;
|
||||
|
||||
public MaxDimensionLimitedTextureLoaderStore(IResourceStore<TextureUpload>? textureStore)
|
||||
{
|
||||
this.textureStore = textureStore;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
textureStore?.Dispose();
|
||||
}
|
||||
|
||||
public TextureUpload Get(string name)
|
||||
{
|
||||
var textureUpload = textureStore?.Get(name);
|
||||
|
||||
// NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp.
|
||||
if (textureUpload == null)
|
||||
return null!;
|
||||
|
||||
return limitTextureUploadSize(textureUpload);
|
||||
}
|
||||
|
||||
public async Task<TextureUpload> GetAsync(string name, CancellationToken cancellationToken = new CancellationToken())
|
||||
{
|
||||
// NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp.
|
||||
if (textureStore == null)
|
||||
return null!;
|
||||
|
||||
var textureUpload = await textureStore.GetAsync(name, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (textureUpload == null)
|
||||
return null!;
|
||||
|
||||
return await Task.Run(() => limitTextureUploadSize(textureUpload), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private TextureUpload limitTextureUploadSize(TextureUpload textureUpload)
|
||||
{
|
||||
// So there's a thing where some users have taken it upon themselves to create skin elements of insane dimensions.
|
||||
// To the point where GPUs cannot load the textures (along with most image editor apps).
|
||||
// To work around this, let's look out for any stupid images and shrink them down into a usable size.
|
||||
const int max_supported_texture_size = 8192;
|
||||
|
||||
if (textureUpload.Height > max_supported_texture_size || textureUpload.Width > max_supported_texture_size)
|
||||
{
|
||||
var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height);
|
||||
|
||||
// The original texture upload will no longer be returned or used.
|
||||
textureUpload.Dispose();
|
||||
|
||||
image.Mutate(i => i.Resize(new Size(
|
||||
Math.Min(textureUpload.Width, max_supported_texture_size),
|
||||
Math.Min(textureUpload.Height, max_supported_texture_size)
|
||||
)));
|
||||
|
||||
return new TextureUpload(image);
|
||||
}
|
||||
|
||||
return textureUpload;
|
||||
}
|
||||
|
||||
public Stream? GetStream(string name) => textureStore?.GetStream(name);
|
||||
|
||||
public IEnumerable<string> GetAvailableResources() => textureStore?.GetAvailableResources() ?? Array.Empty<string>();
|
||||
}
|
||||
}
|
@ -69,15 +69,18 @@ namespace osu.Game.Skinning
|
||||
storage ??= realmBackedStorage = new RealmBackedResourceStore<SkinInfo>(SkinInfo, resources.Files, resources.RealmAccess);
|
||||
|
||||
var samples = resources.AudioManager?.GetSampleStore(storage);
|
||||
|
||||
if (samples != null)
|
||||
{
|
||||
samples.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY;
|
||||
|
||||
// osu-stable performs audio lookups in order of wav -> mp3 -> ogg.
|
||||
// The GetSampleStore() call above internally adds wav and mp3, so ogg is added at the end to ensure expected ordering.
|
||||
(storage as ResourceStore<byte[]>)?.AddExtension("ogg");
|
||||
// osu-stable performs audio lookups in order of wav -> mp3 -> ogg.
|
||||
// The GetSampleStore() call above internally adds wav and mp3, so ogg is added at the end to ensure expected ordering.
|
||||
samples.AddExtension(@"ogg");
|
||||
}
|
||||
|
||||
Samples = samples;
|
||||
Textures = new TextureStore(resources.Renderer, resources.CreateTextureLoaderStore(storage));
|
||||
Textures = new TextureStore(resources.Renderer, new MaxDimensionLimitedTextureLoaderStore(resources.CreateTextureLoaderStore(storage)));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -35,7 +35,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="10.18.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2023.120.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2023.131.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1221.0" />
|
||||
<PackageReference Include="Sentry" Version="3.23.1" />
|
||||
<PackageReference Include="SharpCompress" Version="0.32.2" />
|
||||
|
@ -16,6 +16,6 @@
|
||||
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.120.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.131.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
Loading…
Reference in New Issue
Block a user