1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-06 09:07:25 +08:00

Merge branch 'master' into sv_to_scrollspeed

This commit is contained in:
Dean Herbert 2022-10-27 11:29:56 +09:00 committed by GitHub
commit 73a9fac6d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
185 changed files with 3716 additions and 991 deletions

View File

@ -15,6 +15,8 @@ M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Gen
M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks. M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks.
P:System.Threading.Tasks.Task`1.Result;Don't use Task.Result. Use Task.GetResultSafely() to ensure we avoid deadlocks. P:System.Threading.Tasks.Task`1.Result;Don't use Task.Result. Use Task.GetResultSafely() to ensure we avoid deadlocks.
M:System.Threading.ManualResetEventSlim.Wait();Specify a timeout to avoid waiting forever. M:System.Threading.ManualResetEventSlim.Wait();Specify a timeout to avoid waiting forever.
M:System.Char.ToLower(System.Char);char.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use char.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture.
M:System.Char.ToUpper(System.Char);char.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use char.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture.
M:System.String.ToLower();string.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString. M:System.String.ToLower();string.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString.
M:System.String.ToUpper();string.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString. M:System.String.ToUpper();string.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString.
M:Humanizer.InflectorExtensions.Pascalize(System.String);Humanizer's .Pascalize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToPascalCase() instead. M:Humanizer.InflectorExtensions.Pascalize(System.String);Humanizer's .Pascalize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToPascalCase() instead.

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1008.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.1021.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.1011.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.1022.1" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -0,0 +1,23 @@
// 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 NUnit.Framework;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests.Mods
{
public class TestSceneCatchModFlashlight : ModTestScene
{
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
[TestCase(1f)]
[TestCase(0.5f)]
[TestCase(1.25f)]
[TestCase(1.5f)]
public void TestSizeMultiplier(float sizeMultiplier) => CreateModTest(new ModTestData { Mod = new CatchModFlashlight { SizeMultiplier = { Value = sizeMultiplier } }, PassCondition = () => true });
[Test]
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new CatchModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
}
}

View File

@ -1,10 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
@ -12,6 +12,8 @@ using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -19,15 +21,28 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
public class TestSceneComboCounter : CatchSkinnableTestScene public class TestSceneComboCounter : CatchSkinnableTestScene
{ {
private ScoreProcessor scoreProcessor; private ScoreProcessor scoreProcessor = null!;
private Color4 judgedObjectColour = Color4.White; private Color4 judgedObjectColour = Color4.White;
private readonly Bindable<bool> showHud = new Bindable<bool>(true);
[BackgroundDependencyLoader]
private void load()
{
Dependencies.CacheAs<Player>(new TestPlayer
{
ShowingOverlayComponents = { BindTarget = showHud },
});
}
[SetUp] [SetUp]
public void SetUp() => Schedule(() => public void SetUp() => Schedule(() =>
{ {
scoreProcessor = new ScoreProcessor(new CatchRuleset()); scoreProcessor = new ScoreProcessor(new CatchRuleset());
showHud.Value = true;
SetContents(_ => new CatchComboDisplay SetContents(_ => new CatchComboDisplay
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
@ -51,9 +66,15 @@ namespace osu.Game.Rulesets.Catch.Tests
1f 1f
); );
}); });
AddStep("set hud to never show", () => showHud.Value = false);
AddRepeatStep("perform hit", () => performJudgement(HitResult.Great), 5);
AddStep("set hud to show", () => showHud.Value = true);
AddRepeatStep("perform hit", () => performJudgement(HitResult.Great), 5);
} }
private void performJudgement(HitResult type, Judgement judgement = null) private void performJudgement(HitResult type, Judgement? judgement = null)
{ {
var judgedObject = new DrawableFruit(new Fruit()) { AccentColour = { Value = judgedObjectColour } }; var judgedObject = new DrawableFruit(new Fruit()) { AccentColour = { Value = judgedObjectColour } };

View File

@ -36,5 +36,7 @@ namespace osu.Game.Rulesets.Catch.Edit
return base.CreateHitObjectBlueprintFor(hitObject); return base.CreateHitObjectBlueprintFor(hitObject);
} }
protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
} }
} }

View File

@ -0,0 +1,49 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit
{
public class CatchEditorPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
{
protected override Container<Drawable> Content => content;
private readonly Container content;
public CatchEditorPlayfieldAdjustmentContainer()
{
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
Size = new Vector2(0.8f, 0.9f);
InternalChild = new ScalingContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Child = content = new Container { RelativeSizeAxes = Axes.Both },
};
}
private class ScalingContainer : Container
{
public ScalingContainer()
{
RelativeSizeAxes = Axes.Y;
Width = CatchPlayfield.WIDTH;
}
protected override void Update()
{
base.Update();
Scale = new Vector2(Math.Min(Parent.ChildSize.X / CatchPlayfield.WIDTH, Parent.ChildSize.Y / CatchPlayfield.HEIGHT));
Height = 1 / Scale.Y;
}
}
}
}

View File

@ -3,6 +3,7 @@
#nullable disable #nullable disable
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
@ -10,10 +11,11 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
@ -21,7 +23,6 @@ using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK; using osuTK;
@ -33,10 +34,14 @@ namespace osu.Game.Rulesets.Catch.Edit
private CatchDistanceSnapGrid distanceSnapGrid; private CatchDistanceSnapGrid distanceSnapGrid;
private readonly Bindable<TernaryState> distanceSnapToggle = new Bindable<TernaryState>();
private InputManager inputManager; private InputManager inputManager;
private readonly BindableDouble timeRangeMultiplier = new BindableDouble(1)
{
MinValue = 1,
MaxValue = 10,
};
public CatchHitObjectComposer(CatchRuleset ruleset) public CatchHitObjectComposer(CatchRuleset ruleset)
: base(ruleset) : base(ruleset)
{ {
@ -51,7 +56,10 @@ namespace osu.Game.Rulesets.Catch.Edit
LayerBelowRuleset.Add(new PlayfieldBorder LayerBelowRuleset.Add(new PlayfieldBorder
{ {
RelativeSizeAxes = Axes.Both, Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.X,
Height = CatchPlayfield.HEIGHT,
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners } PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
}); });
@ -70,6 +78,19 @@ namespace osu.Game.Rulesets.Catch.Edit
inputManager = GetContainingInputManager(); inputManager = GetContainingInputManager();
} }
protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
{
// osu!catch's distance snap implementation is limited, in that a custom spacing cannot be specified.
// Therefore this functionality is not currently used.
//
// The implementation below is probably correct but should be checked if/when exposed via controls.
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
float actualDistance = Math.Abs(((CatchHitObject)before).EffectiveX - ((CatchHitObject)after).EffectiveX);
return actualDistance / expectedDistance;
}
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
@ -77,8 +98,30 @@ namespace osu.Game.Rulesets.Catch.Edit
updateDistanceSnapGrid(); updateDistanceSnapGrid();
} }
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
switch (e.Action)
{
// Note that right now these are hard to use as the default key bindings conflict with existing editor key bindings.
// In the future we will want to expose this via UI and potentially change the key bindings to be editor-specific.
// May be worth considering standardising "zoom" behaviour with what the timeline uses (ie. alt-wheel) but that may cause new conflicts.
case GlobalAction.IncreaseScrollSpeed:
this.TransformBindableTo(timeRangeMultiplier, timeRangeMultiplier.Value - 1, 200, Easing.OutQuint);
break;
case GlobalAction.DecreaseScrollSpeed:
this.TransformBindableTo(timeRangeMultiplier, timeRangeMultiplier.Value + 1, 200, Easing.OutQuint);
break;
}
return base.OnPressed(e);
}
protected override DrawableRuleset<CatchHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => protected override DrawableRuleset<CatchHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) =>
new DrawableCatchEditorRuleset(ruleset, beatmap, mods); new DrawableCatchEditorRuleset(ruleset, beatmap, mods)
{
TimeRangeMultiplier = { BindTarget = timeRangeMultiplier, }
};
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[] protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
{ {
@ -87,11 +130,6 @@ namespace osu.Game.Rulesets.Catch.Edit
new BananaShowerCompositionTool() new BananaShowerCompositionTool()
}; };
protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
{
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
});
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
{ {
var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType); var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
@ -163,7 +201,7 @@ namespace osu.Game.Rulesets.Catch.Edit
private void updateDistanceSnapGrid() private void updateDistanceSnapGrid()
{ {
if (distanceSnapToggle.Value != TernaryState.True) if (DistanceSnapToggle.Value != TernaryState.True)
{ {
distanceSnapGrid.Hide(); distanceSnapGrid.Hide();
return; return;

View File

@ -4,6 +4,7 @@
#nullable disable #nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Bindables;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -13,11 +14,24 @@ namespace osu.Game.Rulesets.Catch.Edit
{ {
public class DrawableCatchEditorRuleset : DrawableCatchRuleset public class DrawableCatchEditorRuleset : DrawableCatchRuleset
{ {
public readonly BindableDouble TimeRangeMultiplier = new BindableDouble(1);
public DrawableCatchEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) public DrawableCatchEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
: base(ruleset, beatmap, mods) : base(ruleset, beatmap, mods)
{ {
} }
protected override void Update()
{
base.Update();
double gamePlayTimeRange = GetTimeRange(Beatmap.Difficulty.ApproachRate);
float playfieldStretch = Playfield.DrawHeight / CatchPlayfield.HEIGHT;
TimeRange.Value = gamePlayTimeRange * TimeRangeMultiplier.Value * playfieldStretch;
}
protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.Difficulty); protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.Difficulty);
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchEditorPlayfieldAdjustmentContainer();
} }
} }

View File

@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
this.playfield = playfield; this.playfield = playfield;
FlashlightSize = new Vector2(0, GetSizeFor(0)); FlashlightSize = new Vector2(0, GetSize());
FlashlightSmoothness = 1.4f; FlashlightSmoothness = 1.4f;
} }
@ -66,9 +66,9 @@ namespace osu.Game.Rulesets.Catch.Mods
FlashlightPosition = playfield.CatcherArea.ToSpaceOfOtherDrawable(playfield.Catcher.DrawPosition, this); FlashlightPosition = playfield.CatcherArea.ToSpaceOfOtherDrawable(playfield.Catcher.DrawPosition, this);
} }
protected override void OnComboChange(ValueChangedEvent<int> e) protected override void UpdateFlashlightSize(float size)
{ {
this.TransformTo(nameof(FlashlightSize), new Vector2(0, GetSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); this.TransformTo(nameof(FlashlightSize), new Vector2(0, size), FLASHLIGHT_FADE_DURATION);
} }
protected override string FragmentShader => "CircularFlashlight"; protected override string FragmentShader => "CircularFlashlight";

View File

@ -19,17 +19,20 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public override LocalisableString Description => @"Use the mouse to control the catcher."; public override LocalisableString Description => @"Use the mouse to control the catcher.";
private DrawableRuleset<CatchHitObject> drawableRuleset = null!; private DrawableCatchRuleset drawableRuleset = null!;
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
{ {
this.drawableRuleset = drawableRuleset; this.drawableRuleset = (DrawableCatchRuleset)drawableRuleset;
} }
public void ApplyToPlayer(Player player) public void ApplyToPlayer(Player player)
{ {
if (!drawableRuleset.HasReplayLoaded.Value) if (!drawableRuleset.HasReplayLoaded.Value)
drawableRuleset.Cursor.Add(new MouseInputHelper((CatchPlayfield)drawableRuleset.Playfield)); {
var catchPlayfield = (CatchPlayfield)drawableRuleset.Playfield;
catchPlayfield.CatcherArea.Add(new MouseInputHelper(catchPlayfield.CatcherArea));
}
} }
private class MouseInputHelper : Drawable, IKeyBindingHandler<CatchAction>, IRequireHighFrequencyMousePosition private class MouseInputHelper : Drawable, IKeyBindingHandler<CatchAction>, IRequireHighFrequencyMousePosition
@ -38,9 +41,10 @@ namespace osu.Game.Rulesets.Catch.Mods
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
public MouseInputHelper(CatchPlayfield playfield) public MouseInputHelper(CatcherArea catcherArea)
{ {
catcherArea = playfield.CatcherArea; this.catcherArea = catcherArea;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }

View File

@ -13,6 +13,10 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{ {
public class CatchLegacySkinTransformer : LegacySkinTransformer public class CatchLegacySkinTransformer : LegacySkinTransformer
{ {
public override bool IsProvidingLegacyResources => base.IsProvidingLegacyResources || hasPear;
private bool hasPear => GetTexture("fruit-pear") != null;
/// <summary> /// <summary>
/// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default. /// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default.
/// </summary> /// </summary>
@ -49,7 +53,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
switch (catchSkinComponent.Component) switch (catchSkinComponent.Component)
{ {
case CatchSkinComponents.Fruit: case CatchSkinComponents.Fruit:
if (GetTexture("fruit-pear") != null) if (hasPear)
return new LegacyFruitPiece(); return new LegacyFruitPiece();
return null; return null;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;

View File

@ -1,12 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable using osu.Framework.Allocation;
using osu.Framework.Bindables;
using JetBrains.Annotations; using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK.Graphics; using osuTK.Graphics;
@ -19,14 +20,29 @@ namespace osu.Game.Rulesets.Catch.UI
{ {
private int currentCombo; private int currentCombo;
[CanBeNull] public ICatchComboCounter? ComboCounter => Drawable as ICatchComboCounter;
public ICatchComboCounter ComboCounter => Drawable as ICatchComboCounter;
private readonly IBindable<bool> showCombo = new BindableBool(true);
public CatchComboDisplay() public CatchComboDisplay()
: base(new CatchSkinComponent(CatchSkinComponents.CatchComboCounter), _ => Empty()) : base(new CatchSkinComponent(CatchSkinComponents.CatchComboCounter), _ => Empty())
{ {
} }
[Resolved(canBeNull: true)]
private Player? player { get; set; }
protected override void LoadComplete()
{
base.LoadComplete();
if (player != null)
{
showCombo.BindTo(player.ShowingOverlayComponents);
showCombo.BindValueChanged(s => this.FadeTo(s.NewValue ? 1 : 0, HUDOverlay.FADE_DURATION, HUDOverlay.FADE_EASING), true);
}
}
protected override void SkinChanged(ISkinSource skin) protected override void SkinChanged(ISkinSource skin)
{ {
base.SkinChanged(skin); base.SkinChanged(skin);

View File

@ -23,6 +23,12 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary> /// </summary>
public const float WIDTH = 512; public const float WIDTH = 512;
/// <summary>
/// The height of the playfield.
/// This doesn't include the catcher area.
/// </summary>
public const float HEIGHT = 384;
/// <summary> /// <summary>
/// The center position of the playfield. /// The center position of the playfield.
/// </summary> /// </summary>

View File

@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.UI
: base(ruleset, beatmap, mods) : base(ruleset, beatmap, mods)
{ {
Direction.Value = ScrollingDirection.Down; Direction.Value = ScrollingDirection.Down;
TimeRange.Value = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450); TimeRange.Value = GetTimeRange(beatmap.Difficulty.ApproachRate);
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -42,6 +42,8 @@ namespace osu.Game.Rulesets.Catch.UI
KeyBindingInputManager.Add(new CatchTouchInputMapper()); KeyBindingInputManager.Add(new CatchTouchInputMapper());
} }
protected double GetTimeRange(float approachRate) => IBeatmapDifficultyInfo.DifficultyRange(approachRate, 1800, 1200, 450);
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay); protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay);
protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield); protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield);

View File

@ -0,0 +1,23 @@
// 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 NUnit.Framework;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Mods
{
public class TestSceneManiaModFlashlight : ModTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
[TestCase(1f)]
[TestCase(0.5f)]
[TestCase(1.5f)]
[TestCase(3f)]
public void TestSizeMultiplier(float sizeMultiplier) => CreateModTest(new ModTestData { Mod = new ManiaModFlashlight { SizeMultiplier = { Value = sizeMultiplier } }, PassCondition = () => true });
[Test]
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new ManiaModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
}
}

View File

@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Tests
assertHeadJudgement(HitResult.Miss); assertHeadJudgement(HitResult.Miss);
assertTickJudgement(HitResult.LargeTickMiss); assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss); assertTailJudgement(HitResult.Miss);
assertNoteJudgement(HitResult.IgnoreHit); assertNoteJudgement(HitResult.IgnoreMiss);
} }
/// <summary> /// <summary>

View File

@ -76,8 +76,8 @@ namespace osu.Game.Rulesets.Mania.Tests
performTest(objects, new List<ReplayFrame>()); performTest(objects, new List<ReplayFrame>());
addJudgementAssert(objects[0], HitResult.IgnoreHit); addJudgementAssert(objects[0], HitResult.IgnoreMiss);
addJudgementAssert(objects[1], HitResult.IgnoreHit); addJudgementAssert(objects[1], HitResult.IgnoreMiss);
} }
[Test] [Test]

View File

@ -33,5 +33,7 @@ namespace osu.Game.Rulesets.Mania.Edit
} }
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new ManiaSelectionHandler(); protected override SelectionHandler<HitObject> CreateSelectionHandler() => new ManiaSelectionHandler();
protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
} }
} }

View File

@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public ManiaFlashlight(ManiaModFlashlight modFlashlight) public ManiaFlashlight(ManiaModFlashlight modFlashlight)
: base(modFlashlight) : base(modFlashlight)
{ {
FlashlightSize = new Vector2(DrawWidth, GetSizeFor(0)); FlashlightSize = new Vector2(DrawWidth, GetSize());
AddLayout(flashlightProperties); AddLayout(flashlightProperties);
} }
@ -54,9 +54,9 @@ namespace osu.Game.Rulesets.Mania.Mods
} }
} }
protected override void OnComboChange(ValueChangedEvent<int> e) protected override void UpdateFlashlightSize(float size)
{ {
this.TransformTo(nameof(FlashlightSize), new Vector2(DrawWidth, GetSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); this.TransformTo(nameof(FlashlightSize), new Vector2(DrawWidth, size), FLASHLIGHT_FADE_DURATION);
} }
protected override string FragmentShader => "RectangularFlashlight"; protected override string FragmentShader => "RectangularFlashlight";

View File

@ -69,6 +69,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary> /// </summary>
private double? releaseTime; private double? releaseTime;
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
public DrawableHoldNote() public DrawableHoldNote()
: this(null) : this(null)
{ {
@ -260,7 +262,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
tick.MissForcefully(); tick.MissForcefully();
} }
ApplyResult(r => r.Type = r.Judgement.MaxResult); ApplyResult(r => r.Type = Tail.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
endHold(); endHold();
} }

View File

@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{ {
public class ManiaLegacySkinTransformer : LegacySkinTransformer public class ManiaLegacySkinTransformer : LegacySkinTransformer
{ {
public override bool IsProvidingLegacyResources => base.IsProvidingLegacyResources || hasKeyTexture.Value;
/// <summary> /// <summary>
/// Mapping of <see cref="HitResult"/> to their corresponding /// Mapping of <see cref="HitResult"/> to their corresponding
/// <see cref="LegacyManiaSkinConfigurationLookups"/> value. /// <see cref="LegacyManiaSkinConfigurationLookups"/> value.

View File

@ -4,14 +4,18 @@
#nullable disable #nullable disable
using System; using System;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Overlays;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Edit;
@ -33,6 +37,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[Cached(typeof(IBeatSnapProvider))] [Cached(typeof(IBeatSnapProvider))]
private readonly EditorBeatmap editorBeatmap; private readonly EditorBeatmap editorBeatmap;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
[Cached] [Cached]
private readonly EditorClock editorClock; private readonly EditorClock editorClock;
@ -48,6 +55,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
}; };
private OsuDistanceSnapGrid grid; private OsuDistanceSnapGrid grid;
private SnappingCursorContainer cursor;
public TestSceneOsuDistanceSnapGrid() public TestSceneOsuDistanceSnapGrid()
{ {
@ -84,8 +92,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4.SlateGray Colour = Color4.SlateGray
}, },
cursor = new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position },
grid = new OsuDistanceSnapGrid(new HitCircle { Position = grid_position }), grid = new OsuDistanceSnapGrid(new HitCircle { Position = grid_position }),
new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position }
}; };
}); });
@ -150,6 +158,37 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertSnappedDistance(expectedDistance); assertSnappedDistance(expectedDistance);
} }
[Test]
public void TestReferenceObjectNotOnSnapGrid()
{
AddStep("create grid", () =>
{
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.SlateGray
},
cursor = new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position },
grid = new OsuDistanceSnapGrid(new HitCircle
{
Position = grid_position,
// This is important. It sets the reference object to a point in time that isn't on the current snap divisor's grid.
// We are testing that the grid's display is offset correctly.
StartTime = 40,
}),
};
});
AddStep("move mouse to point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2(beat_length, 0) * 2)));
AddAssert("Ensure cursor is on a grid line", () =>
{
return grid.ChildrenOfType<CircularProgress>().Any(p => Precision.AlmostEquals(p.ScreenSpaceDrawQuad.TopRight.X, grid.ToScreenSpace(cursor.LastSnappedPosition).X));
});
}
[Test] [Test]
public void TestLimitedDistance() public void TestLimitedDistance()
{ {
@ -162,8 +201,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4.SlateGray Colour = Color4.SlateGray
}, },
cursor = new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position },
grid = new OsuDistanceSnapGrid(new HitCircle { Position = grid_position }, new HitCircle { StartTime = 200 }), grid = new OsuDistanceSnapGrid(new HitCircle { Position = grid_position }, new HitCircle { StartTime = 200 }),
new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position }
}; };
}); });
@ -182,6 +221,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{ {
public Func<Vector2, Vector2> GetSnapPosition; public Func<Vector2, Vector2> GetSnapPosition;
public Vector2 LastSnappedPosition { get; private set; }
private readonly Drawable cursor; private readonly Drawable cursor;
private InputManager inputManager; private InputManager inputManager;
@ -210,7 +251,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
cursor.Position = GetSnapPosition.Invoke(inputManager.CurrentState.Mouse.Position); cursor.Position = LastSnappedPosition = GetSnapPosition.Invoke(inputManager.CurrentState.Mouse.Position);
} }
} }
} }

View File

@ -20,20 +20,49 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
[Test] [Test]
public void TestGridExclusivity() public void TestGridToggles()
{ {
AddStep("enable distance snap grid", () => InputManager.Key(Key.T)); AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1))); AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any()); AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
rectangularGridActive(false); rectangularGridActive(false);
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y)); AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
AddUntilStep("distance snap grid still visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
rectangularGridActive(true); rectangularGridActive(true);
AddStep("enable distance snap grid", () => InputManager.Key(Key.T)); AddStep("disable distance snap grid", () => InputManager.Key(Key.T));
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1))); AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
rectangularGridActive(true);
AddStep("disable rectangular grid", () => InputManager.Key(Key.Y));
AddUntilStep("distance snap grid still hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
rectangularGridActive(false);
}
[Test]
public void TestDistanceSnapMomentaryToggle()
{
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("hold alt", () => InputManager.PressKey(Key.AltLeft));
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any()); AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
}
[Test]
public void TestGridSnapMomentaryToggle()
{
rectangularGridActive(false);
AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
rectangularGridActive(true);
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
rectangularGridActive(false); rectangularGridActive(false);
} }
@ -50,8 +79,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("placement blueprint at (0, 0)", () => Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position, new Vector2(0, 0))); AddAssert("placement blueprint at (0, 0)", () => Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position, new Vector2(0, 0)));
else else
AddAssert("placement blueprint at (1, 1)", () => Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position, new Vector2(1, 1))); AddAssert("placement blueprint at (1, 1)", () => Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position, new Vector2(1, 1)));
AddStep("choose selection tool", () => InputManager.Key(Key.Number1));
} }
[Test] [Test]

View File

@ -0,0 +1,25 @@
// 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 NUnit.Framework;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModFlashlight : OsuModTestScene
{
[TestCase(600)]
[TestCase(120)]
[TestCase(1200)]
public void TestFollowDelay(double followDelay) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { FollowDelay = { Value = followDelay } }, PassCondition = () => true });
[TestCase(1f)]
[TestCase(0.5f)]
[TestCase(1.5f)]
[TestCase(2f)]
public void TestSizeMultiplier(float sizeMultiplier) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { SizeMultiplier = { Value = sizeMultiplier } }, PassCondition = () => true });
[Test]
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
}
}

View File

@ -0,0 +1,22 @@
// 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 NUnit.Framework;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModFreezeFrame : OsuModTestScene
{
[Test]
public void TestFreezeFrame()
{
CreateModTest(new ModTestData
{
Mod = new OsuModFreezeFrame(),
PassCondition = () => true,
Autoplay = false,
});
}
}
}

View File

@ -4,6 +4,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -12,6 +13,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods namespace osu.Game.Rulesets.Osu.Tests.Mods
@ -145,6 +147,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
private bool isBreak() => Player.IsBreakTime.Value; private bool isBreak() => Player.IsBreakTime.Value;
private bool cursorAlphaAlmostEquals(float alpha) => Precision.AlmostEquals(Player.DrawableRuleset.Cursor.Alpha, alpha, 0.1f); private OsuPlayfield playfield => (OsuPlayfield)Player.DrawableRuleset.Playfield;
private bool cursorAlphaAlmostEquals(float alpha) =>
Precision.AlmostEquals(playfield.Cursor.AsNonNull().Alpha, alpha, 0.1f) &&
Precision.AlmostEquals(playfield.Smoke.Alpha, alpha, 0.1f);
} }
} }

View File

@ -1,21 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneOsuFlashlight : TestSceneOsuPlayer
{
protected override TestPlayer CreatePlayer(Ruleset ruleset)
{
SelectedMods.Value = new Mod[] { new OsuModAutoplay(), new OsuModFlashlight(), };
return base.CreatePlayer(ruleset);
}
}
}

View File

@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning.Legacy;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
@ -68,10 +69,8 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("create slider", () => AddStep("create slider", () =>
{ {
var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo()); var skin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo());
tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1"; var provider = Ruleset.Value.CreateInstance().CreateSkinTransformer(skin, Beatmap.Value.Beatmap);
var provider = Ruleset.Value.CreateInstance().CreateSkinTransformer(tintingSkin, Beatmap.Value.Beatmap);
Child = new SkinProvidingContainer(provider) Child = new SkinProvidingContainer(provider)
{ {
@ -92,10 +91,10 @@ namespace osu.Game.Rulesets.Osu.Tests
}); });
AddStep("set accent white", () => dho.AccentColour.Value = Color4.White); AddStep("set accent white", () => dho.AccentColour.Value = Color4.White);
AddAssert("ball is white", () => dho.ChildrenOfType<DrawableSliderBall>().Single().AccentColour == Color4.White); AddAssert("ball is white", () => dho.ChildrenOfType<LegacySliderBall>().Single().BallColour == Color4.White);
AddStep("set accent red", () => dho.AccentColour.Value = Color4.Red); AddStep("set accent red", () => dho.AccentColour.Value = Color4.Red);
AddAssert("ball is red", () => dho.ChildrenOfType<DrawableSliderBall>().Single().AccentColour == Color4.Red); AddAssert("ball is red", () => dho.ChildrenOfType<LegacySliderBall>().Single().BallColour == Color4.Red);
} }
private Slider prepareObject(Slider slider) private Slider prepareObject(Slider slider)

View File

@ -13,8 +13,11 @@ using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -44,12 +47,10 @@ namespace osu.Game.Rulesets.Osu.Edit
new SpinnerCompositionTool() new SpinnerCompositionTool()
}; };
private readonly Bindable<TernaryState> distanceSnapToggle = new Bindable<TernaryState>();
private readonly Bindable<TernaryState> rectangularGridSnapToggle = new Bindable<TernaryState>(); private readonly Bindable<TernaryState> rectangularGridSnapToggle = new Bindable<TernaryState>();
protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[] protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
{ {
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }),
new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Th }) new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Th })
}); });
@ -60,6 +61,9 @@ namespace osu.Game.Rulesets.Osu.Edit
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
// Give a bit of breathing room around the playfield content.
PlayfieldContentContainer.Padding = new MarginPadding(10);
LayerBelowRuleset.AddRange(new Drawable[] LayerBelowRuleset.AddRange(new Drawable[]
{ {
distanceSnapGridContainer = new Container distanceSnapGridContainer = new Container
@ -77,19 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit
placementObject = EditorBeatmap.PlacementObject.GetBoundCopy(); placementObject = EditorBeatmap.PlacementObject.GetBoundCopy();
placementObject.ValueChanged += _ => updateDistanceSnapGrid(); placementObject.ValueChanged += _ => updateDistanceSnapGrid();
distanceSnapToggle.ValueChanged += _ => DistanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid();
{
updateDistanceSnapGrid();
if (distanceSnapToggle.Value == TernaryState.True)
rectangularGridSnapToggle.Value = TernaryState.False;
};
rectangularGridSnapToggle.ValueChanged += _ =>
{
if (rectangularGridSnapToggle.Value == TernaryState.True)
distanceSnapToggle.Value = TernaryState.False;
};
// we may be entering the screen with a selection already active // we may be entering the screen with a selection already active
updateDistanceSnapGrid(); updateDistanceSnapGrid();
@ -109,6 +101,14 @@ namespace osu.Game.Rulesets.Osu.Edit
private RectangularPositionSnapGrid rectangularPositionSnapGrid; private RectangularPositionSnapGrid rectangularPositionSnapGrid;
protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
{
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position);
return actualDistance / expectedDistance;
}
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
@ -129,24 +129,46 @@ namespace osu.Game.Rulesets.Osu.Edit
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
{ {
if (snapType.HasFlagFast(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) if (snapType.HasFlagFast(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
{
// In the case of snapping to nearby objects, a time value is not provided.
// This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap
// this could result in unexpected behaviour when distance snapping is turned on and a user attempts to place an object that is
// BOTH on a valid distance snap ring, and also at the same position as a previous object.
//
// We want to ensure that in this particular case, the time-snapping component of distance snap is still applied.
// The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over
// the time value if the proposed positions are roughly the same.
if (snapType.HasFlagFast(SnapType.Grids) && DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{
(Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition));
if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1))
snapResult.Time = distanceSnappedTime;
}
return snapResult; return snapResult;
}
SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
if (snapType.HasFlagFast(SnapType.Grids)) if (snapType.HasFlagFast(SnapType.Grids))
{ {
if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) if (DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{ {
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
result.ScreenSpacePosition = distanceSnapGrid.ToScreenSpace(pos);
result.Time = time;
} }
if (rectangularGridSnapToggle.Value == TernaryState.True) if (rectangularGridSnapToggle.Value == TernaryState.True)
{ {
Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition)); Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(result.ScreenSpacePosition));
return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition));
result.ScreenSpacePosition = rectangularPositionSnapGrid.ToScreenSpace(pos);
} }
} }
return base.FindSnappedPositionAndTime(screenSpacePosition, snapType); return result;
} }
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult) private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
@ -199,7 +221,7 @@ namespace osu.Game.Rulesets.Osu.Edit
distanceSnapGridCache.Invalidate(); distanceSnapGridCache.Invalidate();
distanceSnapGrid = null; distanceSnapGrid = null;
if (distanceSnapToggle.Value != TernaryState.True) if (DistanceSnapToggle.Value != TernaryState.True)
return; return;
switch (BlueprintContainer.CurrentTool) switch (BlueprintContainer.CurrentTool)
@ -226,6 +248,42 @@ namespace osu.Game.Rulesets.Osu.Edit
} }
} }
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Repeat)
return false;
handleToggleViaKey(e);
return base.OnKeyDown(e);
}
protected override void OnKeyUp(KeyUpEvent e)
{
handleToggleViaKey(e);
base.OnKeyUp(e);
}
protected override bool AdjustDistanceSpacing(GlobalAction action, float amount)
{
// To allow better visualisation, ensure that the spacing grid is visible before adjusting.
DistanceSnapToggle.Value = TernaryState.True;
return base.AdjustDistanceSpacing(action, amount);
}
private bool gridSnapMomentary;
private void handleToggleViaKey(KeyboardEvent key)
{
bool shiftPressed = key.ShiftPressed;
if (shiftPressed != gridSnapMomentary)
{
gridSnapMomentary = shiftPressed;
rectangularGridSnapToggle.Value = rectangularGridSnapToggle.Value == TernaryState.False ? TernaryState.True : TernaryState.False;
}
}
private DistanceSnapGrid createDistanceSnapGrid(IEnumerable<HitObject> selectedHitObjects) private DistanceSnapGrid createDistanceSnapGrid(IEnumerable<HitObject> selectedHitObjects)
{ {
if (BlueprintContainer.CurrentTool is SpinnerCompositionTool) if (BlueprintContainer.CurrentTool is SpinnerCompositionTool)

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle; public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle;
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) }; public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModFreezeFrame) };
[SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 0)] [SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 0)]
public BindableFloat Scale { get; } = new BindableFloat(4) public BindableFloat Scale { get; } = new BindableFloat(4)

View File

@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
followDelay = modFlashlight.FollowDelay.Value; followDelay = modFlashlight.FollowDelay.Value;
FlashlightSize = new Vector2(0, GetSizeFor(0)); FlashlightSize = new Vector2(0, GetSize());
FlashlightSmoothness = 1.4f; FlashlightSmoothness = 1.4f;
} }
@ -83,9 +83,9 @@ namespace osu.Game.Rulesets.Osu.Mods
return base.OnMouseMove(e); return base.OnMouseMove(e);
} }
protected override void OnComboChange(ValueChangedEvent<int> e) protected override void UpdateFlashlightSize(float size)
{ {
this.TransformTo(nameof(FlashlightSize), new Vector2(0, GetSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); this.TransformTo(nameof(FlashlightSize), new Vector2(0, size), FLASHLIGHT_FADE_DURATION);
} }
protected override string FragmentShader => "CircularFlashlight"; protected override string FragmentShader => "CircularFlashlight";

View File

@ -0,0 +1,89 @@
// 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.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModFreezeFrame : Mod, IApplicableToDrawableHitObject, IApplicableToBeatmap
{
public override string Name => "Freeze Frame";
public override string Acronym => "FR";
public override double ScoreMultiplier => 1;
public override LocalisableString Description => "Burn the notes into your memory.";
//Alters the transforms of the approach circles, breaking the effects of these mods.
public override Type[] IncompatibleMods => new[] { typeof(OsuModApproachDifferent) };
public override ModType Type => ModType.Fun;
//mod breaks normal approach circle preempt
private double originalPreempt;
public void ApplyToBeatmap(IBeatmap beatmap)
{
var firstHitObject = beatmap.HitObjects.OfType<OsuHitObject>().FirstOrDefault();
if (firstHitObject == null)
return;
double lastNewComboTime = 0;
originalPreempt = firstHitObject.TimePreempt;
foreach (var obj in beatmap.HitObjects.OfType<OsuHitObject>())
{
if (obj.NewCombo) { lastNewComboTime = obj.StartTime; }
applyFadeInAdjustment(obj);
}
void applyFadeInAdjustment(OsuHitObject osuObject)
{
osuObject.TimePreempt += osuObject.StartTime - lastNewComboTime;
foreach (var nested in osuObject.NestedHitObjects.OfType<OsuHitObject>())
{
switch (nested)
{
//SliderRepeat wont layer correctly if preempt is changed.
case SliderRepeat:
break;
default:
applyFadeInAdjustment(nested);
break;
}
}
}
}
public void ApplyToDrawableHitObject(DrawableHitObject drawableObject)
{
drawableObject.ApplyCustomUpdateState += (drawableHitObject, _) =>
{
if (drawableHitObject is not DrawableHitCircle drawableHitCircle) return;
var hitCircle = drawableHitCircle.HitObject;
var approachCircle = drawableHitCircle.ApproachCircle;
// Reapply scale, ensuring the AR isn't changed due to the new preempt.
approachCircle.ClearTransforms(targetMember: nameof(approachCircle.Scale));
approachCircle.ScaleTo(4 * (float)(hitCircle.TimePreempt / originalPreempt));
using (drawableHitCircle.ApproachCircle.BeginAbsoluteSequence(hitCircle.StartTime - hitCircle.TimePreempt))
approachCircle.ScaleTo(1, hitCircle.TimePreempt).Then().Expire();
};
}
}
}

View File

@ -3,6 +3,7 @@
using System; using System;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Timing; using osu.Framework.Timing;
@ -46,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public void Update(Playfield playfield) public void Update(Playfield playfield)
{ {
var cursorPos = playfield.Cursor.ActiveCursor.DrawPosition; var cursorPos = playfield.Cursor.AsNonNull().ActiveCursor.DrawPosition;
foreach (var drawable in playfield.HitObjectContainer.AliveObjects) foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
{ {

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Localisation; using osu.Framework.Localisation;
@ -9,6 +10,7 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Utils; using osu.Game.Utils;
@ -33,9 +35,15 @@ namespace osu.Game.Rulesets.Osu.Mods
public void Update(Playfield playfield) public void Update(Playfield playfield)
{ {
bool shouldAlwaysShowCursor = IsBreakTime.Value || spinnerPeriods.IsInAny(playfield.Clock.CurrentTime); var osuPlayfield = (OsuPlayfield)playfield;
Debug.Assert(osuPlayfield.Cursor != null);
bool shouldAlwaysShowCursor = IsBreakTime.Value || spinnerPeriods.IsInAny(osuPlayfield.Clock.CurrentTime);
float targetAlpha = shouldAlwaysShowCursor ? 1 : ComboBasedAlpha; float targetAlpha = shouldAlwaysShowCursor ? 1 : ComboBasedAlpha;
playfield.Cursor.Alpha = (float)Interpolation.Lerp(playfield.Cursor.Alpha, targetAlpha, Math.Clamp(playfield.Time.Elapsed / TRANSITION_DURATION, 0, 1)); float currentAlpha = (float)Interpolation.Lerp(osuPlayfield.Cursor.Alpha, targetAlpha, Math.Clamp(osuPlayfield.Time.Elapsed / TRANSITION_DURATION, 0, 1));
osuPlayfield.Cursor.Alpha = currentAlpha;
osuPlayfield.Smoke.Alpha = currentAlpha;
} }
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Framework.Utils; using osu.Framework.Utils;
@ -45,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public void Update(Playfield playfield) public void Update(Playfield playfield)
{ {
var cursorPos = playfield.Cursor.ActiveCursor.DrawPosition; var cursorPos = playfield.Cursor.AsNonNull().ActiveCursor.DrawPosition;
foreach (var drawable in playfield.HitObjectContainer.AliveObjects) foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
{ {

View File

@ -14,12 +14,10 @@ using osu.Game.Audio;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
@ -106,7 +104,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
foreach (var drawableHitObject in NestedHitObjects) foreach (var drawableHitObject in NestedHitObjects)
drawableHitObject.AccentColour.Value = colour.NewValue; drawableHitObject.AccentColour.Value = colour.NewValue;
updateBallTint();
}, true); }, true);
Tracking.BindValueChanged(updateSlidingSample); Tracking.BindValueChanged(updateSlidingSample);
@ -257,22 +254,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
SliderBody?.RecyclePath(); SliderBody?.RecyclePath();
} }
protected override void ApplySkin(ISkinSource skin, bool allowFallback)
{
base.ApplySkin(skin, allowFallback);
updateBallTint();
}
private void updateBallTint()
{
if (CurrentSkin == null)
return;
bool allowBallTint = CurrentSkin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false;
Ball.AccentColour = allowBallTint ? AccentColour.Value : Color4.White;
}
protected override void CheckForResult(bool userTriggered, double timeOffset) protected override void CheckForResult(bool userTriggered, double timeOffset)
{ {
if (userTriggered || Time.Current < HitObject.EndTime) if (userTriggered || Time.Current < HitObject.EndTime)
@ -331,7 +312,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
base.UpdateHitStateTransforms(state); base.UpdateHitStateTransforms(state);
const float fade_out_time = 450; const float fade_out_time = 240;
switch (state) switch (state)
{ {
@ -341,7 +322,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
break; break;
} }
this.FadeOut(fade_out_time, Easing.OutQuint).Expire(); this.FadeOut(fade_out_time).Expire();
} }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => SliderBody?.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => SliderBody?.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos);

View File

@ -11,28 +11,20 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public class DrawableSliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour public class DrawableSliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition
{ {
public const float FOLLOW_AREA = 2.4f; public const float FOLLOW_AREA = 2.4f;
public Func<OsuAction?> GetInitialHitAction; public Func<OsuAction?> GetInitialHitAction;
public Color4 AccentColour
{
get => ball.Colour;
set => ball.Colour = value;
}
private Drawable followCircleReceptor; private Drawable followCircleReceptor;
private DrawableSlider drawableSlider; private DrawableSlider drawableSlider;
private Drawable ball; private Drawable ball;

View File

@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// </summary> /// </summary>
public readonly IBindable<double> SpinsPerMinute = new BindableDouble(); public readonly IBindable<double> SpinsPerMinute = new BindableDouble();
private const double fade_out_duration = 160; private const double fade_out_duration = 240;
public DrawableSpinner() public DrawableSpinner()
: this(null) : this(null)

View File

@ -201,7 +201,8 @@ namespace osu.Game.Rulesets.Osu
new OsuModMuted(), new OsuModMuted(),
new OsuModNoScope(), new OsuModNoScope(),
new MultiMod(new OsuModMagnetised(), new OsuModRepel()), new MultiMod(new OsuModMagnetised(), new OsuModRepel()),
new ModAdaptiveSpeed() new ModAdaptiveSpeed(),
new OsuModFreezeFrame()
}; };
case ModType.System: case ModType.System:

View File

@ -75,6 +75,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{ {
default: default:
JudgementText JudgementText
.FadeInFromZero(300, Easing.OutQuint)
.ScaleTo(Vector2.One) .ScaleTo(Vector2.One)
.ScaleTo(new Vector2(1.2f), 1800, Easing.OutQuint); .ScaleTo(new Vector2(1.2f), 1800, Easing.OutQuint);
break; break;
@ -96,7 +97,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
ringExplosion?.PlayAnimation(); ringExplosion?.PlayAnimation();
} }
public Drawable? GetAboveHitObjectsProxiedContent() => null; public Drawable? GetAboveHitObjectsProxiedContent() => JudgementText.CreateProxy();
private class RingExplosion : CompositeDrawable private class RingExplosion : CompositeDrawable
{ {

View File

@ -108,18 +108,23 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{ {
base.LoadComplete(); base.LoadComplete();
accentColour.BindValueChanged(colour =>
{
outerFill.Colour = innerFill.Colour = colour.NewValue.Darken(4);
outerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.1f));
innerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue.Darken(0.5f), colour.NewValue.Darken(0.6f));
flash.Colour = colour.NewValue;
}, true);
indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true); indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true);
accentColour.BindValueChanged(colour =>
{
// A colour transform is applied.
// Without removing transforms first, when it is rewound it may apply an old colour.
outerGradient.ClearTransforms(targetMember: nameof(Colour));
outerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.1f));
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;
updateStateTransforms(drawableObject, drawableObject.State.Value);
}, true);
drawableObject.ApplyCustomUpdateState += updateStateTransforms; drawableObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableObject, drawableObject.State.Value);
} }
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
@ -173,11 +178,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
.FadeOut(flash_in_duration); .FadeOut(flash_in_duration);
} }
// The flash layer starts white to give the wanted brightness, but is almost immediately
// recoloured to the accent colour. This would more correctly be done with two layers (one for the initial flash)
// but works well enough with the colour fade.
flash.FadeTo(1, flash_in_duration, Easing.OutQuint); flash.FadeTo(1, flash_in_duration, Easing.OutQuint);
flash.FlashColour(accentColour.Value, fade_out_time, Easing.OutQuint);
this.FadeOut(fade_out_time, Easing.OutQuad); this.FadeOut(fade_out_time, Easing.OutQuad);
break; break;

View File

@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
InternalChildren = new[] InternalChildren = new[]
{ {
CircleSprite = new KiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) }) CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) })
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Child = OverlaySprite = new KiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d)) Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d))
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -134,10 +134,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
switch (state) switch (state)
{ {
case ArmedState.Hit: case ArmedState.Hit:
CircleSprite.FadeOut(legacy_fade_duration, Easing.Out); CircleSprite.FadeOut(legacy_fade_duration);
CircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); CircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
OverlaySprite.FadeOut(legacy_fade_duration, Easing.Out); OverlaySprite.FadeOut(legacy_fade_duration);
OverlaySprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); OverlaySprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
if (hasNumber) if (hasNumber)
@ -146,11 +146,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
if (legacyVersion >= 2.0m) if (legacyVersion >= 2.0m)
// legacy skins of version 2.0 and newer only apply very short fade out to the number piece. // legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out); hitCircleText.FadeOut(legacy_fade_duration / 4);
else else
{ {
// old skins scale and fade it normally along other pieces. // old skins scale and fade it normally along other pieces.
hitCircleText.FadeOut(legacy_fade_duration, Easing.Out); hitCircleText.FadeOut(legacy_fade_duration);
hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
} }
} }

View File

@ -107,8 +107,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt)) using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
this.FadeOut(); this.FadeOut();
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2)) using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn))
this.FadeInFromZero(spinner.TimeFadeIn / 2); this.FadeInFromZero(spinner.TimeFadeIn);
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt)) using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
{ {

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
@ -21,6 +22,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private DrawableHitObject? parentObject { get; set; } private DrawableHitObject? parentObject { get; set; }
public Color4 BallColour => animationContent.Colour;
private Sprite layerNd = null!; private Sprite layerNd = null!;
private Sprite layerSpec = null!; private Sprite layerSpec = null!;
@ -61,6 +64,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
}; };
} }
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
@ -69,6 +74,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
parentObject.ApplyCustomUpdateState += updateStateTransforms; parentObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(parentObject, parentObject.State.Value); updateStateTransforms(parentObject, parentObject.State.Value);
if (skin.GetConfig<SkinConfiguration.LegacySetting, bool>(SkinConfiguration.LegacySetting.AllowSliderBallTint)?.Value == true)
{
accentColour.BindTo(parentObject.AccentColour);
accentColour.BindValueChanged(a => animationContent.Colour = a.NewValue, true);
}
} }
} }

View File

@ -65,6 +65,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
spin = new Sprite spin = new Sprite
{ {
Alpha = 0,
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-spin"), Texture = source.GetTexture("spinner-spin"),
@ -82,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
}, },
bonusCounter = new LegacySpriteText(LegacyFont.Score) bonusCounter = new LegacySpriteText(LegacyFont.Score)
{ {
Alpha = 0f, Alpha = 0,
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Scale = new Vector2(SPRITE_SCALE), Scale = new Vector2(SPRITE_SCALE),
@ -179,6 +180,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out); spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out);
} }
using (BeginAbsoluteSequence(d.HitObject.StartTime - d.HitObject.TimeFadeIn / 2))
spin.FadeInFromZero(d.HitObject.TimeFadeIn / 2);
using (BeginAbsoluteSequence(d.HitObject.StartTime)) using (BeginAbsoluteSequence(d.HitObject.StartTime))
ApproachCircle?.ScaleTo(SPRITE_SCALE * 0.1f, d.HitObject.Duration); ApproachCircle?.ScaleTo(SPRITE_SCALE * 0.1f, d.HitObject.Duration);

View File

@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
public class OsuLegacySkinTransformer : LegacySkinTransformer public class OsuLegacySkinTransformer : LegacySkinTransformer
{ {
public override bool IsProvidingLegacyResources => base.IsProvidingLegacyResources || hasHitCircle.Value;
private readonly Lazy<bool> hasHitCircle; private readonly Lazy<bool> hasHitCircle;
/// <summary> /// <summary>

View File

@ -9,7 +9,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
SliderBorderSize, SliderBorderSize,
SliderPathRadius, SliderPathRadius,
AllowSliderBallTint,
CursorCentre, CursorCentre,
CursorExpand, CursorExpand,
CursorRotate, CursorRotate,

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -14,6 +15,7 @@ using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Utils;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -21,8 +23,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
public abstract class SmokeSegment : Drawable, ITexturedShaderDrawable public abstract class SmokeSegment : Drawable, ITexturedShaderDrawable
{ {
private const int max_point_count = 18_000;
// fade anim values // fade anim values
private const double initial_fade_out_duration = 4000; private const double initial_fade_out_duration = 4000;
@ -84,12 +84,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
totalDistance = pointInterval; totalDistance = pointInterval;
} }
private Vector2 nextPointDirection()
{
float angle = RNG.NextSingle(0, 2 * MathF.PI);
return new Vector2(MathF.Sin(angle), -MathF.Cos(angle));
}
public void AddPosition(Vector2 position, double time) public void AddPosition(Vector2 position, double time)
{ {
lastPosition ??= position; lastPosition ??= position;
@ -106,33 +100,27 @@ namespace osu.Game.Rulesets.Osu.Skinning
Vector2 pointPos = (pointInterval - (totalDistance - delta)) * increment + (Vector2)lastPosition; Vector2 pointPos = (pointInterval - (totalDistance - delta)) * increment + (Vector2)lastPosition;
increment *= pointInterval; increment *= pointInterval;
if (SmokePoints.Count > 0 && SmokePoints[^1].Time > time)
{
int index = ~SmokePoints.BinarySearch(new SmokePoint { Time = time }, new SmokePoint.UpperBoundComparer());
SmokePoints.RemoveRange(index, SmokePoints.Count - index);
}
totalDistance %= pointInterval; totalDistance %= pointInterval;
for (int i = 0; i < count; i++) if (SmokePoints.Count == 0 || SmokePoints[^1].Time <= time)
{ {
SmokePoints.Add(new SmokePoint for (int i = 0; i < count; i++)
{ {
Position = pointPos, SmokePoints.Add(new SmokePoint
Time = time, {
Direction = nextPointDirection(), Position = pointPos,
}); Time = time,
Angle = RNG.NextSingle(0, 2 * MathF.PI),
});
pointPos += increment; pointPos += increment;
}
} }
Invalidate(Invalidation.DrawNode); Invalidate(Invalidation.DrawNode);
} }
lastPosition = position; lastPosition = position;
if (SmokePoints.Count >= max_point_count)
FinishDrawing(time);
} }
public void FinishDrawing(double time) public void FinishDrawing(double time)
@ -156,7 +144,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
public Vector2 Position; public Vector2 Position;
public double Time; public double Time;
public Vector2 Direction; public float Angle;
public struct UpperBoundComparer : IComparer<SmokePoint> public struct UpperBoundComparer : IComparer<SmokePoint>
{ {
@ -170,6 +158,17 @@ namespace osu.Game.Rulesets.Osu.Skinning
return x.Time > target.Time ? 1 : -1; return x.Time > target.Time ? 1 : -1;
} }
} }
public struct LowerBoundComparer : IComparer<SmokePoint>
{
public int Compare(SmokePoint x, SmokePoint target)
{
// Similar logic as UpperBoundComparer, except returned index will always be
// the first element larger or equal
return x.Time < target.Time ? -1 : 1;
}
}
} }
protected class SmokeDrawNode : TexturedShaderDrawNode protected class SmokeDrawNode : TexturedShaderDrawNode
@ -185,17 +184,17 @@ namespace osu.Game.Rulesets.Osu.Skinning
private float radius; private float radius;
private Vector2 drawSize; private Vector2 drawSize;
private Texture? texture; private Texture? texture;
private int rotationSeed;
private int firstVisiblePointIndex;
// anim calculation vars (color, scale, direction) // anim calculation vars (color, scale, direction)
private double initialFadeOutDurationTrunc; private double initialFadeOutDurationTrunc;
private double firstVisiblePointTime; private double firstVisiblePointTimeAfterSmokeEnded;
private double initialFadeOutTime; private double initialFadeOutTime;
private double reFadeInTime; private double reFadeInTime;
private double finalFadeOutTime; private double finalFadeOutTime;
private Random rotationRNG = new Random();
public SmokeDrawNode(ITexturedShaderDrawable source) public SmokeDrawNode(ITexturedShaderDrawable source)
: base(source) : base(source)
{ {
@ -205,9 +204,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
base.ApplyState(); base.ApplyState();
points.Clear();
points.AddRange(Source.SmokePoints);
radius = Source.radius; radius = Source.radius;
drawSize = Source.DrawSize; drawSize = Source.DrawSize;
texture = Source.Texture; texture = Source.Texture;
@ -216,14 +212,21 @@ namespace osu.Game.Rulesets.Osu.Skinning
SmokeEndTime = Source.smokeEndTime; SmokeEndTime = Source.smokeEndTime;
CurrentTime = Source.Clock.CurrentTime; CurrentTime = Source.Clock.CurrentTime;
rotationRNG = new Random(Source.rotationSeed); rotationSeed = Source.rotationSeed;
initialFadeOutDurationTrunc = Math.Min(initial_fade_out_duration, SmokeEndTime - SmokeStartTime); initialFadeOutDurationTrunc = Math.Min(initial_fade_out_duration, SmokeEndTime - SmokeStartTime);
firstVisiblePointTime = SmokeEndTime - initialFadeOutDurationTrunc; firstVisiblePointTimeAfterSmokeEnded = SmokeEndTime - initialFadeOutDurationTrunc;
initialFadeOutTime = CurrentTime; initialFadeOutTime = Math.Min(CurrentTime, SmokeEndTime);
reFadeInTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / re_fade_in_speed); reFadeInTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTimeAfterSmokeEnded * (1 - 1 / re_fade_in_speed);
finalFadeOutTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / final_fade_out_speed); finalFadeOutTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTimeAfterSmokeEnded * (1 - 1 / final_fade_out_speed);
double firstVisiblePointTime = Math.Min(SmokeEndTime, CurrentTime) - initialFadeOutDurationTrunc;
firstVisiblePointIndex = ~Source.SmokePoints.BinarySearch(new SmokePoint { Time = firstVisiblePointTime }, new SmokePoint.LowerBoundComparer());
int futurePointIndex = ~Source.SmokePoints.BinarySearch(new SmokePoint { Time = CurrentTime }, new SmokePoint.UpperBoundComparer());
points.Clear();
points.AddRange(Source.SmokePoints.Skip(firstVisiblePointIndex).Take(futurePointIndex - firstVisiblePointIndex));
} }
public sealed override void Draw(IRenderer renderer) public sealed override void Draw(IRenderer renderer)
@ -233,7 +236,14 @@ namespace osu.Game.Rulesets.Osu.Skinning
if (points.Count == 0) if (points.Count == 0)
return; return;
quadBatch ??= renderer.CreateQuadBatch<TexturedVertex2D>(max_point_count / 10, 10); quadBatch ??= renderer.CreateQuadBatch<TexturedVertex2D>(200, 4);
if (points.Count > quadBatch.Size && quadBatch.Size != IRenderer.MAX_QUADS)
{
int batchSize = Math.Min(quadBatch.Size * 2, IRenderer.MAX_QUADS);
quadBatch = renderer.CreateQuadBatch<TexturedVertex2D>(batchSize, 4);
}
texture ??= renderer.WhitePixel; texture ??= renderer.WhitePixel;
RectangleF textureRect = texture.GetTextureRect(); RectangleF textureRect = texture.GetTextureRect();
@ -245,8 +255,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
shader.Bind(); shader.Bind();
texture.Bind(); texture.Bind();
foreach (var point in points) for (int i = 0; i < points.Count; i++)
drawPointQuad(point, textureRect); drawPointQuad(points[i], textureRect, i + firstVisiblePointIndex);
shader.Unbind(); shader.Unbind();
renderer.PopLocalMatrix(); renderer.PopLocalMatrix();
@ -260,30 +270,34 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
var color = Color4.White; var color = Color4.White;
double timeDoingInitialFadeOut = Math.Min(initialFadeOutTime, SmokeEndTime) - point.Time; double timeDoingFinalFadeOut = finalFadeOutTime - point.Time / final_fade_out_speed;
if (timeDoingInitialFadeOut > 0) if (timeDoingFinalFadeOut > 0 && point.Time >= firstVisiblePointTimeAfterSmokeEnded)
{ {
float fraction = Math.Clamp((float)(timeDoingInitialFadeOut / initial_fade_out_duration), 0, 1); float fraction = Math.Clamp((float)(timeDoingFinalFadeOut / final_fade_out_duration), 0, 1);
color.A = (1 - fraction) * initial_alpha; fraction = MathF.Pow(fraction, 5);
color.A = (1 - fraction) * re_fade_in_alpha;
} }
else
if (color.A > 0)
{ {
double timeDoingReFadeIn = reFadeInTime - point.Time / re_fade_in_speed; double timeDoingInitialFadeOut = initialFadeOutTime - point.Time;
double timeDoingFinalFadeOut = finalFadeOutTime - point.Time / final_fade_out_speed;
if (timeDoingFinalFadeOut > 0) if (timeDoingInitialFadeOut > 0)
{ {
float fraction = Math.Clamp((float)(timeDoingFinalFadeOut / final_fade_out_duration), 0, 1); float fraction = Math.Clamp((float)(timeDoingInitialFadeOut / initial_fade_out_duration), 0, 1);
fraction = MathF.Pow(fraction, 5); color.A = (1 - fraction) * initial_alpha;
color.A = (1 - fraction) * re_fade_in_alpha;
} }
else if (timeDoingReFadeIn > 0)
if (point.Time > firstVisiblePointTimeAfterSmokeEnded)
{ {
float fraction = Math.Clamp((float)(timeDoingReFadeIn / re_fade_in_duration), 0, 1); double timeDoingReFadeIn = reFadeInTime - point.Time / re_fade_in_speed;
fraction = 1 - MathF.Pow(1 - fraction, 5);
color.A = fraction * (re_fade_in_alpha - color.A) + color.A; if (timeDoingReFadeIn > 0)
{
float fraction = Math.Clamp((float)(timeDoingReFadeIn / re_fade_in_duration), 0, 1);
fraction = 1 - MathF.Pow(1 - fraction, 5);
color.A = fraction * (re_fade_in_alpha - color.A) + color.A;
}
} }
} }
@ -298,33 +312,33 @@ namespace osu.Game.Rulesets.Osu.Skinning
return fraction * (final_scale - initial_scale) + initial_scale; return fraction * (final_scale - initial_scale) + initial_scale;
} }
protected virtual Vector2 PointDirection(SmokePoint point) protected virtual Vector2 PointDirection(SmokePoint point, int index)
{ {
float initialAngle = MathF.Atan2(point.Direction.Y, point.Direction.X);
float finalAngle = initialAngle + nextRotation();
double timeDoingRotation = CurrentTime - point.Time; double timeDoingRotation = CurrentTime - point.Time;
float fraction = Math.Clamp((float)(timeDoingRotation / rotation_duration), 0, 1); float fraction = Math.Clamp((float)(timeDoingRotation / rotation_duration), 0, 1);
fraction = 1 - MathF.Pow(1 - fraction, 5); fraction = 1 - MathF.Pow(1 - fraction, 5);
float angle = fraction * (finalAngle - initialAngle) + initialAngle; float angle = fraction * getRotation(index) + point.Angle;
return new Vector2(MathF.Sin(angle), -MathF.Cos(angle)); return new Vector2(MathF.Sin(angle), -MathF.Cos(angle));
} }
private float nextRotation() => max_rotation * ((float)rotationRNG.NextDouble() * 2 - 1); private float getRotation(int index) => max_rotation * (StatelessRNG.NextSingle(rotationSeed, index) * 2 - 1);
private void drawPointQuad(SmokePoint point, RectangleF textureRect) private void drawPointQuad(SmokePoint point, RectangleF textureRect, int index)
{ {
Debug.Assert(quadBatch != null); Debug.Assert(quadBatch != null);
var colour = PointColour(point); var colour = PointColour(point);
float scale = PointScale(point); if (colour.A == 0)
var dir = PointDirection(point);
var ortho = dir.PerpendicularLeft;
if (colour.A == 0 || scale == 0)
return; return;
float scale = PointScale(point);
if (scale == 0)
return;
var dir = PointDirection(point, index);
var ortho = dir.PerpendicularLeft;
var localTopLeft = point.Position + (radius * scale * (-ortho - dir)); var localTopLeft = point.Position + (radius * scale * (-ortho - dir));
var localTopRight = point.Position + (radius * scale * (-ortho + dir)); var localTopRight = point.Position + (radius * scale * (-ortho + dir));
var localBotLeft = point.Position + (radius * scale * (ortho - dir)); var localBotLeft = point.Position + (radius * scale * (ortho - dir));

View File

@ -36,6 +36,7 @@ namespace osu.Game.Rulesets.Osu.UI
private readonly ProxyContainer spinnerProxies; private readonly ProxyContainer spinnerProxies;
private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer; private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer;
public SmokeContainer Smoke { get; }
public FollowPointRenderer FollowPoints { get; } public FollowPointRenderer FollowPoints { get; }
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
@ -54,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.UI
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both }, playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
new SmokeContainer { RelativeSizeAxes = Axes.Both }, Smoke = new SmokeContainer { RelativeSizeAxes = Axes.Both },
spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both }, spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both },
FollowPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both }, FollowPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both },
judgementLayer = new JudgementContainer<DrawableOsuJudgement> { RelativeSizeAxes = Axes.Both }, judgementLayer = new JudgementContainer<DrawableOsuJudgement> { RelativeSizeAxes = Axes.Both },

View File

@ -0,0 +1,56 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.Taiko.UI;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Tests.Mods
{
public class TestSceneTaikoModFlashlight : TaikoModTestScene
{
[TestCase(1f)]
[TestCase(0.5f)]
[TestCase(1.25f)]
[TestCase(1.5f)]
public void TestSizeMultiplier(float sizeMultiplier) => CreateModTest(new ModTestData { Mod = new TaikoModFlashlight { SizeMultiplier = { Value = sizeMultiplier } }, PassCondition = () => true });
[Test]
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new TaikoModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
[Test]
public void TestFlashlightAlwaysHasNonZeroSize()
{
bool failed = false;
CreateModTest(new ModTestData
{
Mod = new TestTaikoModFlashlight { ComboBasedSize = { Value = true } },
Autoplay = false,
PassCondition = () =>
{
failed |= this.ChildrenOfType<TestTaikoModFlashlight.TestTaikoFlashlight>().SingleOrDefault()?.FlashlightSize.Y == 0;
return !failed;
}
});
}
private class TestTaikoModFlashlight : TaikoModFlashlight
{
protected override Flashlight CreateFlashlight() => new TestTaikoFlashlight(this, Playfield);
public class TestTaikoFlashlight : TaikoFlashlight
{
public TestTaikoFlashlight(TaikoModFlashlight modFlashlight, TaikoPlayfield taikoPlayfield)
: base(modFlashlight, taikoPlayfield)
{
}
public new Vector2 FlashlightSize => base.FlashlightSize;
}
}
}
}

View File

@ -25,8 +25,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
TimeRange = { Value = 5000 }, TimeRange = { Value = 5000 },
}; };
[BackgroundDependencyLoader] [Test]
private void load() public void DrumrollTest()
{ {
AddStep("Drum roll", () => SetContents(_ => AddStep("Drum roll", () => SetContents(_ =>
{ {

View File

@ -0,0 +1,30 @@
// 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 NUnit.Framework;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps;
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
[TestFixture]
public class TestSceneDrawableDrumRollKiai : TestSceneDrawableDrumRoll
{
[SetUp]
public void SetUp() => Schedule(() =>
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
Beatmap.Value = CreateWorkingBeatmap(new Beatmap
{
ControlPointInfo = controlPointInfo
});
// track needs to be playing for BeatSyncedContainer to work.
Beatmap.Value.Track.Start();
});
}
}

View File

@ -4,7 +4,6 @@
#nullable disable #nullable disable
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -16,8 +15,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[TestFixture] [TestFixture]
public class TestSceneDrawableHit : TaikoSkinnableTestScene public class TestSceneDrawableHit : TaikoSkinnableTestScene
{ {
[BackgroundDependencyLoader] [Test]
private void load() public void TestHits()
{ {
AddStep("Centre hit", () => SetContents(_ => new DrawableHit(createHitAtCurrentTime()) AddStep("Centre hit", () => SetContents(_ => new DrawableHit(createHitAtCurrentTime())
{ {

View File

@ -0,0 +1,30 @@
// 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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
[TestFixture]
public class TestSceneDrawableHitKiai : TestSceneDrawableHit
{
[SetUp]
public void SetUp() => Schedule(() =>
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
Beatmap.Value = CreateWorkingBeatmap(new Beatmap
{
ControlPointInfo = controlPointInfo
});
// track needs to be playing for BeatSyncedContainer to work.
Beatmap.Value.Track.Start();
});
}
}

View File

@ -27,6 +27,12 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
}; };
} }
protected override void LoadComplete()
{
base.LoadComplete();
BeginPlacement();
}
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)
{ {
switch (e.Button) switch (e.Button)

View File

@ -52,6 +52,12 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
private double originalStartTime; private double originalStartTime;
private Vector2 originalPosition; private Vector2 originalPosition;
protected override void LoadComplete()
{
base.LoadComplete();
BeginPlacement();
}
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)
{ {
if (e.Button != MouseButton.Left) if (e.Button != MouseButton.Left)

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;

View File

@ -27,17 +27,17 @@ namespace osu.Game.Rulesets.Taiko.Mods
public override float DefaultFlashlightSize => 200; public override float DefaultFlashlightSize => 200;
protected override Flashlight CreateFlashlight() => new TaikoFlashlight(this, playfield); protected override Flashlight CreateFlashlight() => new TaikoFlashlight(this, Playfield);
private TaikoPlayfield playfield = null!; protected TaikoPlayfield Playfield { get; private set; } = null!;
public override void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset) public override void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
{ {
playfield = (TaikoPlayfield)drawableRuleset.Playfield; Playfield = (TaikoPlayfield)drawableRuleset.Playfield;
base.ApplyToDrawableRuleset(drawableRuleset); base.ApplyToDrawableRuleset(drawableRuleset);
} }
private class TaikoFlashlight : Flashlight public class TaikoFlashlight : Flashlight
{ {
private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo); private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo);
private readonly TaikoPlayfield taikoPlayfield; private readonly TaikoPlayfield taikoPlayfield;
@ -47,21 +47,28 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
this.taikoPlayfield = taikoPlayfield; this.taikoPlayfield = taikoPlayfield;
FlashlightSize = getSizeFor(0); FlashlightSize = adjustSizeForPlayfieldAspectRatio(GetSize());
FlashlightSmoothness = 1.4f; FlashlightSmoothness = 1.4f;
AddLayout(flashlightProperties); AddLayout(flashlightProperties);
} }
private Vector2 getSizeFor(int combo) /// <summary>
/// Returns the aspect ratio-adjusted size of the flashlight.
/// This ensures that the size of the flashlight remains independent of taiko-specific aspect ratio adjustments.
/// </summary>
/// <param name="size">
/// The size of the flashlight.
/// The value provided here should always come from <see cref="ModFlashlight{T}.Flashlight.GetSize"/>.
/// </param>
private Vector2 adjustSizeForPlayfieldAspectRatio(float size)
{ {
// Preserve flashlight size through the playfield's aspect adjustment. return new Vector2(0, size * taikoPlayfield.DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT);
return new Vector2(0, GetSizeFor(combo) * taikoPlayfield.DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT);
} }
protected override void OnComboChange(ValueChangedEvent<int> e) protected override void UpdateFlashlightSize(float size)
{ {
this.TransformTo(nameof(FlashlightSize), getSizeFor(e.NewValue), FLASHLIGHT_FADE_DURATION); this.TransformTo(nameof(FlashlightSize), adjustSizeForPlayfieldAspectRatio(size), FLASHLIGHT_FADE_DURATION);
} }
protected override string FragmentShader => "CircularFlashlight"; protected override string FragmentShader => "CircularFlashlight";
@ -75,7 +82,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
FlashlightPosition = ToLocalSpace(taikoPlayfield.HitTarget.ScreenSpaceDrawQuad.Centre); FlashlightPosition = ToLocalSpace(taikoPlayfield.HitTarget.ScreenSpaceDrawQuad.Centre);
ClearTransforms(targetMember: nameof(FlashlightSize)); ClearTransforms(targetMember: nameof(FlashlightSize));
FlashlightSize = getSizeFor(Combo.Value); FlashlightSize = adjustSizeForPlayfieldAspectRatio(GetSize());
flashlightProperties.Validate(); flashlightProperties.Validate();
} }

View File

@ -3,6 +3,7 @@
#nullable disable #nullable disable
using osu.Framework.Allocation;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -13,6 +14,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
using osuTK.Graphics; using osuTK.Graphics;
@ -32,6 +34,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
private const double pre_beat_transition_time = 80; private const double pre_beat_transition_time = 80;
private const float flash_opacity = 0.3f;
private Color4 accentColour; private Color4 accentColour;
/// <summary> /// <summary>
@ -152,11 +156,22 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
}; };
} }
[Resolved]
private DrawableHitObject drawableHitObject { get; set; }
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{ {
if (!effectPoint.KiaiMode) if (!effectPoint.KiaiMode)
return; return;
if (drawableHitObject.State.Value == ArmedState.Idle)
{
FlashBox
.FadeTo(flash_opacity)
.Then()
.FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine);
}
if (beatIndex % timingPoint.TimeSignature.Numerator != 0) if (beatIndex % timingPoint.TimeSignature.Numerator != 0)
return; return;

View File

@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
} }
// backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer. // backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer.
AddInternal(backgroundLayer = getDrawableFor("circle")); AddInternal(backgroundLayer = new LegacyKiaiFlashingDrawable(() => getDrawableFor("circle")));
var foregroundLayer = getDrawableFor("circleoverlay"); var foregroundLayer = getDrawableFor("circleoverlay");
if (foregroundLayer != null) if (foregroundLayer != null)

View File

@ -14,8 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{ {
public class TaikoLegacySkinTransformer : LegacySkinTransformer public class TaikoLegacySkinTransformer : LegacySkinTransformer
{ {
public override bool IsProvidingLegacyResources => base.IsProvidingLegacyResources || hasHitCircle || hasBarLeft;
private readonly Lazy<bool> hasExplosion; private readonly Lazy<bool> hasExplosion;
private bool hasHitCircle => GetTexture("taikohitcircle") != null;
private bool hasBarLeft => GetTexture("taiko-bar-left") != null;
public TaikoLegacySkinTransformer(ISkin skin) public TaikoLegacySkinTransformer(ISkin skin)
: base(skin) : base(skin)
{ {
@ -42,14 +47,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
return null; return null;
case TaikoSkinComponents.InputDrum: case TaikoSkinComponents.InputDrum:
if (GetTexture("taiko-bar-left") != null) if (hasBarLeft)
return new LegacyInputDrum(); return new LegacyInputDrum();
return null; return null;
case TaikoSkinComponents.CentreHit: case TaikoSkinComponents.CentreHit:
case TaikoSkinComponents.RimHit: case TaikoSkinComponents.RimHit:
if (GetTexture("taikohitcircle") != null) if (hasHitCircle)
return new LegacyHit(taikoComponent.Component); return new LegacyHit(taikoComponent.Component);
return null; return null;

View File

@ -9,8 +9,10 @@ using osu.Framework.Extensions;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -96,5 +98,41 @@ namespace osu.Game.Tests.Beatmaps
var second = beatmaps.GetWorkingBeatmap(beatmap, true); var second = beatmaps.GetWorkingBeatmap(beatmap, true);
Assert.That(first, Is.Not.SameAs(second)); Assert.That(first, Is.Not.SameAs(second));
}); });
[Test]
public void TestSavePreservesCollections() => AddStep("run test", () =>
{
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID).Detach());
var working = beatmaps.GetWorkingBeatmap(beatmap);
Assert.That(working.BeatmapInfo.BeatmapSet?.Files, Has.Count.GreaterThan(0));
string initialHash = working.BeatmapInfo.MD5Hash;
var preserveCollection = new BeatmapCollection("test contained");
preserveCollection.BeatmapMD5Hashes.Add(initialHash);
var noNewCollection = new BeatmapCollection("test not contained");
Realm.Write(r =>
{
r.Add(preserveCollection);
r.Add(noNewCollection);
});
Assert.That(preserveCollection.BeatmapMD5Hashes, Does.Contain(initialHash));
Assert.That(noNewCollection.BeatmapMD5Hashes, Does.Not.Contain(initialHash));
beatmaps.Save(working.BeatmapInfo, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo));
string finalHash = working.BeatmapInfo.MD5Hash;
Assert.That(finalHash, Is.Not.SameAs(initialHash));
Assert.That(preserveCollection.BeatmapMD5Hashes, Does.Not.Contain(initialHash));
Assert.That(preserveCollection.BeatmapMD5Hashes, Does.Contain(finalHash));
Assert.That(noNewCollection.BeatmapMD5Hashes, Does.Not.Contain(finalHash));
});
} }
} }

View File

@ -71,9 +71,9 @@ namespace osu.Game.Tests.Editing
[TestCase(1)] [TestCase(1)]
[TestCase(2)] [TestCase(2)]
public void TestSpeedMultiplier(float multiplier) public void TestSpeedMultiplierDoesNotChangeDistanceSnap(float multiplier)
{ {
assertSnapDistance(100 * multiplier, new HitObject assertSnapDistance(100, new HitObject
{ {
DifficultyControlPoint = new DifficultyControlPoint DifficultyControlPoint = new DifficultyControlPoint
{ {

View File

@ -38,7 +38,9 @@ namespace osu.Game.Tests.Skins
// Covers legacy song progress, UR counter, colour hit error metre. // Covers legacy song progress, UR counter, colour hit error metre.
"Archives/modified-classic-20220801.osk", "Archives/modified-classic-20220801.osk",
// Covers clicks/s counter // Covers clicks/s counter
"Archives/modified-default-20220818.osk" "Archives/modified-default-20220818.osk",
// Covers longest combo counter
"Archives/modified-default-20221012.osk"
}; };
/// <summary> /// <summary>

View File

@ -244,7 +244,10 @@ namespace osu.Game.Tests.Visual.Background
public void TestResumeFromPlayer() public void TestResumeFromPlayer()
{ {
performFullSetup(); performFullSetup();
AddStep("Move mouse to Visual Settings", () => InputManager.MoveMouseTo(playerLoader.VisualSettingsPos)); AddStep("Move mouse to Visual Settings location", () => InputManager.MoveMouseTo(playerLoader.ScreenSpaceDrawQuad.TopRight
+ new Vector2(-playerLoader.VisualSettingsPos.ScreenSpaceDrawQuad.Width,
playerLoader.VisualSettingsPos.ScreenSpaceDrawQuad.Height / 2
)));
AddStep("Resume PlayerLoader", () => player.Restart()); AddStep("Resume PlayerLoader", () => player.Restart());
AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddStep("Move mouse to center of screen", () => InputManager.MoveMouseTo(playerLoader.ScreenPos)); AddStep("Move mouse to center of screen", () => InputManager.MoveMouseTo(playerLoader.ScreenPos));

View File

@ -106,6 +106,49 @@ namespace osu.Game.Tests.Visual.Editing
assertBeatSnap(16); assertBeatSnap(16);
} }
[Test]
public void TestKeyboardNavigation()
{
pressKey(1);
assertBeatSnap(1);
assertPreset(BeatDivisorType.Common);
pressKey(2);
assertBeatSnap(2);
assertPreset(BeatDivisorType.Common);
pressKey(3);
assertBeatSnap(3);
assertPreset(BeatDivisorType.Triplets);
pressKey(4);
assertBeatSnap(4);
assertPreset(BeatDivisorType.Common);
pressKey(5);
assertBeatSnap(5);
assertPreset(BeatDivisorType.Custom, 5);
pressKey(6);
assertBeatSnap(6);
assertPreset(BeatDivisorType.Triplets);
pressKey(7);
assertBeatSnap(7);
assertPreset(BeatDivisorType.Custom, 7);
pressKey(8);
assertBeatSnap(8);
assertPreset(BeatDivisorType.Common);
void pressKey(int key) => AddStep($"press shift+{key}", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.Number0 + key);
InputManager.ReleaseKey(Key.ShiftLeft);
});
}
[Test] [Test]
public void TestBeatPresetNavigation() public void TestBeatPresetNavigation()
{ {

View File

@ -0,0 +1,32 @@
// 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 NUnit.Framework;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
/// <summary>
/// Test editor hotkeys at a high level to ensure they all work well together.
/// </summary>
public class TestSceneEditorBindings : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
[Test]
public void TestBeatDivisorChangeHotkeys()
{
AddStep("hold shift", () => InputManager.PressKey(Key.LShift));
AddStep("press 4", () => InputManager.Key(Key.Number4));
AddAssert("snap updated to 4", () => EditorBeatmap.BeatmapInfo.BeatDivisor, () => Is.EqualTo(4));
AddStep("press 6", () => InputManager.Key(Key.Number6));
AddAssert("snap updated to 6", () => EditorBeatmap.BeatmapInfo.BeatDivisor, () => Is.EqualTo(6));
AddStep("release shift", () => InputManager.ReleaseKey(Key.LShift));
}
}
}

View File

@ -155,6 +155,20 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("composer selection box is visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha > 0); AddUntilStep("composer selection box is visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha > 0);
} }
[Test]
public void TestClone()
{
var addedObject = new HitCircle { StartTime = 1000 };
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1);
AddStep("clone", () => Editor.Clone());
AddAssert("is two objects", () => EditorBeatmap.HitObjects.Count == 2);
AddStep("clone", () => Editor.Clone());
AddAssert("is three objects", () => EditorBeatmap.HitObjects.Count == 3);
}
[Test] [Test]
public void TestCutNothing() public void TestCutNothing()
{ {
@ -175,5 +189,22 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("paste hitobject", () => Editor.Paste()); AddStep("paste hitobject", () => Editor.Paste());
AddAssert("are no objects", () => EditorBeatmap.HitObjects.Count == 0); AddAssert("are no objects", () => EditorBeatmap.HitObjects.Count == 0);
} }
[Test]
public void TestCloneNothing()
{
// Add arbitrary object and copy to clipboard.
// This is tested to ensure that clone doesn't incorrectly read from the clipboard when no selection is made.
var addedObject = new HitCircle { StartTime = 1000 };
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
AddStep("copy hitobject", () => Editor.Copy());
AddStep("deselect all objects", () => EditorBeatmap.SelectedHitObjects.Clear());
AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1);
AddStep("clone", () => Editor.Clone());
AddAssert("still one object", () => EditorBeatmap.HitObjects.Count == 1);
}
} }
} }

View File

@ -4,8 +4,10 @@
#nullable disable #nullable disable
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Screens.Edit.Components.RadioButtons; using osu.Game.Screens.Edit.Components.RadioButtons;
namespace osu.Game.Tests.Visual.Editing namespace osu.Game.Tests.Visual.Editing
@ -13,6 +15,9 @@ namespace osu.Game.Tests.Visual.Editing
[TestFixture] [TestFixture]
public class TestSceneEditorComposeRadioButtons : OsuTestScene public class TestSceneEditorComposeRadioButtons : OsuTestScene
{ {
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
public TestSceneEditorComposeRadioButtons() public TestSceneEditorComposeRadioButtons()
{ {
EditorRadioButtonCollection collection; EditorRadioButtonCollection collection;

View File

@ -148,10 +148,6 @@ namespace osu.Game.Tests.Visual.Editing
}); });
AddAssert("no circles placed", () => editorBeatmap.HitObjects.Count == 0); AddAssert("no circles placed", () => editorBeatmap.HitObjects.Count == 0);
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
AddAssert("circle placed", () => editorBeatmap.HitObjects.Count == 1);
} }
[Test] [Test]
@ -165,10 +161,11 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("hold alt", () => InputManager.PressKey(Key.LAlt)); AddStep("hold alt", () => InputManager.PressKey(Key.LAlt));
AddStep("scroll mouse 5 steps", () => InputManager.ScrollVerticalBy(5)); AddStep("scroll mouse 5 steps", () => InputManager.ScrollVerticalBy(5));
AddAssert("distance spacing increased by 0.5", () => editorBeatmap.BeatmapInfo.DistanceSpacing == originalSpacing + 0.5);
AddStep("release alt", () => InputManager.ReleaseKey(Key.LAlt)); AddStep("release alt", () => InputManager.ReleaseKey(Key.LAlt));
AddStep("release ctrl", () => InputManager.ReleaseKey(Key.LControl)); AddStep("release ctrl", () => InputManager.ReleaseKey(Key.LControl));
AddAssert("distance spacing increased by 0.5", () => editorBeatmap.BeatmapInfo.DistanceSpacing == originalSpacing + 0.5);
} }
public class EditorBeatmapContainer : Container public class EditorBeatmapContainer : Container

View File

@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.Editing
} }
} }
}, },
new MenuCursor() new MenuCursorContainer()
}; };
scrollContainer.Add(innerBox = new Box scrollContainer.Add(innerBox = new Box

View File

@ -0,0 +1,498 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneScoring : OsuTestScene
{
private GraphContainer graphs = null!;
private SettingsSlider<int> sliderMaxCombo = null!;
private FillFlowContainer legend = null!;
[Test]
public void TestBasic()
{
AddStep("setup tests", () =>
{
Children = new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{
graphs = new GraphContainer
{
RelativeSizeAxes = Axes.Both,
},
},
new Drawable[]
{
legend = new FillFlowContainer
{
Padding = new MarginPadding(20),
Direction = FillDirection.Full,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
},
new Drawable[]
{
new FillFlowContainer
{
Padding = new MarginPadding(20),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Full,
Children = new Drawable[]
{
sliderMaxCombo = new SettingsSlider<int>
{
Width = 0.5f,
TransferValueOnCommit = true,
Current = new BindableInt(1024)
{
MinValue = 96,
MaxValue = 8192,
},
LabelText = "max combo",
},
new OsuTextFlowContainer
{
RelativeSizeAxes = Axes.X,
Width = 0.5f,
AutoSizeAxes = Axes.Y,
Text = $"Left click to add miss\nRight click to add OK/{base_ok}"
}
}
},
},
}
}
};
sliderMaxCombo.Current.BindValueChanged(_ => rerun());
graphs.MissLocations.BindCollectionChanged((_, __) => rerun());
graphs.NonPerfectLocations.BindCollectionChanged((_, __) => rerun());
graphs.MaxCombo.BindTo(sliderMaxCombo.Current);
rerun();
});
}
private const int base_great = 300;
private const int base_ok = 100;
private void rerun()
{
graphs.Clear();
legend.Clear();
runForProcessor("lazer-standardised", Color4.YellowGreen, new ScoreProcessor(new OsuRuleset()) { Mode = { Value = ScoringMode.Standardised } });
runForProcessor("lazer-classic", Color4.MediumPurple, new ScoreProcessor(new OsuRuleset()) { Mode = { Value = ScoringMode.Classic } });
runScoreV1();
runScoreV2();
}
private void runScoreV1()
{
int totalScore = 0;
int currentCombo = 0;
void applyHitV1(int baseScore)
{
if (baseScore == 0)
{
currentCombo = 0;
return;
}
const float score_multiplier = 1;
totalScore += baseScore;
// combo multiplier
// ReSharper disable once PossibleLossOfFraction
totalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * score_multiplier));
currentCombo++;
}
runForAlgorithm("ScoreV1 (classic)", Color4.Purple,
() => applyHitV1(base_great),
() => applyHitV1(base_ok),
() => applyHitV1(0),
() =>
{
// Arbitrary value chosen towards the upper range.
const double score_multiplier = 4;
return (int)(totalScore * score_multiplier);
});
}
private void runScoreV2()
{
int maxCombo = sliderMaxCombo.Current.Value;
int currentCombo = 0;
double comboPortion = 0;
double currentBaseScore = 0;
double maxBaseScore = 0;
int currentHits = 0;
for (int i = 0; i < maxCombo; i++)
applyHitV2(base_great);
double comboPortionMax = comboPortion;
currentCombo = 0;
comboPortion = 0;
currentBaseScore = 0;
maxBaseScore = 0;
currentHits = 0;
void applyHitV2(int baseScore)
{
maxBaseScore += base_great;
currentBaseScore += baseScore;
comboPortion += baseScore * (1 + ++currentCombo / 10.0);
currentHits++;
}
runForAlgorithm("ScoreV2", Color4.OrangeRed,
() => applyHitV2(base_great),
() => applyHitV2(base_ok),
() =>
{
currentHits++;
maxBaseScore += base_great;
currentCombo = 0;
}, () =>
{
double accuracy = currentBaseScore / maxBaseScore;
return (int)Math.Round
(
700000 * comboPortion / comboPortionMax +
300000 * Math.Pow(accuracy, 10) * ((double)currentHits / maxCombo)
);
});
}
private void runForProcessor(string name, Color4 colour, ScoreProcessor processor)
{
int maxCombo = sliderMaxCombo.Current.Value;
var beatmap = new OsuBeatmap();
for (int i = 0; i < maxCombo; i++)
beatmap.HitObjects.Add(new HitCircle());
processor.ApplyBeatmap(beatmap);
runForAlgorithm(name, colour,
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }),
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }),
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }),
() => (int)processor.TotalScore.Value);
}
private void runForAlgorithm(string name, Color4 colour, Action applyHit, Action applyNonPerfect, Action applyMiss, Func<int> getTotalScore)
{
int maxCombo = sliderMaxCombo.Current.Value;
List<float> results = new List<float>();
for (int i = 0; i < maxCombo; i++)
{
if (graphs.MissLocations.Contains(i))
applyMiss();
else if (graphs.NonPerfectLocations.Contains(i))
applyNonPerfect();
else
applyHit();
results.Add(getTotalScore());
}
graphs.Add(new LineGraph
{
Name = name,
RelativeSizeAxes = Axes.Both,
LineColour = colour,
Values = results
});
legend.Add(new OsuSpriteText
{
Colour = colour,
RelativeSizeAxes = Axes.X,
Width = 0.5f,
Text = $"{FontAwesome.Solid.Circle.Icon} {name}"
});
legend.Add(new OsuSpriteText
{
Colour = colour,
RelativeSizeAxes = Axes.X,
Width = 0.5f,
Text = $"final score {getTotalScore():#,0}"
});
}
}
public class GraphContainer : Container, IHasCustomTooltip<IEnumerable<LineGraph>>
{
public readonly BindableList<double> MissLocations = new BindableList<double>();
public readonly BindableList<double> NonPerfectLocations = new BindableList<double>();
public Bindable<int> MaxCombo = new Bindable<int>();
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
private readonly Box hoverLine;
private readonly Container missLines;
private readonly Container verticalGridLines;
public int CurrentHoverCombo { get; private set; }
public GraphContainer()
{
InternalChild = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = OsuColour.Gray(0.1f),
RelativeSizeAxes = Axes.Both,
},
verticalGridLines = new Container
{
RelativeSizeAxes = Axes.Both,
},
hoverLine = new Box
{
Colour = Color4.Yellow,
RelativeSizeAxes = Axes.Y,
Origin = Anchor.TopCentre,
Alpha = 0,
Width = 1,
},
missLines = new Container
{
Alpha = 0.6f,
RelativeSizeAxes = Axes.Both,
},
Content,
}
};
MissLocations.BindCollectionChanged((_, _) => updateMissLocations());
NonPerfectLocations.BindCollectionChanged((_, _) => updateMissLocations());
MaxCombo.BindValueChanged(_ =>
{
updateMissLocations();
updateVerticalGridLines();
}, true);
}
private void updateVerticalGridLines()
{
verticalGridLines.Clear();
for (int i = 0; i < MaxCombo.Value; i++)
{
if (i % 100 == 0)
{
verticalGridLines.AddRange(new Drawable[]
{
new Box
{
Colour = OsuColour.Gray(0.2f),
Origin = Anchor.TopCentre,
Width = 1,
RelativeSizeAxes = Axes.Y,
RelativePositionAxes = Axes.X,
X = (float)i / MaxCombo.Value,
},
new OsuSpriteText
{
RelativePositionAxes = Axes.X,
X = (float)i / MaxCombo.Value,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Text = $"{i:#,0}",
Rotation = -30,
Y = -20,
}
});
}
}
}
private void updateMissLocations()
{
missLines.Clear();
foreach (int miss in MissLocations)
{
missLines.Add(new Box
{
Colour = Color4.Red,
Origin = Anchor.TopCentre,
Width = 1,
RelativeSizeAxes = Axes.Y,
RelativePositionAxes = Axes.X,
X = (float)miss / MaxCombo.Value,
});
}
foreach (int miss in NonPerfectLocations)
{
missLines.Add(new Box
{
Colour = Color4.Orange,
Origin = Anchor.TopCentre,
Width = 1,
RelativeSizeAxes = Axes.Y,
RelativePositionAxes = Axes.X,
X = (float)miss / MaxCombo.Value,
});
}
}
protected override bool OnHover(HoverEvent e)
{
hoverLine.Show();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
hoverLine.Hide();
base.OnHoverLost(e);
}
protected override bool OnMouseMove(MouseMoveEvent e)
{
CurrentHoverCombo = (int)(e.MousePosition.X / DrawWidth * MaxCombo.Value);
hoverLine.X = e.MousePosition.X;
return base.OnMouseMove(e);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button == MouseButton.Left)
MissLocations.Add(CurrentHoverCombo);
else
NonPerfectLocations.Add(CurrentHoverCombo);
return true;
}
private GraphTooltip? tooltip;
public ITooltip<IEnumerable<LineGraph>> GetCustomTooltip() => tooltip ??= new GraphTooltip(this);
public IEnumerable<LineGraph> TooltipContent => Content.OfType<LineGraph>();
public class GraphTooltip : CompositeDrawable, ITooltip<IEnumerable<LineGraph>>
{
private readonly GraphContainer graphContainer;
private readonly OsuTextFlowContainer textFlow;
public GraphTooltip(GraphContainer graphContainer)
{
this.graphContainer = graphContainer;
AutoSizeAxes = Axes.Both;
Masking = true;
CornerRadius = 10;
InternalChildren = new Drawable[]
{
new Box
{
Colour = OsuColour.Gray(0.15f),
RelativeSizeAxes = Axes.Both,
},
textFlow = new OsuTextFlowContainer
{
Colour = Color4.White,
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding(10),
}
};
}
private int? lastContentCombo;
public void SetContent(IEnumerable<LineGraph> content)
{
int relevantCombo = graphContainer.CurrentHoverCombo;
if (lastContentCombo == relevantCombo)
return;
lastContentCombo = relevantCombo;
textFlow.Clear();
textFlow.AddParagraph($"At combo {relevantCombo}:");
foreach (var graph in content)
{
float valueAtHover = graph.Values.ElementAt(relevantCombo);
float ofTotal = valueAtHover / graph.Values.Last();
textFlow.AddParagraph($"{graph.Name}: {valueAtHover:#,0} ({ofTotal * 100:N0}% of final)\n", st => st.Colour = graph.LineColour);
}
}
public void Move(Vector2 pos) => this.MoveTo(pos);
}
}
}

View File

@ -214,7 +214,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void addControlPoints(IList<MultiplierControlPoint> controlPoints, double sequenceStartTime) private void addControlPoints(IList<MultiplierControlPoint> controlPoints, double sequenceStartTime)
{ {
controlPoints.ForEach(point => point.StartTime += sequenceStartTime); controlPoints.ForEach(point => point.Time += sequenceStartTime);
scrollContainers.ForEach(container => scrollContainers.ForEach(container =>
{ {
@ -224,7 +224,7 @@ namespace osu.Game.Tests.Visual.Gameplay
foreach (var playfield in playfields) foreach (var playfield in playfields)
{ {
foreach (var controlPoint in controlPoints) foreach (var controlPoint in controlPoints)
playfield.Add(createDrawablePoint(playfield, controlPoint.StartTime)); playfield.Add(createDrawablePoint(playfield, controlPoint.Time));
} }
} }

View File

@ -97,14 +97,23 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
[Test] [Test]
public void TestCurrentItemDoesNotHaveDeleteButton() public void TestSingleItemDoesNotHaveDeleteButton()
{
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode.Value == QueueMode.AllPlayers);
assertDeleteButtonVisibility(0, false);
}
[Test]
public void TestCurrentItemHasDeleteButtonIfNotSingle()
{ {
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode.Value == QueueMode.AllPlayers); AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode.Value == QueueMode.AllPlayers);
addPlaylistItem(() => API.LocalUser.Value.OnlineID); addPlaylistItem(() => API.LocalUser.Value.OnlineID);
assertDeleteButtonVisibility(0, false); assertDeleteButtonVisibility(0, true);
assertDeleteButtonVisibility(1, true); assertDeleteButtonVisibility(1, true);
AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely()); AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely());

View File

@ -25,6 +25,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Navigation namespace osu.Game.Tests.Visual.Navigation
{ {
@ -79,6 +80,16 @@ namespace osu.Game.Tests.Visual.Navigation
[Resolved] [Resolved]
private OsuGameBase gameBase { get; set; } private OsuGameBase gameBase { get; set; }
[Test]
public void TestCursorHidesWhenIdle()
{
AddStep("click mouse", () => InputManager.Click(MouseButton.Left));
AddUntilStep("wait until idle", () => Game.IsIdle.Value);
AddUntilStep("menu cursor hidden", () => Game.GlobalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 0);
AddStep("click mouse", () => InputManager.Click(MouseButton.Left));
AddUntilStep("menu cursor shown", () => Game.GlobalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 1);
}
[Test] [Test]
public void TestNullRulesetHandled() public void TestNullRulesetHandled()
{ {

View File

@ -0,0 +1,257 @@
// 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 System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Comments;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneCommentActions : OsuManualInputManagerTestScene
{
private Container<Drawable> content = null!;
protected override Container<Drawable> Content => content;
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
[Cached(typeof(IDialogOverlay))]
private readonly DialogOverlay dialogOverlay = new DialogOverlay();
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
private CommentsContainer commentsContainer = null!;
[BackgroundDependencyLoader]
private void load()
{
base.Content.AddRange(new Drawable[]
{
content = new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both
},
dialogOverlay
});
}
[SetUpSteps]
public void SetUp()
{
Schedule(() =>
{
API.Login("test", "test");
Child = commentsContainer = new CommentsContainer();
});
}
[Test]
public void TestNonOwnCommentCantBeDeleted()
{
addTestComments();
AddUntilStep("First comment has button", () =>
{
var comments = this.ChildrenOfType<DrawableComment>();
var ourComment = comments.SingleOrDefault(x => x.Comment.Id == 1);
return ourComment != null && ourComment.ChildrenOfType<OsuSpriteText>().Any(x => x.Text == "Delete");
});
AddAssert("Second doesn't", () =>
{
var comments = this.ChildrenOfType<DrawableComment>();
var ourComment = comments.Single(x => x.Comment.Id == 2);
return ourComment.ChildrenOfType<OsuSpriteText>().All(x => x.Text != "Delete");
});
}
private readonly ManualResetEventSlim deletionPerformed = new ManualResetEventSlim();
[Test]
public void TestDeletion()
{
DrawableComment? ourComment = null;
addTestComments();
AddUntilStep("Comment exists", () =>
{
var comments = this.ChildrenOfType<DrawableComment>();
ourComment = comments.SingleOrDefault(x => x.Comment.Id == 1);
return ourComment != null;
});
AddStep("It has delete button", () =>
{
var btn = ourComment.ChildrenOfType<OsuSpriteText>().Single(x => x.Text == "Delete");
InputManager.MoveMouseTo(btn);
});
AddStep("Click delete button", () =>
{
InputManager.Click(MouseButton.Left);
});
AddStep("Setup request handling", () =>
{
deletionPerformed.Reset();
dummyAPI.HandleRequest = request =>
{
if (!(request is CommentDeleteRequest req))
return false;
if (req.CommentId != 1)
return false;
CommentBundle cb = new CommentBundle
{
Comments = new List<Comment>
{
new Comment
{
Id = 2,
Message = "This is a comment by another user",
UserId = API.LocalUser.Value.Id + 1,
CreatedAt = DateTimeOffset.Now,
User = new APIUser
{
Id = API.LocalUser.Value.Id + 1,
Username = "Another user"
}
},
},
IncludedComments = new List<Comment>(),
PinnedComments = new List<Comment>(),
};
Task.Run(() =>
{
deletionPerformed.Wait(10000);
req.TriggerSuccess(cb);
});
return true;
};
});
AddStep("Confirm dialog", () => InputManager.Key(Key.Number1));
AddAssert("Loading spinner shown", () => commentsContainer.ChildrenOfType<LoadingSpinner>().Any(d => d.IsPresent));
AddStep("Complete request", () => deletionPerformed.Set());
AddUntilStep("Comment is deleted locally", () => this.ChildrenOfType<DrawableComment>().Single(x => x.Comment.Id == 1).WasDeleted);
}
[Test]
public void TestDeletionFail()
{
DrawableComment? ourComment = null;
bool delete = false;
addTestComments();
AddUntilStep("Comment exists", () =>
{
var comments = this.ChildrenOfType<DrawableComment>();
ourComment = comments.SingleOrDefault(x => x.Comment.Id == 1);
return ourComment != null;
});
AddStep("It has delete button", () =>
{
var btn = ourComment.ChildrenOfType<OsuSpriteText>().Single(x => x.Text == "Delete");
InputManager.MoveMouseTo(btn);
});
AddStep("Click delete button", () =>
{
InputManager.Click(MouseButton.Left);
});
AddStep("Setup request handling", () =>
{
dummyAPI.HandleRequest = request =>
{
if (request is not CommentDeleteRequest req)
return false;
req.TriggerFailure(new Exception());
delete = true;
return false;
};
});
AddStep("Confirm dialog", () => InputManager.Key(Key.Number1));
AddUntilStep("Deletion requested", () => delete);
AddUntilStep("Comment is available", () =>
{
return !this.ChildrenOfType<DrawableComment>().Single(x => x.Comment.Id == 1).WasDeleted;
});
AddAssert("Loading spinner hidden", () =>
{
return ourComment.ChildrenOfType<LoadingSpinner>().All(d => !d.IsPresent);
});
AddAssert("Actions available", () =>
{
return ourComment.ChildrenOfType<LinkFlowContainer>().Single(x => x.Name == @"Actions buttons").IsPresent;
});
}
private void addTestComments()
{
AddStep("set up response", () =>
{
CommentBundle cb = new CommentBundle
{
Comments = new List<Comment>
{
new Comment
{
Id = 1,
Message = "This is our comment",
UserId = API.LocalUser.Value.Id,
CreatedAt = DateTimeOffset.Now,
User = API.LocalUser.Value,
},
new Comment
{
Id = 2,
Message = "This is a comment by another user",
UserId = API.LocalUser.Value.Id + 1,
CreatedAt = DateTimeOffset.Now,
User = new APIUser
{
Id = API.LocalUser.Value.Id + 1,
Username = "Another user"
}
},
},
IncludedComments = new List<Comment>(),
PinnedComments = new List<Comment>(),
};
setUpCommentsResponse(cb);
});
AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123));
}
private void setUpCommentsResponse(CommentBundle commentBundle)
{
dummyAPI.HandleRequest = request =>
{
if (!(request is GetCommentsRequest getCommentsRequest))
return false;
getCommentsRequest.TriggerSuccess(commentBundle);
return true;
};
}
}
}

View File

@ -3,18 +3,17 @@
#nullable disable #nullable disable
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Tests.Visual.UserInterface;
namespace osu.Game.Tests.Visual.Settings namespace osu.Game.Tests.Visual.Settings
{ {
public class TestSceneDirectorySelector : OsuTestScene public class TestSceneDirectorySelector : ThemeComparisonTestScene
{ {
[BackgroundDependencyLoader] protected override Drawable CreateContent() => new OsuDirectorySelector
private void load()
{ {
Add(new OsuDirectorySelector { RelativeSizeAxes = Axes.Both }); RelativeSizeAxes = Axes.Both
} };
} }
} }

View File

@ -4,23 +4,43 @@
#nullable disable #nullable disable
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Tests.Visual.UserInterface;
namespace osu.Game.Tests.Visual.Settings namespace osu.Game.Tests.Visual.Settings
{ {
public class TestSceneFileSelector : OsuTestScene public class TestSceneFileSelector : ThemeComparisonTestScene
{ {
[Test] [Resolved]
public void TestAllFiles() private OsuColour colours { get; set; }
{
AddStep("create", () => Child = new OsuFileSelector { RelativeSizeAxes = Axes.Both });
}
[Test] [Test]
public void TestJpgFilesOnly() public void TestJpgFilesOnly()
{ {
AddStep("create", () => Child = new OsuFileSelector(validFileExtensions: new[] { ".jpg" }) { RelativeSizeAxes = Axes.Both }); AddStep("create", () =>
{
Cell(0, 0).Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.GreySeaFoam
},
new OsuFileSelector(validFileExtensions: new[] { ".jpg" })
{
RelativeSizeAxes = Axes.Both,
},
};
});
} }
protected override Drawable CreateContent() => new OsuFileSelector
{
RelativeSizeAxes = Axes.Both,
};
} }
} }

View File

@ -862,52 +862,6 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1); AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1);
} }
[Test]
public void TestRandomFallbackOnNonMatchingPrevious()
{
List<BeatmapSetInfo> manySets = new List<BeatmapSetInfo>();
AddStep("populate maps", () =>
{
manySets.Clear();
for (int i = 0; i < 10; i++)
{
manySets.Add(TestResources.CreateTestBeatmapSetInfo(3, new[]
{
// all taiko except for first
rulesets.GetRuleset(i > 0 ? 1 : 0)
}));
}
});
loadBeatmaps(manySets);
for (int i = 0; i < 10; i++)
{
AddStep("Reset filter", () => carousel.Filter(new FilterCriteria(), false));
AddStep("select first beatmap", () => carousel.SelectBeatmap(manySets.First().Beatmaps.First()));
AddStep("Toggle non-matching filter", () =>
{
carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false);
});
AddAssert("selection lost", () => carousel.SelectedBeatmapInfo == null);
AddStep("Restore different ruleset filter", () =>
{
carousel.Filter(new FilterCriteria { Ruleset = rulesets.GetRuleset(1) }, false);
eagerSelectedIDs.Add(carousel.SelectedBeatmapSet!.ID);
});
AddAssert("selection changed", () => !carousel.SelectedBeatmapInfo!.Equals(manySets.First().Beatmaps.First()));
}
AddAssert("Selection was random", () => eagerSelectedIDs.Count > 2);
}
[Test] [Test]
public void TestFilteringByUserStarDifficulty() public void TestFilteringByUserStarDifficulty()
{ {
@ -955,6 +909,63 @@ namespace osu.Game.Tests.Visual.SongSelect
checkVisibleItemCount(true, 15); checkVisibleItemCount(true, 15);
} }
[Test]
public void TestCarouselSelectsNextWhenPreviousIsFiltered()
{
List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>();
// 10 sets that go osu! -> taiko -> catch -> osu! -> ...
for (int i = 0; i < 10; i++)
{
var rulesetInfo = rulesets.AvailableRulesets.ElementAt(i % 3);
sets.Add(TestResources.CreateTestBeatmapSetInfo(5, new[] { rulesetInfo }));
}
// Sort mode is important to keep the ruleset order
loadBeatmaps(sets, () => new FilterCriteria { Sort = SortMode.Title });
setSelected(1, 1);
for (int i = 1; i < 10; i++)
{
var rulesetInfo = rulesets.AvailableRulesets.ElementAt(i % 3);
AddStep($"Set ruleset to {rulesetInfo.ShortName}", () =>
{
carousel.Filter(new FilterCriteria { Ruleset = rulesetInfo, Sort = SortMode.Title }, false);
});
waitForSelection(i + 1, 1);
}
}
[Test]
public void TestCarouselSelectsBackwardsWhenDistanceIsShorter()
{
List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>();
// 10 sets that go taiko, osu!, osu!, osu!, taiko, osu!, osu!, osu!, ...
for (int i = 0; i < 10; i++)
{
var rulesetInfo = rulesets.AvailableRulesets.ElementAt(i % 4 == 0 ? 1 : 0);
sets.Add(TestResources.CreateTestBeatmapSetInfo(5, new[] { rulesetInfo }));
}
// Sort mode is important to keep the ruleset order
loadBeatmaps(sets, () => new FilterCriteria { Sort = SortMode.Title });
for (int i = 2; i < 10; i += 4)
{
setSelected(i, 1);
AddStep("Set ruleset to taiko", () =>
{
carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1), Sort = SortMode.Title }, false);
});
waitForSelection(i - 1, 1);
AddStep("Remove ruleset filter", () =>
{
carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false);
});
}
}
private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null, Func<FilterCriteria> initialCriteria = null, Action<BeatmapCarousel> carouselAdjust = null, int? count = null, private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null, Func<FilterCriteria> initialCriteria = null, Action<BeatmapCarousel> carouselAdjust = null, int? count = null,
bool randomDifficulties = false) bool randomDifficulties = false)
{ {

View File

@ -6,14 +6,18 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Carousel;
using osu.Game.Tests.Online; using osu.Game.Tests.Online;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect namespace osu.Game.Tests.Visual.SongSelect
{ {
@ -41,17 +45,7 @@ namespace osu.Game.Tests.Visual.SongSelect
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {
AddStep("create carousel", () => AddStep("create carousel", () => Child = createCarousel());
{
Child = carousel = new BeatmapCarousel
{
RelativeSizeAxes = Axes.Both,
BeatmapSets = new List<BeatmapSetInfo>
{
(testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo()),
}
};
});
AddUntilStep("wait for load", () => carousel.BeatmapSetsLoaded); AddUntilStep("wait for load", () => carousel.BeatmapSetsLoaded);
@ -152,5 +146,62 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("wait for button enabled", () => getUpdateButton()?.Enabled.Value == true); AddUntilStep("wait for button enabled", () => getUpdateButton()?.Enabled.Value == true);
} }
[Test]
public void TestUpdateLocalBeatmap()
{
DialogOverlay dialogOverlay = null!;
UpdateBeatmapSetButton? updateButton = null;
AddStep("create carousel with dialog overlay", () =>
{
dialogOverlay = new DialogOverlay();
Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[] { (typeof(IDialogOverlay), dialogOverlay), },
Children = new Drawable[]
{
createCarousel(),
dialogOverlay,
},
};
});
AddStep("setup beatmap state", () =>
{
testBeatmapSetInfo.Beatmaps.First().OnlineMD5Hash = "different hash";
testBeatmapSetInfo.Beatmaps.First().LastOnlineUpdate = DateTimeOffset.Now;
testBeatmapSetInfo.Status = BeatmapOnlineStatus.LocallyModified;
carousel.UpdateBeatmapSet(testBeatmapSetInfo);
});
AddUntilStep("wait for update button", () => (updateButton = getUpdateButton()) != null);
AddStep("click button", () => updateButton.AsNonNull().TriggerClick());
AddAssert("dialog displayed", () => dialogOverlay.CurrentDialog is UpdateLocalConfirmationDialog);
AddStep("click confirmation", () =>
{
InputManager.MoveMouseTo(dialogOverlay.CurrentDialog.ChildrenOfType<PopupDialogButton>().First());
InputManager.PressButton(MouseButton.Left);
});
AddUntilStep("update started", () => beatmapDownloader.GetExistingDownload(testBeatmapSetInfo) != null);
AddStep("release mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
}
private BeatmapCarousel createCarousel()
{
return carousel = new BeatmapCarousel
{
RelativeSizeAxes = Axes.Both,
BeatmapSets = new List<BeatmapSetInfo>
{
(testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo()),
}
};
}
} }
} }

View File

@ -15,6 +15,7 @@ using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
{ {
@ -81,25 +82,24 @@ namespace osu.Game.Tests.Visual.UserInterface
}; };
AddToggleStep("Smooth transitions", b => cursorBoxes.ForEach(box => box.SmoothTransition = b)); AddToggleStep("Smooth transitions", b => cursorBoxes.ForEach(box => box.SmoothTransition = b));
testUserCursor();
testLocalCursor();
testUserCursorOverride();
testMultipleLocalCursors();
} }
[SetUp]
public void SetUp() => Schedule(moveOut);
/// <summary> /// <summary>
/// -- Green Box -- /// -- Green Box --
/// Tests whether hovering in and out of a drawable that provides the user cursor (green) /// Tests whether hovering in and out of a drawable that provides the user cursor (green)
/// results in the correct visibility state for that cursor. /// results in the correct visibility state for that cursor.
/// </summary> /// </summary>
private void testUserCursor() [Test]
public void TestUserCursor()
{ {
AddStep("Move to green area", () => InputManager.MoveMouseTo(cursorBoxes[0])); AddStep("Move to green area", () => InputManager.MoveMouseTo(cursorBoxes[0]));
AddAssert("Check green cursor visible", () => checkVisible(cursorBoxes[0].MenuCursor)); AddAssert("Check green cursor visible", () => checkVisible(cursorBoxes[0].Cursor));
AddAssert("Check green cursor at mouse", () => checkAtMouse(cursorBoxes[0].MenuCursor)); AddAssert("Check green cursor at mouse", () => checkAtMouse(cursorBoxes[0].Cursor));
AddStep("Move out", moveOut); AddStep("Move out", moveOut);
AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].MenuCursor)); AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].Cursor));
AddAssert("Check global cursor visible", () => checkVisible(globalCursorDisplay.MenuCursor)); AddAssert("Check global cursor visible", () => checkVisible(globalCursorDisplay.MenuCursor));
} }
@ -108,15 +108,16 @@ namespace osu.Game.Tests.Visual.UserInterface
/// Tests whether hovering in and out of a drawable that provides a local cursor (purple) /// Tests whether hovering in and out of a drawable that provides a local cursor (purple)
/// results in the correct visibility and state for that cursor. /// results in the correct visibility and state for that cursor.
/// </summary> /// </summary>
private void testLocalCursor() [Test]
public void TestLocalCursor()
{ {
AddStep("Move to purple area", () => InputManager.MoveMouseTo(cursorBoxes[3])); AddStep("Move to purple area", () => InputManager.MoveMouseTo(cursorBoxes[3]));
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor)); AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].MenuCursor)); AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].Cursor));
AddAssert("Check global cursor visible", () => checkVisible(globalCursorDisplay.MenuCursor)); AddAssert("Check global cursor visible", () => checkVisible(globalCursorDisplay.MenuCursor));
AddAssert("Check global cursor at mouse", () => checkAtMouse(globalCursorDisplay.MenuCursor)); AddAssert("Check global cursor at mouse", () => checkAtMouse(globalCursorDisplay.MenuCursor));
AddStep("Move out", moveOut); AddStep("Move out", moveOut);
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor)); AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
AddAssert("Check global cursor visible", () => checkVisible(globalCursorDisplay.MenuCursor)); AddAssert("Check global cursor visible", () => checkVisible(globalCursorDisplay.MenuCursor));
} }
@ -125,47 +126,98 @@ namespace osu.Game.Tests.Visual.UserInterface
/// Tests whether overriding a user cursor (green) with another user cursor (blue) /// Tests whether overriding a user cursor (green) with another user cursor (blue)
/// results in the correct visibility and states for the cursors. /// results in the correct visibility and states for the cursors.
/// </summary> /// </summary>
private void testUserCursorOverride() [Test]
public void TestUserCursorOverride()
{ {
AddStep("Move to blue-green boundary", () => InputManager.MoveMouseTo(cursorBoxes[1].ScreenSpaceDrawQuad.BottomRight - new Vector2(10))); AddStep("Move to blue-green boundary", () => InputManager.MoveMouseTo(cursorBoxes[1].ScreenSpaceDrawQuad.BottomRight - new Vector2(10)));
AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].MenuCursor)); AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].Cursor));
AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].MenuCursor)); AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].Cursor));
AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].MenuCursor)); AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].Cursor));
AddStep("Move out", moveOut); AddStep("Move out", moveOut);
AddAssert("Check blue cursor not visible", () => !checkVisible(cursorBoxes[1].MenuCursor)); AddAssert("Check blue cursor not visible", () => !checkVisible(cursorBoxes[1].Cursor));
AddAssert("Check green cursor not visible", () => !checkVisible(cursorBoxes[0].MenuCursor)); AddAssert("Check green cursor not visible", () => !checkVisible(cursorBoxes[0].Cursor));
} }
/// <summary> /// <summary>
/// -- Yellow-Purple Box Boundary -- /// -- Yellow-Purple Box Boundary --
/// Tests whether multiple local cursors (purple + yellow) may be visible and at the mouse position at the same time. /// Tests whether multiple local cursors (purple + yellow) may be visible and at the mouse position at the same time.
/// </summary> /// </summary>
private void testMultipleLocalCursors() [Test]
public void TestMultipleLocalCursors()
{ {
AddStep("Move to yellow-purple boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.BottomRight - new Vector2(10))); AddStep("Move to yellow-purple boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.BottomRight - new Vector2(10)));
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor)); AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].MenuCursor)); AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].Cursor));
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor)); AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].MenuCursor)); AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].Cursor));
AddStep("Move out", moveOut); AddStep("Move out", moveOut);
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor)); AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor)); AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
} }
/// <summary> /// <summary>
/// -- Yellow-Blue Box Boundary -- /// -- Yellow-Blue Box Boundary --
/// Tests whether a local cursor (yellow) may be displayed along with a user cursor override (blue). /// Tests whether a local cursor (yellow) may be displayed along with a user cursor override (blue).
/// </summary> /// </summary>
private void testUserOverrideWithLocal() [Test]
public void TestUserOverrideWithLocal()
{ {
AddStep("Move to yellow-blue boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.TopRight - new Vector2(10))); AddStep("Move to yellow-blue boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.TopRight - new Vector2(10, 0)));
AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].MenuCursor)); AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].Cursor));
AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].MenuCursor)); AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].Cursor));
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor)); AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].MenuCursor)); AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].Cursor));
AddStep("Move out", moveOut); AddStep("Move out", moveOut);
AddAssert("Check blue cursor invisible", () => !checkVisible(cursorBoxes[1].MenuCursor)); AddAssert("Check blue cursor invisible", () => !checkVisible(cursorBoxes[1].Cursor));
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor)); AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
}
/// <summary>
/// Ensures non-mouse input hides global cursor on a "local cursor" area (which doesn't hide global cursor).
/// </summary>
[Test]
public void TestKeyboardLocalCursor([Values] bool clickToShow)
{
AddStep("Enable cursor hiding", () => globalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = true);
AddStep("Move to purple area", () => InputManager.MoveMouseTo(cursorBoxes[3].ScreenSpaceDrawQuad.Centre + new Vector2(10, 0)));
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
AddAssert("Check global cursor alpha is 1", () => globalCursorDisplay.MenuCursor.Alpha == 1);
AddStep("Press key", () => InputManager.Key(Key.A));
AddAssert("Check purple cursor still visible", () => checkVisible(cursorBoxes[3].Cursor));
AddUntilStep("Check global cursor alpha is 0", () => globalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 0);
if (clickToShow)
AddStep("Click mouse", () => InputManager.Click(MouseButton.Left));
else
AddStep("Move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + Vector2.One));
AddAssert("Check purple cursor still visible", () => checkVisible(cursorBoxes[3].Cursor));
AddUntilStep("Check global cursor alpha is 1", () => globalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 1);
}
/// <summary>
/// Ensures mouse input after non-mouse input doesn't show global cursor on a "user cursor" area (which hides global cursor).
/// </summary>
[Test]
public void TestKeyboardUserCursor([Values] bool clickToShow)
{
AddStep("Enable cursor hiding", () => globalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = true);
AddStep("Move to green area", () => InputManager.MoveMouseTo(cursorBoxes[0]));
AddAssert("Check green cursor visible", () => checkVisible(cursorBoxes[0].Cursor));
AddAssert("Check global cursor alpha is 0", () => !checkVisible(globalCursorDisplay.MenuCursor) && globalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 0);
AddStep("Press key", () => InputManager.Key(Key.A));
AddAssert("Check green cursor still visible", () => checkVisible(cursorBoxes[0].Cursor));
AddAssert("Check global cursor alpha is still 0", () => !checkVisible(globalCursorDisplay.MenuCursor) && globalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 0);
if (clickToShow)
AddStep("Click mouse", () => InputManager.Click(MouseButton.Left));
else
AddStep("Move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + Vector2.One));
AddAssert("Check green cursor still visible", () => checkVisible(cursorBoxes[0].Cursor));
AddAssert("Check global cursor alpha is still 0", () => !checkVisible(globalCursorDisplay.MenuCursor) && globalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 0);
} }
/// <summary> /// <summary>
@ -191,7 +243,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
public bool SmoothTransition; public bool SmoothTransition;
public CursorContainer MenuCursor { get; } public CursorContainer Cursor { get; }
public bool ProvidingUserCursor { get; } public bool ProvidingUserCursor { get; }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || (SmoothTransition && !ProvidingUserCursor); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || (SmoothTransition && !ProvidingUserCursor);
@ -218,7 +270,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Origin = Anchor.Centre, Origin = Anchor.Centre,
Text = providesUserCursor ? "User cursor" : "Local cursor" Text = providesUserCursor ? "User cursor" : "Local cursor"
}, },
MenuCursor = new TestCursorContainer Cursor = new TestCursorContainer
{ {
State = { Value = providesUserCursor ? Visibility.Hidden : Visibility.Visible }, State = { Value = providesUserCursor ? Visibility.Hidden : Visibility.Visible },
} }

View File

@ -5,12 +5,14 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
@ -20,11 +22,17 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
private SettingsToolboxGroup group; private SettingsToolboxGroup group;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
[SetUp] [SetUp]
public void SetUp() => Schedule(() => public void SetUp() => Schedule(() =>
{ {
Child = group = new SettingsToolboxGroup("example") Child = group = new SettingsToolboxGroup("example")
{ {
Scale = new Vector2(3),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[] Children = new Drawable[]
{ {
new RoundedButton new RoundedButton

View File

@ -6,6 +6,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Drawing; using System.Drawing;
using System.Globalization;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace osu.Game.Tournament namespace osu.Game.Tournament
@ -31,7 +32,9 @@ namespace osu.Game.Tournament
Debug.Assert(str != null); Debug.Assert(str != null);
return new PointConverter().ConvertFromString(str) as Point? ?? new Point(); // Null check suppression is required due to .NET standard expecting a non-null context.
// Seems to work fine at a runtime level (and the parameter is nullable in .NET 6+).
return new PointConverter().ConvertFromString(null!, CultureInfo.InvariantCulture, str) as Point? ?? new Point();
} }
var point = new Point(); var point = new Point();

View File

@ -239,17 +239,17 @@ namespace osu.Game.Tournament.Screens.Editors
var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = Model.ID }); var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = Model.ID });
req.Success += res => req.Success += res => Schedule(() =>
{ {
Model.Beatmap = new TournamentBeatmap(res); Model.Beatmap = new TournamentBeatmap(res);
updatePanel(); updatePanel();
}; });
req.Failure += _ => req.Failure += _ => Schedule(() =>
{ {
Model.Beatmap = null; Model.Beatmap = null;
updatePanel(); updatePanel();
}; });
API.Queue(req); API.Queue(req);
}, true); }, true);

View File

@ -141,18 +141,9 @@ namespace osu.Game.Beatmaps
// Handle collections using permissive difficulty name to track difficulties. // Handle collections using permissive difficulty name to track difficulties.
foreach (var originalBeatmap in original.Beatmaps) foreach (var originalBeatmap in original.Beatmaps)
{ {
var updatedBeatmap = updated.Beatmaps.FirstOrDefault(b => b.DifficultyName == originalBeatmap.DifficultyName); updated.Beatmaps
.FirstOrDefault(b => b.DifficultyName == originalBeatmap.DifficultyName)?
if (updatedBeatmap == null) .TransferCollectionReferences(realm, originalBeatmap.MD5Hash);
continue;
var collections = realm.All<BeatmapCollection>().AsEnumerable().Where(c => c.BeatmapMD5Hashes.Contains(originalBeatmap.MD5Hash));
foreach (var c in collections)
{
c.BeatmapMD5Hashes.Remove(originalBeatmap.MD5Hash);
c.BeatmapMD5Hashes.Add(updatedBeatmap.MD5Hash);
}
} }
} }

View File

@ -8,6 +8,7 @@ using JetBrains.Annotations;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Collections;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Models; using osu.Game.Models;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
@ -213,6 +214,23 @@ namespace osu.Game.Beatmaps
return fileHashX == fileHashY; return fileHashX == fileHashY;
} }
/// <summary>
/// When updating a beatmap, its hashes will change. Collections currently track beatmaps by hash, so they need to be updated.
/// This method will handle updating
/// </summary>
/// <param name="realm">A realm instance in an active write transaction.</param>
/// <param name="previousMD5Hash">The previous MD5 hash of the beatmap before update.</param>
public void TransferCollectionReferences(Realm realm, string previousMD5Hash)
{
var collections = realm.All<BeatmapCollection>().AsEnumerable().Where(c => c.BeatmapMD5Hashes.Contains(previousMD5Hash));
foreach (var c in collections)
{
c.BeatmapMD5Hashes.Remove(previousMD5Hash);
c.BeatmapMD5Hashes.Add(MD5Hash);
}
}
IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata; IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet; IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
IRulesetInfo IBeatmapInfo.Ruleset => Ruleset; IRulesetInfo IBeatmapInfo.Ruleset => Ruleset;

View File

@ -311,6 +311,8 @@ namespace osu.Game.Beatmaps
if (existingFileInfo != null) if (existingFileInfo != null)
DeleteFile(setInfo, existingFileInfo); DeleteFile(setInfo, existingFileInfo);
string oldMd5Hash = beatmapInfo.MD5Hash;
beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
beatmapInfo.Hash = stream.ComputeSHA2Hash(); beatmapInfo.Hash = stream.ComputeSHA2Hash();
@ -327,6 +329,8 @@ namespace osu.Game.Beatmaps
setInfo.CopyChangesToRealm(liveBeatmapSet); setInfo.CopyChangesToRealm(liveBeatmapSet);
beatmapInfo.TransferCollectionReferences(r, oldMd5Hash);
ProcessBeatmap?.Invoke((liveBeatmapSet, false)); ProcessBeatmap?.Invoke((liveBeatmapSet, false));
}); });
} }

View File

@ -9,11 +9,8 @@ using osuTK.Graphics;
namespace osu.Game.Beatmaps.ControlPoints namespace osu.Game.Beatmaps.ControlPoints
{ {
public abstract class ControlPoint : IComparable<ControlPoint>, IDeepCloneable<ControlPoint>, IEquatable<ControlPoint> public abstract class ControlPoint : IComparable<ControlPoint>, IDeepCloneable<ControlPoint>, IEquatable<ControlPoint>, IControlPoint
{ {
/// <summary>
/// The time at which the control point takes effect.
/// </summary>
[JsonIgnore] [JsonIgnore]
public double Time { get; set; } public double Time { get; set; }

View File

@ -196,8 +196,8 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <param name="time">The time to find the control point at.</param> /// <param name="time">The time to find the control point at.</param>
/// <param name="fallback">The control point to use when <paramref name="time"/> is before any control points.</param> /// <param name="fallback">The control point to use when <paramref name="time"/> is before any control points.</param>
/// <returns>The active control point at <paramref name="time"/>, or a fallback <see cref="ControlPoint"/> if none found.</returns> /// <returns>The active control point at <paramref name="time"/>, or a fallback <see cref="ControlPoint"/> if none found.</returns>
protected T BinarySearchWithFallback<T>(IReadOnlyList<T> list, double time, T fallback) public static T BinarySearchWithFallback<T>(IReadOnlyList<T> list, double time, T fallback)
where T : ControlPoint where T : class, IControlPoint
{ {
return BinarySearch(list, time) ?? fallback; return BinarySearch(list, time) ?? fallback;
} }
@ -207,9 +207,9 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary> /// </summary>
/// <param name="list">The list to search.</param> /// <param name="list">The list to search.</param>
/// <param name="time">The time to find the control point at.</param> /// <param name="time">The time to find the control point at.</param>
/// <returns>The active control point at <paramref name="time"/>.</returns> /// <returns>The active control point at <paramref name="time"/>. Will return <c>null</c> if there are no control points, or if the time is before the first control point.</returns>
protected virtual T BinarySearch<T>(IReadOnlyList<T> list, double time) public static T BinarySearch<T>(IReadOnlyList<T> list, double time)
where T : ControlPoint where T : class, IControlPoint
{ {
if (list == null) if (list == null)
throw new ArgumentNullException(nameof(list)); throw new ArgumentNullException(nameof(list));

View File

@ -0,0 +1,13 @@
// 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.Beatmaps.ControlPoints
{
public interface IControlPoint
{
/// <summary>
/// The time at which the control point takes effect.
/// </summary>
double Time { get; }
}
}

View File

@ -355,6 +355,14 @@ namespace osu.Game.Beatmaps.Formats
switch (type) switch (type)
{ {
case LegacyEventType.Sprite:
// Generally, the background is the first thing defined in a beatmap file.
// In some older beatmaps, it is not present and replaced by a storyboard-level background instead.
// Allow the first sprite (by file order) to act as the background in such cases.
if (string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.BackgroundFile))
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[3]);
break;
case LegacyEventType.Background: case LegacyEventType.Background:
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]); beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]);
break; break;
@ -427,8 +435,10 @@ namespace osu.Game.Beatmaps.Formats
addControlPoint(time, controlPoint, true); addControlPoint(time, controlPoint, true);
} }
int onlineRulesetID = beatmap.BeatmapInfo.Ruleset.OnlineID;
#pragma warning disable 618 #pragma warning disable 618
addControlPoint(time, new LegacyDifficultyControlPoint(beatLength) addControlPoint(time, new LegacyDifficultyControlPoint(onlineRulesetID, beatLength)
#pragma warning restore 618 #pragma warning restore 618
{ {
SliderVelocity = speedMultiplier, SliderVelocity = speedMultiplier,

View File

@ -174,11 +174,15 @@ namespace osu.Game.Beatmaps.Formats
/// </summary> /// </summary>
public bool GenerateTicks { get; private set; } = true; public bool GenerateTicks { get; private set; } = true;
public LegacyDifficultyControlPoint(double beatLength) public LegacyDifficultyControlPoint(int rulesetId, double beatLength)
: this() : this()
{ {
// Note: In stable, the division occurs on floats, but with compiler optimisations turned on actually seems to occur on doubles via some .NET black magic (possibly inlining?). // Note: In stable, the division occurs on floats, but with compiler optimisations turned on actually seems to occur on doubles via some .NET black magic (possibly inlining?).
BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100.0 : 1; if (rulesetId == 1 || rulesetId == 3)
BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100.0 : 1;
else
BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 1000) / 100.0 : 1;
GenerateTicks = !double.IsNaN(beatLength); GenerateTicks = !double.IsNaN(beatLength);
} }

View File

@ -134,6 +134,6 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// Reads the correct track restart point from beatmap metadata and sets looping to enabled. /// Reads the correct track restart point from beatmap metadata and sets looping to enabled.
/// </summary> /// </summary>
void PrepareTrackForPreview(bool looping); void PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint = 0);
} }
} }

View File

@ -110,7 +110,7 @@ namespace osu.Game.Beatmaps
public Track LoadTrack() => track = GetBeatmapTrack() ?? GetVirtualTrack(1000); public Track LoadTrack() => track = GetBeatmapTrack() ?? GetVirtualTrack(1000);
public void PrepareTrackForPreview(bool looping) public void PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint = 0)
{ {
Track.Looping = looping; Track.Looping = looping;
Track.RestartPoint = Metadata.PreviewTime; Track.RestartPoint = Metadata.PreviewTime;
@ -125,6 +125,8 @@ namespace osu.Game.Beatmaps
Track.RestartPoint = 0.4f * Track.Length; Track.RestartPoint = 0.4f * Track.Length;
} }
Track.RestartPoint += offsetFromPreviewPoint;
} }
/// <summary> /// <summary>

View File

@ -294,15 +294,38 @@ namespace osu.Game.Database
// Log output here will be missing a valid hash in non-batch imports. // Log output here will be missing a valid hash in non-batch imports.
LogForModel(item, $@"Beginning import from {archive?.Name ?? "unknown"}..."); LogForModel(item, $@"Beginning import from {archive?.Name ?? "unknown"}...");
List<RealmNamedFileUsage> files = new List<RealmNamedFileUsage>();
if (archive != null)
{
// Import files to the disk store.
// We intentionally delay adding to realm to avoid blocking on a write during disk operations.
foreach (var filenames in getShortenedFilenames(archive))
{
using (Stream s = archive.GetStream(filenames.original))
files.Add(new RealmNamedFileUsage(Files.Add(s, realm, false), filenames.shortened));
}
}
using (var transaction = realm.BeginWrite())
{
// Add all files to realm in one go.
// This is done ahead of the main transaction to ensure we can correctly cleanup the files, even if the import fails.
foreach (var file in files)
{
if (!file.File.IsManaged)
realm.Add(file.File, true);
}
transaction.Commit();
}
item.Files.AddRange(files);
item.Hash = ComputeHash(item);
// TODO: do we want to make the transaction this local? not 100% sure, will need further investigation. // TODO: do we want to make the transaction this local? not 100% sure, will need further investigation.
using (var transaction = realm.BeginWrite()) using (var transaction = realm.BeginWrite())
{ {
if (archive != null)
// TODO: look into rollback of file additions (or delayed commit).
item.Files.AddRange(createFileInfos(archive, Files, realm));
item.Hash = ComputeHash(item);
// TODO: we may want to run this outside of the transaction. // TODO: we may want to run this outside of the transaction.
Populate(item, archive, realm, cancellationToken); Populate(item, archive, realm, cancellationToken);
@ -425,16 +448,6 @@ namespace osu.Game.Database
{ {
var fileInfos = new List<RealmNamedFileUsage>(); var fileInfos = new List<RealmNamedFileUsage>();
// import files to manager
foreach (var filenames in getShortenedFilenames(reader))
{
using (Stream s = reader.GetStream(filenames.original))
{
var item = new RealmNamedFileUsage(files.Add(s, realm), filenames.shortened);
fileInfos.Add(item);
}
}
return fileInfos; return fileInfos;
} }

View File

@ -40,8 +40,8 @@ namespace osu.Game.Database
/// </summary> /// </summary>
/// <param name="data">The file data stream.</param> /// <param name="data">The file data stream.</param>
/// <param name="realm">The realm instance to add to. Should already be in a transaction.</param> /// <param name="realm">The realm instance to add to. Should already be in a transaction.</param>
/// <returns></returns> /// <param name="addToRealm">Whether the <see cref="RealmFile"/> should immediately be added to the underlying realm. If <c>false</c> is provided here, the instance must be manually added.</param>
public RealmFile Add(Stream data, Realm realm) public RealmFile Add(Stream data, Realm realm, bool addToRealm = true)
{ {
string hash = data.ComputeSHA2Hash(); string hash = data.ComputeSHA2Hash();
@ -52,7 +52,7 @@ namespace osu.Game.Database
if (!checkFileExistsAndMatchesHash(file)) if (!checkFileExistsAndMatchesHash(file))
copyToStore(file, data); copyToStore(file, data);
if (!file.IsManaged) if (addToRealm && !file.IsManaged)
realm.Add(file); realm.Add(file);
return file; return file;

View File

@ -13,7 +13,7 @@ using osu.Game.Configuration;
namespace osu.Game.Graphics.Cursor namespace osu.Game.Graphics.Cursor
{ {
/// <summary> /// <summary>
/// A container which provides the main <see cref="Cursor.MenuCursor"/>. /// A container which provides the main <see cref="MenuCursorContainer"/>.
/// Also handles cases where a more localised cursor is provided by another component (via <see cref="IProvideCursor"/>). /// Also handles cases where a more localised cursor is provided by another component (via <see cref="IProvideCursor"/>).
/// </summary> /// </summary>
public class GlobalCursorDisplay : Container, IProvideCursor public class GlobalCursorDisplay : Container, IProvideCursor
@ -23,7 +23,9 @@ namespace osu.Game.Graphics.Cursor
/// </summary> /// </summary>
internal bool ShowCursor = true; internal bool ShowCursor = true;
public CursorContainer MenuCursor { get; } CursorContainer IProvideCursor.Cursor => MenuCursor;
public MenuCursorContainer MenuCursor { get; }
public bool ProvidingUserCursor => true; public bool ProvidingUserCursor => true;
@ -42,8 +44,8 @@ namespace osu.Game.Graphics.Cursor
{ {
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
{ {
MenuCursor = new MenuCursor { State = { Value = Visibility.Hidden } }, Content = new Container { RelativeSizeAxes = Axes.Both },
Content = new Container { RelativeSizeAxes = Axes.Both } MenuCursor = new MenuCursorContainer { State = { Value = Visibility.Hidden } }
}); });
} }
@ -64,7 +66,7 @@ namespace osu.Game.Graphics.Cursor
if (!hasValidInput || !ShowCursor) if (!hasValidInput || !ShowCursor)
{ {
currentOverrideProvider?.MenuCursor?.Hide(); currentOverrideProvider?.Cursor?.Hide();
currentOverrideProvider = null; currentOverrideProvider = null;
return; return;
} }
@ -83,8 +85,8 @@ namespace osu.Game.Graphics.Cursor
if (currentOverrideProvider == newOverrideProvider) if (currentOverrideProvider == newOverrideProvider)
return; return;
currentOverrideProvider?.MenuCursor?.Hide(); currentOverrideProvider?.Cursor?.Hide();
newOverrideProvider.MenuCursor?.Show(); newOverrideProvider.Cursor?.Show();
currentOverrideProvider = newOverrideProvider; currentOverrideProvider = newOverrideProvider;
} }

View File

@ -17,10 +17,10 @@ namespace osu.Game.Graphics.Cursor
/// The cursor provided by this <see cref="IDrawable"/>. /// The cursor provided by this <see cref="IDrawable"/>.
/// May be null if no cursor should be visible. /// May be null if no cursor should be visible.
/// </summary> /// </summary>
CursorContainer MenuCursor { get; } CursorContainer Cursor { get; }
/// <summary> /// <summary>
/// Whether <see cref="MenuCursor"/> should be displayed as the singular user cursor. This will temporarily hide any other user cursor. /// Whether <see cref="Cursor"/> should be displayed as the singular user cursor. This will temporarily hide any other user cursor.
/// This value is checked every frame and may be used to control whether multiple cursors are displayed (e.g. watching replays). /// This value is checked every frame and may be used to control whether multiple cursors are displayed (e.g. watching replays).
/// </summary> /// </summary>
bool ProvidingUserCursor { get; } bool ProvidingUserCursor { get; }

Some files were not shown because too many files have changed in this diff Show More