mirror of
https://github.com/ppy/osu.git
synced 2025-03-14 07:07:19 +08:00
Merge branch 'master' into move-already-placed-objects-when-adjusting-offset-bpm
This commit is contained in:
commit
d5ef32e46b
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.809.2" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.831.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
if (withModifiedSkin)
|
||||
{
|
||||
AddStep("change component scale", () => Player.ChildrenOfType<LegacyScoreCounter>().First().Scale = new Vector2(2f));
|
||||
AddStep("update target", () => Player.ChildrenOfType<SkinComponentsContainer>().ForEach(LegacySkin.UpdateDrawableTarget));
|
||||
AddStep("update target", () => Player.ChildrenOfType<SkinnableContainer>().ForEach(LegacySkin.UpdateDrawableTarget));
|
||||
AddStep("exit player", () => Player.Exit());
|
||||
CreateTest();
|
||||
}
|
||||
|
@ -254,5 +254,7 @@ namespace osu.Game.Rulesets.Catch
|
||||
|
||||
return adjustedDifficulty;
|
||||
}
|
||||
|
||||
public override bool EditorShowScrollSpeed => false;
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch
|
||||
{
|
||||
public class CatchSkinComponentLookup : GameplaySkinComponentLookup<CatchSkinComponents>
|
||||
public class CatchSkinComponentLookup : SkinComponentLookup<CatchSkinComponents>
|
||||
{
|
||||
public CatchSkinComponentLookup(CatchSkinComponents component)
|
||||
: base(component)
|
||||
|
@ -16,9 +16,12 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
{
|
||||
public new CatchBeatmap Beatmap => (CatchBeatmap)base.Beatmap;
|
||||
|
||||
private readonly float halfCatcherWidth;
|
||||
|
||||
public CatchAutoGenerator(IBeatmap beatmap)
|
||||
: base(beatmap)
|
||||
{
|
||||
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) * 0.5f;
|
||||
}
|
||||
|
||||
protected override void GenerateFrames()
|
||||
@ -47,10 +50,7 @@ namespace osu.Game.Rulesets.Catch.Replays
|
||||
bool dashRequired = speedRequired > Catcher.BASE_WALK_SPEED;
|
||||
bool impossibleJump = speedRequired > Catcher.BASE_DASH_SPEED;
|
||||
|
||||
// todo: get correct catcher size, based on difficulty CS.
|
||||
const float catcher_width_half = Catcher.BASE_SIZE * 0.3f * 0.5f;
|
||||
|
||||
if (lastPosition - catcher_width_half < h.EffectiveX && lastPosition + catcher_width_half > h.EffectiveX)
|
||||
if (lastPosition - halfCatcherWidth < h.EffectiveX && lastPosition + halfCatcherWidth > h.EffectiveX)
|
||||
{
|
||||
// we are already in the correct range.
|
||||
lastTime = h.StartTime;
|
||||
|
@ -30,23 +30,19 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
{
|
||||
switch (lookup)
|
||||
{
|
||||
case SkinComponentsContainerLookup containerLookup:
|
||||
case GlobalSkinnableContainerLookup containerLookup:
|
||||
// Only handle per ruleset defaults here.
|
||||
if (containerLookup.Ruleset == null)
|
||||
return base.GetDrawableComponent(lookup);
|
||||
|
||||
// Skin has configuration.
|
||||
if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d)
|
||||
return d;
|
||||
|
||||
// we don't have enough assets to display these components (this is especially the case on a "beatmap" skin).
|
||||
if (!IsProvidingLegacyResources)
|
||||
return null;
|
||||
|
||||
// Our own ruleset components default.
|
||||
switch (containerLookup.Target)
|
||||
switch (containerLookup.Lookup)
|
||||
{
|
||||
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
|
||||
case GlobalSkinnableContainers.MainHUDComponents:
|
||||
// todo: remove CatchSkinComponents.CatchComboCounter and refactor LegacyCatchComboCounter to be added here instead.
|
||||
return new DefaultSkinComponentsContainer(container =>
|
||||
{
|
||||
@ -56,10 +52,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
{
|
||||
// set the anchor to top right so that it won't squash to the return button to the top
|
||||
keyCounter.Anchor = Anchor.CentreRight;
|
||||
keyCounter.Origin = Anchor.CentreRight;
|
||||
keyCounter.X = 0;
|
||||
// 340px is the default height inherit from stable
|
||||
keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y;
|
||||
keyCounter.Origin = Anchor.TopRight;
|
||||
keyCounter.Position = new Vector2(0, -40) * 1.6f;
|
||||
}
|
||||
})
|
||||
{
|
||||
|
@ -85,9 +85,25 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
|
||||
protected void SetTexture(Texture? texture, Texture? overlayTexture)
|
||||
{
|
||||
colouredSprite.Texture = texture;
|
||||
overlaySprite.Texture = overlayTexture;
|
||||
hyperSprite.Texture = texture;
|
||||
// Sizes are reset due to an arguable osu!framework bug where Sprite retains the size of the first set texture.
|
||||
|
||||
if (colouredSprite.Texture != texture)
|
||||
{
|
||||
colouredSprite.Size = Vector2.Zero;
|
||||
colouredSprite.Texture = texture;
|
||||
}
|
||||
|
||||
if (overlaySprite.Texture != overlayTexture)
|
||||
{
|
||||
overlaySprite.Size = Vector2.Zero;
|
||||
overlaySprite.Texture = overlayTexture;
|
||||
}
|
||||
|
||||
if (hyperSprite.Texture != texture)
|
||||
{
|
||||
hyperSprite.Size = Vector2.Zero;
|
||||
hyperSprite.Texture = texture;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -110,9 +110,9 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
if (Catcher.Dashing || Catcher.HyperDashing)
|
||||
{
|
||||
double generationInterval = Catcher.HyperDashing ? 25 : 50;
|
||||
const double trail_generation_interval = 16;
|
||||
|
||||
if (Time.Current - catcherTrails.LastDashTrailTime >= generationInterval)
|
||||
if (Time.Current - catcherTrails.LastDashTrailTime >= trail_generation_interval)
|
||||
displayCatcherTrail(Catcher.HyperDashing ? CatcherTrailAnimation.HyperDashing : CatcherTrailAnimation.Dashing);
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
public abstract partial class ManiaSkinnableTestScene : SkinnableTestScene
|
||||
{
|
||||
[Cached(Type = typeof(IScrollingInfo))]
|
||||
private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo();
|
||||
protected readonly TestScrollingInfo ScrollingInfo = new TestScrollingInfo();
|
||||
|
||||
[Cached]
|
||||
private readonly StageDefinition stage = new StageDefinition(4);
|
||||
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
|
||||
protected ManiaSkinnableTestScene()
|
||||
{
|
||||
scrollingInfo.Direction.Value = ScrollingDirection.Down;
|
||||
ScrollingInfo.Direction.Value = ScrollingDirection.Down;
|
||||
|
||||
Add(new Box
|
||||
{
|
||||
@ -43,16 +43,16 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
[Test]
|
||||
public void TestScrollingDown()
|
||||
{
|
||||
AddStep("change direction to down", () => scrollingInfo.Direction.Value = ScrollingDirection.Down);
|
||||
AddStep("change direction to down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScrollingUp()
|
||||
{
|
||||
AddStep("change direction to up", () => scrollingInfo.Direction.Value = ScrollingDirection.Up);
|
||||
AddStep("change direction to up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
|
||||
}
|
||||
|
||||
private class TestScrollingInfo : IScrollingInfo
|
||||
protected class TestScrollingInfo : IScrollingInfo
|
||||
{
|
||||
public readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
|
@ -1,13 +1,17 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Argon;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Legacy;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
@ -17,22 +21,75 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
[Cached]
|
||||
private ScoreProcessor scoreProcessor = new ScoreProcessor(new ManiaRuleset());
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
[Test]
|
||||
public void TestDisplay()
|
||||
{
|
||||
AddStep("setup", () => SetContents(s =>
|
||||
{
|
||||
if (s is ArgonSkin)
|
||||
return new ArgonManiaComboCounter();
|
||||
|
||||
if (s is LegacySkin)
|
||||
return new LegacyManiaComboCounter();
|
||||
|
||||
return new LegacyManiaComboCounter();
|
||||
}));
|
||||
|
||||
setup(Anchor.Centre);
|
||||
AddRepeatStep("perform hit", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Great }), 20);
|
||||
AddStep("perform miss", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAnchorOrigin()
|
||||
{
|
||||
AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
|
||||
setup(Anchor.TopCentre, 20);
|
||||
AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
|
||||
check(Anchor.BottomCentre, -20);
|
||||
|
||||
AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
|
||||
setup(Anchor.BottomCentre, -20);
|
||||
AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
|
||||
check(Anchor.TopCentre, 20);
|
||||
|
||||
AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
|
||||
setup(Anchor.Centre, 20);
|
||||
AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
|
||||
check(Anchor.Centre, 20);
|
||||
|
||||
AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
|
||||
setup(Anchor.Centre, -20);
|
||||
AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
|
||||
check(Anchor.Centre, -20);
|
||||
}
|
||||
|
||||
private void setup(Anchor anchor, float y = 0)
|
||||
{
|
||||
AddStep($"setup {anchor} {y}", () => SetContents(s =>
|
||||
{
|
||||
var container = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
|
||||
if (s is ArgonSkin)
|
||||
container.Add(new ArgonManiaComboCounter());
|
||||
else if (s is LegacySkin)
|
||||
container.Add(new LegacyManiaComboCounter());
|
||||
else
|
||||
container.Add(new LegacyManiaComboCounter());
|
||||
|
||||
container.Child.Anchor = anchor;
|
||||
container.Child.Origin = Anchor.Centre;
|
||||
container.Child.Y = y;
|
||||
|
||||
return container;
|
||||
}));
|
||||
}
|
||||
|
||||
private void check(Anchor anchor, float y)
|
||||
{
|
||||
AddAssert($"check {anchor} {y}", () =>
|
||||
{
|
||||
foreach (var combo in this.ChildrenOfType<ISerialisableDrawable>())
|
||||
{
|
||||
var drawableCombo = (Drawable)combo;
|
||||
if (drawableCombo.Anchor != anchor || drawableCombo.Y != y)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
@ -271,7 +270,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
Duration = endTimeData.Duration,
|
||||
Column = column,
|
||||
Samples = HitObject.Samples,
|
||||
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples ?? defaultNodeSamples
|
||||
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples ?? HoldNote.CreateDefaultNodeSamples(HitObject)
|
||||
});
|
||||
}
|
||||
else if (HitObject is IHasXPosition)
|
||||
@ -286,16 +285,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
||||
|
||||
return pattern;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// osu!mania-specific beatmaps in stable only play samples at the start of the hold note.
|
||||
/// </remarks>
|
||||
private List<IList<HitSampleInfo>> defaultNodeSamples
|
||||
=> new List<IList<HitSampleInfo>>
|
||||
{
|
||||
HitObject.Samples,
|
||||
new List<HitSampleInfo>()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
public class ManiaSkinComponentLookup : GameplaySkinComponentLookup<ManiaSkinComponents>
|
||||
public class ManiaSkinComponentLookup : SkinComponentLookup<ManiaSkinComponents>
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ManiaSkinComponentLookup"/>.
|
||||
|
@ -7,6 +7,7 @@ using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
@ -91,6 +92,10 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
{
|
||||
base.CreateNestedHitObjects(cancellationToken);
|
||||
|
||||
// Generally node samples will be populated by ManiaBeatmapConverter, but in a case like the editor they may not be.
|
||||
// Ensure they are set to a sane default here.
|
||||
NodeSamples ??= CreateDefaultNodeSamples(this);
|
||||
|
||||
AddNested(Head = new HeadNote
|
||||
{
|
||||
StartTime = StartTime,
|
||||
@ -102,7 +107,7 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
{
|
||||
StartTime = EndTime,
|
||||
Column = Column,
|
||||
Samples = GetNodeSamples((NodeSamples?.Count - 1) ?? 1),
|
||||
Samples = GetNodeSamples(NodeSamples.Count - 1),
|
||||
});
|
||||
|
||||
AddNested(Body = new HoldNoteBody
|
||||
@ -116,7 +121,20 @@ namespace osu.Game.Rulesets.Mania.Objects
|
||||
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
|
||||
public IList<HitSampleInfo> GetNodeSamples(int nodeIndex) =>
|
||||
nodeIndex < NodeSamples?.Count ? NodeSamples[nodeIndex] : Samples;
|
||||
public IList<HitSampleInfo> GetNodeSamples(int nodeIndex) => nodeIndex < NodeSamples?.Count ? NodeSamples[nodeIndex] : Samples;
|
||||
|
||||
/// <summary>
|
||||
/// Create the default note samples for a hold note, based off their main sample.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// By default, osu!mania beatmaps in only play samples at the start of the hold note.
|
||||
/// </remarks>
|
||||
/// <param name="obj">The object to use as a basis for the head sample.</param>
|
||||
/// <returns>Defaults for assigning to <see cref="HoldNote.NodeSamples"/>.</returns>
|
||||
public static List<IList<HitSampleInfo>> CreateDefaultNodeSamples(HitObject obj) => new List<IList<HitSampleInfo>>
|
||||
{
|
||||
obj.Samples,
|
||||
new List<HitSampleInfo>(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -38,11 +38,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
private void updateAnchor()
|
||||
{
|
||||
// if the anchor isn't a vertical center, set top or bottom anchor based on scroll direction
|
||||
if (!Anchor.HasFlag(Anchor.y1))
|
||||
{
|
||||
Anchor &= ~(Anchor.y0 | Anchor.y2);
|
||||
Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0;
|
||||
}
|
||||
if (Anchor.HasFlag(Anchor.y1))
|
||||
return;
|
||||
|
||||
Anchor &= ~(Anchor.y0 | Anchor.y2);
|
||||
Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0;
|
||||
|
||||
// change the sign of the Y coordinate in line with the scrolling direction.
|
||||
// i.e. if the user changes direction from down to up, the anchor is changed from top to bottom, and the Y is flipped from positive to negative here.
|
||||
|
@ -28,18 +28,14 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
{
|
||||
switch (lookup)
|
||||
{
|
||||
case SkinComponentsContainerLookup containerLookup:
|
||||
case GlobalSkinnableContainerLookup containerLookup:
|
||||
// Only handle per ruleset defaults here.
|
||||
if (containerLookup.Ruleset == null)
|
||||
return base.GetDrawableComponent(lookup);
|
||||
|
||||
// Skin has configuration.
|
||||
if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d)
|
||||
return d;
|
||||
|
||||
switch (containerLookup.Target)
|
||||
switch (containerLookup.Lookup)
|
||||
{
|
||||
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
|
||||
case GlobalSkinnableContainers.MainHUDComponents:
|
||||
return new DefaultSkinComponentsContainer(container =>
|
||||
{
|
||||
var combo = container.ChildrenOfType<ArgonManiaComboCounter>().FirstOrDefault();
|
||||
@ -59,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
|
||||
return null;
|
||||
|
||||
case GameplaySkinComponentLookup<HitResult> resultComponent:
|
||||
case SkinComponentLookup<HitResult> resultComponent:
|
||||
// This should eventually be moved to a skin setting, when supported.
|
||||
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
|
||||
return Drawable.Empty();
|
||||
|
@ -1,6 +1,7 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -44,16 +45,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
private void updateAnchor()
|
||||
{
|
||||
// if the anchor isn't a vertical center, set top or bottom anchor based on scroll direction
|
||||
if (!Anchor.HasFlag(Anchor.y1))
|
||||
{
|
||||
Anchor &= ~(Anchor.y0 | Anchor.y2);
|
||||
Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0;
|
||||
}
|
||||
if (Anchor.HasFlag(Anchor.y1))
|
||||
return;
|
||||
|
||||
// since we flip the vertical anchor when changing scroll direction,
|
||||
// we can use the sign of the Y value as an indicator to make the combo counter displayed correctly.
|
||||
if ((Y < 0 && direction.Value == ScrollingDirection.Down) || (Y > 0 && direction.Value == ScrollingDirection.Up))
|
||||
Y = -Y;
|
||||
Anchor &= ~(Anchor.y0 | Anchor.y2);
|
||||
Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0;
|
||||
|
||||
// change the sign of the Y coordinate in line with the scrolling direction.
|
||||
// i.e. if the user changes direction from down to up, the anchor is changed from top to bottom, and the Y is flipped from positive to negative here.
|
||||
Y = Math.Abs(Y) * (direction.Value == ScrollingDirection.Up ? -1 : 1);
|
||||
}
|
||||
|
||||
protected override void OnCountIncrement()
|
||||
|
@ -80,22 +80,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
{
|
||||
switch (lookup)
|
||||
{
|
||||
case SkinComponentsContainerLookup containerLookup:
|
||||
case GlobalSkinnableContainerLookup containerLookup:
|
||||
// Modifications for global components.
|
||||
if (containerLookup.Ruleset == null)
|
||||
return base.GetDrawableComponent(lookup);
|
||||
|
||||
// Skin has configuration.
|
||||
if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d)
|
||||
return d;
|
||||
|
||||
// we don't have enough assets to display these components (this is especially the case on a "beatmap" skin).
|
||||
if (!IsProvidingLegacyResources)
|
||||
return null;
|
||||
|
||||
switch (containerLookup.Target)
|
||||
switch (containerLookup.Lookup)
|
||||
{
|
||||
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
|
||||
case GlobalSkinnableContainers.MainHUDComponents:
|
||||
return new DefaultSkinComponentsContainer(container =>
|
||||
{
|
||||
var combo = container.ChildrenOfType<LegacyManiaComboCounter>().FirstOrDefault();
|
||||
@ -114,7 +110,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
|
||||
return null;
|
||||
|
||||
case GameplaySkinComponentLookup<HitResult> resultComponent:
|
||||
case SkinComponentLookup<HitResult> resultComponent:
|
||||
return getResult(resultComponent.Component);
|
||||
|
||||
case ManiaSkinComponentLookup maniaComponent:
|
||||
|
@ -99,12 +99,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
Show();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool OnTouchDown(TouchDownEvent e)
|
||||
{
|
||||
Show();
|
||||
@ -172,17 +166,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
updateButton(false);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
updateButton(true);
|
||||
return false; // handled by parent container to show overlay.
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
updateButton(false);
|
||||
}
|
||||
|
||||
private void updateButton(bool press)
|
||||
{
|
||||
if (press == isPressed)
|
||||
|
@ -24,24 +24,24 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
[Test]
|
||||
public void TestGridToggles()
|
||||
{
|
||||
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
|
||||
AddStep("enable distance snap grid", () => InputManager.Key(Key.Y));
|
||||
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||
|
||||
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||
gridActive<RectangularPositionSnapGrid>(false);
|
||||
|
||||
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
|
||||
AddStep("enable rectangular grid", () => InputManager.Key(Key.T));
|
||||
|
||||
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||
AddUntilStep("distance snap grid still visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||
gridActive<RectangularPositionSnapGrid>(true);
|
||||
|
||||
AddStep("disable distance snap grid", () => InputManager.Key(Key.T));
|
||||
AddStep("disable distance snap 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)));
|
||||
gridActive<RectangularPositionSnapGrid>(true);
|
||||
|
||||
AddStep("disable rectangular grid", () => InputManager.Key(Key.Y));
|
||||
AddStep("disable rectangular grid", () => InputManager.Key(Key.T));
|
||||
AddUntilStep("distance snap grid still hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||
gridActive<RectangularPositionSnapGrid>(false);
|
||||
}
|
||||
@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
|
||||
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||
|
||||
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
|
||||
AddStep("enable distance snap grid", () => InputManager.Key(Key.Y));
|
||||
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||
AddStep("hold alt", () => InputManager.PressKey(Key.AltLeft));
|
||||
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||
@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
double distanceSnap = double.PositiveInfinity;
|
||||
|
||||
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
|
||||
AddStep("enable distance snap grid", () => InputManager.Key(Key.Y));
|
||||
|
||||
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||
@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
[Test]
|
||||
public void TestGridSizeToggling()
|
||||
{
|
||||
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
|
||||
AddStep("enable rectangular grid", () => InputManager.Key(Key.T));
|
||||
AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Any());
|
||||
gridSizeIs(4);
|
||||
|
||||
|
@ -163,6 +163,44 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
checkControlPointSelected(1, false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAdjustLength()
|
||||
{
|
||||
AddStep("move mouse to drag marker", () =>
|
||||
{
|
||||
Vector2 position = slider.Position + slider.Path.PositionAt(1) + new Vector2(60, 0);
|
||||
InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position));
|
||||
});
|
||||
AddStep("start drag", () => InputManager.PressButton(MouseButton.Left));
|
||||
AddStep("move mouse to control point 1", () =>
|
||||
{
|
||||
Vector2 position = slider.Position + slider.Path.ControlPoints[1].Position + new Vector2(60, 0);
|
||||
InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position));
|
||||
});
|
||||
AddStep("end adjust length", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
AddAssert("expected distance halved",
|
||||
() => Precision.AlmostEquals(slider.Path.Distance, 172.2, 0.1));
|
||||
|
||||
AddStep("move mouse to drag marker", () =>
|
||||
{
|
||||
Vector2 position = slider.Position + slider.Path.PositionAt(1) + new Vector2(60, 0);
|
||||
InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position));
|
||||
});
|
||||
AddStep("start drag", () => InputManager.PressButton(MouseButton.Left));
|
||||
AddStep("move mouse beyond last control point", () =>
|
||||
{
|
||||
Vector2 position = slider.Position + slider.Path.ControlPoints[2].Position + new Vector2(100, 0);
|
||||
InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position));
|
||||
});
|
||||
AddStep("end adjust length", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
AddAssert("expected distance is calculated distance",
|
||||
() => Precision.AlmostEquals(slider.Path.Distance, slider.Path.CalculatedDistance, 0.1));
|
||||
|
||||
moveMouseToControlPoint(1);
|
||||
AddAssert("expected distance is unchanged",
|
||||
() => Precision.AlmostEquals(slider.Path.Distance, slider.Path.CalculatedDistance, 0.1));
|
||||
}
|
||||
|
||||
private void moveHitObject()
|
||||
{
|
||||
AddStep("move hitobject", () =>
|
||||
|
@ -88,6 +88,21 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddAssert("trail is disjoint", () => this.ChildrenOfType<LegacyCursorTrail>().Single().DisjointTrail, () => Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClickExpand()
|
||||
{
|
||||
createTest(() => new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Scale = new Vector2(10),
|
||||
Child = new CursorTrail(),
|
||||
});
|
||||
|
||||
AddStep("expand", () => this.ChildrenOfType<CursorTrail>().Single().NewPartScale = new Vector2(3));
|
||||
AddWaitStep("let the cursor trail draw a bit", 5);
|
||||
AddStep("contract", () => this.ChildrenOfType<CursorTrail>().Single().NewPartScale = Vector2.One);
|
||||
}
|
||||
|
||||
private void createTest(Func<Drawable> createContent) => AddStep("create trail", () =>
|
||||
{
|
||||
Clear();
|
||||
|
@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
public readonly PathControlPoint ControlPoint;
|
||||
|
||||
private readonly T hitObject;
|
||||
private readonly Circle circle;
|
||||
private readonly FastCircle circle;
|
||||
private readonly Drawable markerRing;
|
||||
|
||||
[Resolved]
|
||||
@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
circle = new Circle
|
||||
circle = new FastCircle
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
|
@ -1,7 +1,10 @@
|
||||
// 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.Framework.Graphics.Primitives;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
@ -9,11 +12,28 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
public partial class SliderCircleOverlay : CompositeDrawable
|
||||
{
|
||||
public SliderEndDragMarker? EndDragMarker { get; }
|
||||
|
||||
public RectangleF VisibleQuad
|
||||
{
|
||||
get
|
||||
{
|
||||
var result = CirclePiece.ScreenSpaceDrawQuad.AABBFloat;
|
||||
|
||||
if (endDragMarkerContainer == null) return result;
|
||||
|
||||
var size = result.Size * 1.4f;
|
||||
var location = result.TopLeft - result.Size * 0.2f;
|
||||
return new RectangleF(location, size);
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly HitCirclePiece CirclePiece;
|
||||
|
||||
private readonly Slider slider;
|
||||
private readonly SliderPosition position;
|
||||
private readonly HitCircleOverlapMarker? marker;
|
||||
private readonly Container? endDragMarkerContainer;
|
||||
|
||||
public SliderCircleOverlay(Slider slider, SliderPosition position)
|
||||
{
|
||||
@ -24,26 +44,49 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
AddInternal(marker = new HitCircleOverlapMarker());
|
||||
|
||||
AddInternal(CirclePiece = new HitCirclePiece());
|
||||
|
||||
if (position == SliderPosition.End)
|
||||
{
|
||||
AddInternal(endDragMarkerContainer = new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Padding = new MarginPadding(-2.5f),
|
||||
Child = EndDragMarker = new SliderEndDragMarker()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
var circle = position == SliderPosition.Start ? (HitCircle)slider.HeadCircle : slider.TailCircle;
|
||||
var circle = position == SliderPosition.Start ? (HitCircle)slider.HeadCircle :
|
||||
slider.RepeatCount % 2 == 0 ? slider.TailCircle : slider.LastRepeat!;
|
||||
|
||||
CirclePiece.UpdateFrom(circle);
|
||||
marker?.UpdateFrom(circle);
|
||||
|
||||
if (endDragMarkerContainer != null)
|
||||
{
|
||||
endDragMarkerContainer.Position = circle.Position;
|
||||
endDragMarkerContainer.Scale = CirclePiece.Scale * 1.2f;
|
||||
var diff = slider.Path.PositionAt(1) - slider.Path.PositionAt(0.99f);
|
||||
endDragMarkerContainer.Rotation = float.RadiansToDegrees(MathF.Atan2(diff.Y, diff.X));
|
||||
}
|
||||
}
|
||||
|
||||
public override void Hide()
|
||||
{
|
||||
CirclePiece.Hide();
|
||||
endDragMarkerContainer?.Hide();
|
||||
}
|
||||
|
||||
public override void Show()
|
||||
{
|
||||
CirclePiece.Show();
|
||||
endDragMarkerContainer?.Show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,84 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Lines;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
public partial class SliderEndDragMarker : SmoothPath
|
||||
{
|
||||
public Action<DragStartEvent>? StartDrag { get; set; }
|
||||
public Action<DragEvent>? Drag { get; set; }
|
||||
public Action? EndDrag { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
var path = PathApproximator.CircularArcToPiecewiseLinear([
|
||||
new Vector2(0, OsuHitObject.OBJECT_RADIUS),
|
||||
new Vector2(OsuHitObject.OBJECT_RADIUS, 0),
|
||||
new Vector2(0, -OsuHitObject.OBJECT_RADIUS)
|
||||
]);
|
||||
|
||||
Anchor = Anchor.CentreLeft;
|
||||
Origin = Anchor.CentreLeft;
|
||||
PathRadius = 5;
|
||||
Vertices = path;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
updateState();
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateState();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
updateState();
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
updateState();
|
||||
StartDrag?.Invoke(e);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnDrag(DragEvent e)
|
||||
{
|
||||
updateState();
|
||||
base.OnDrag(e);
|
||||
Drag?.Invoke(e);
|
||||
}
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e)
|
||||
{
|
||||
updateState();
|
||||
EndDrag?.Invoke();
|
||||
base.OnDragEnd(e);
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
Colour = IsHovered || IsDragged ? colours.Red : colours.Yellow;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,9 @@
|
||||
// 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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
@ -29,30 +25,29 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
public new Slider HitObject => (Slider)base.HitObject;
|
||||
|
||||
private SliderBodyPiece bodyPiece;
|
||||
private HitCirclePiece headCirclePiece;
|
||||
private HitCirclePiece tailCirclePiece;
|
||||
private PathControlPointVisualiser<Slider> controlPointVisualiser;
|
||||
private SliderBodyPiece bodyPiece = null!;
|
||||
private HitCirclePiece headCirclePiece = null!;
|
||||
private HitCirclePiece tailCirclePiece = null!;
|
||||
private PathControlPointVisualiser<Slider> controlPointVisualiser = null!;
|
||||
|
||||
private InputManager inputManager;
|
||||
private InputManager inputManager = null!;
|
||||
|
||||
private PathControlPoint? cursor;
|
||||
|
||||
private SliderPlacementState state;
|
||||
private PathControlPoint segmentStart;
|
||||
private PathControlPoint cursor;
|
||||
|
||||
private int currentSegmentLength;
|
||||
private bool usingCustomSegmentType;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[CanBeNull]
|
||||
private IPositionSnapProvider positionSnapProvider { get; set; }
|
||||
[Resolved]
|
||||
private IPositionSnapProvider? positionSnapProvider { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[CanBeNull]
|
||||
private IDistanceSnapProvider distanceSnapProvider { get; set; }
|
||||
[Resolved]
|
||||
private IDistanceSnapProvider? distanceSnapProvider { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[CanBeNull]
|
||||
private FreehandSliderToolboxGroup freehandToolboxGroup { get; set; }
|
||||
[Resolved]
|
||||
private FreehandSliderToolboxGroup? freehandToolboxGroup { get; set; }
|
||||
|
||||
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
|
||||
|
||||
@ -84,7 +79,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
inputManager = GetContainingInputManager();
|
||||
|
||||
inputManager = GetContainingInputManager()!;
|
||||
|
||||
if (freehandToolboxGroup != null)
|
||||
{
|
||||
@ -108,7 +104,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private EditorBeatmap editorBeatmap { get; set; }
|
||||
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
{
|
||||
@ -151,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
case SliderPlacementState.ControlPoints:
|
||||
if (canPlaceNewControlPoint(out var lastPoint))
|
||||
placeNewControlPoint();
|
||||
else
|
||||
else if (lastPoint != null)
|
||||
beginNewSegment(lastPoint);
|
||||
|
||||
break;
|
||||
@ -162,9 +158,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
private void beginNewSegment(PathControlPoint lastPoint)
|
||||
{
|
||||
// Transform the last point into a new segment.
|
||||
Debug.Assert(lastPoint != null);
|
||||
|
||||
segmentStart = lastPoint;
|
||||
segmentStart.Type = PathType.LINEAR;
|
||||
|
||||
@ -384,7 +377,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
/// </summary>
|
||||
/// <param name="lastPoint">The last-placed control point. May be null, but is not null if <c>false</c> is returned.</param>
|
||||
/// <returns>Whether a new control point can be placed at the current position.</returns>
|
||||
private bool canPlaceNewControlPoint([CanBeNull] out PathControlPoint lastPoint)
|
||||
private bool canPlaceNewControlPoint(out PathControlPoint? lastPoint)
|
||||
{
|
||||
// We cannot rely on the ordering of drawable pieces, so find the respective drawable piece by searching for the last non-cursor control point.
|
||||
var last = HitObject.Path.ControlPoints.LastOrDefault(p => p != cursor);
|
||||
@ -436,7 +429,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
// Replace this segment with a circular arc if it is a reasonable substitute.
|
||||
var circleArcSegment = tryCircleArc(segment);
|
||||
|
||||
if (circleArcSegment is not null)
|
||||
if (circleArcSegment != null)
|
||||
{
|
||||
HitObject.Path.ControlPoints.Add(new PathControlPoint(circleArcSegment[0], PathType.PERFECT_CURVE));
|
||||
HitObject.Path.ControlPoints.Add(new PathControlPoint(circleArcSegment[1]));
|
||||
@ -453,7 +446,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2[] tryCircleArc(List<Vector2> segment)
|
||||
private Vector2[]? tryCircleArc(List<Vector2> segment)
|
||||
{
|
||||
if (segment.Count < 3 || freehandToolboxGroup?.CircleThreshold.Value == 0) return null;
|
||||
|
||||
|
@ -1,13 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
@ -33,27 +33,28 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
protected new DrawableSlider DrawableObject => (DrawableSlider)base.DrawableObject;
|
||||
|
||||
protected SliderBodyPiece BodyPiece { get; private set; }
|
||||
protected SliderCircleOverlay HeadOverlay { get; private set; }
|
||||
protected SliderCircleOverlay TailOverlay { get; private set; }
|
||||
protected SliderBodyPiece BodyPiece { get; private set; } = null!;
|
||||
protected SliderCircleOverlay HeadOverlay { get; private set; } = null!;
|
||||
protected SliderCircleOverlay TailOverlay { get; private set; } = null!;
|
||||
|
||||
[CanBeNull]
|
||||
protected PathControlPointVisualiser<Slider> ControlPointVisualiser { get; private set; }
|
||||
protected PathControlPointVisualiser<Slider>? ControlPointVisualiser { get; private set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IDistanceSnapProvider distanceSnapProvider { get; set; }
|
||||
[Resolved]
|
||||
private IDistanceSnapProvider? distanceSnapProvider { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IPlacementHandler placementHandler { get; set; }
|
||||
[Resolved]
|
||||
private IPlacementHandler? placementHandler { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private EditorBeatmap editorBeatmap { get; set; }
|
||||
[Resolved]
|
||||
private EditorBeatmap? editorBeatmap { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IEditorChangeHandler changeHandler { get; set; }
|
||||
[Resolved]
|
||||
private IEditorChangeHandler? changeHandler { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private BindableBeatDivisor beatDivisor { get; set; }
|
||||
[Resolved]
|
||||
private BindableBeatDivisor? beatDivisor { get; set; }
|
||||
|
||||
private PathControlPoint? placementControlPoint;
|
||||
|
||||
public override Quad SelectionQuad
|
||||
{
|
||||
@ -61,6 +62,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
{
|
||||
var result = BodyPiece.ScreenSpaceDrawQuad.AABBFloat;
|
||||
|
||||
result = RectangleF.Union(result, HeadOverlay.VisibleQuad);
|
||||
result = RectangleF.Union(result, TailOverlay.VisibleQuad);
|
||||
|
||||
if (ControlPointVisualiser != null)
|
||||
{
|
||||
foreach (var piece in ControlPointVisualiser.Pieces)
|
||||
@ -76,6 +80,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
private readonly BindableList<HitObject> selectedObjects = new BindableList<HitObject>();
|
||||
private readonly Bindable<bool> showHitMarkers = new Bindable<bool>();
|
||||
|
||||
// Cached slider path which ignored the expected distance value.
|
||||
private readonly Cached<SliderPath> fullPathCache = new Cached<SliderPath>();
|
||||
|
||||
private Vector2 lastRightClickPosition;
|
||||
|
||||
public SliderSelectionBlueprint(Slider slider)
|
||||
: base(slider)
|
||||
{
|
||||
@ -91,6 +100,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
TailOverlay = CreateCircleOverlay(HitObject, SliderPosition.End),
|
||||
};
|
||||
|
||||
// tail will always have a non-null end drag marker.
|
||||
Debug.Assert(TailOverlay.EndDragMarker != null);
|
||||
|
||||
TailOverlay.EndDragMarker.StartDrag += startAdjustingLength;
|
||||
TailOverlay.EndDragMarker.Drag += adjustLength;
|
||||
TailOverlay.EndDragMarker.EndDrag += endAdjustLength;
|
||||
|
||||
config.BindWith(OsuSetting.EditorShowHitMarkers, showHitMarkers);
|
||||
}
|
||||
|
||||
@ -99,6 +115,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
base.LoadComplete();
|
||||
|
||||
controlPoints.BindTo(HitObject.Path.ControlPoints);
|
||||
controlPoints.CollectionChanged += (_, _) => fullPathCache.Invalidate();
|
||||
|
||||
pathVersion.BindTo(HitObject.Path.Version);
|
||||
pathVersion.BindValueChanged(_ => editorBeatmap?.Update(HitObject));
|
||||
@ -123,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
return false;
|
||||
|
||||
hoveredControlPoint.IsSelected.Value = true;
|
||||
ControlPointVisualiser.DeleteSelected();
|
||||
ControlPointVisualiser?.DeleteSelected();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -141,7 +158,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateVisualDefinition();
|
||||
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
@ -186,17 +202,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 rightClickPosition;
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
switch (e.Button)
|
||||
{
|
||||
case MouseButton.Right:
|
||||
rightClickPosition = e.MouseDownPosition;
|
||||
lastRightClickPosition = e.MouseDownPosition;
|
||||
return false; // Allow right click to be handled by context menu
|
||||
|
||||
case MouseButton.Left:
|
||||
|
||||
// If there's more than two objects selected, ctrl+click should deselect
|
||||
if (e.ControlPressed && IsSelected && selectedObjects.Count < 2)
|
||||
{
|
||||
@ -212,8 +227,134 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
return false;
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
private PathControlPoint placementControlPoint;
|
||||
#region Length Adjustment (independent of path nodes)
|
||||
|
||||
private Vector2 lengthAdjustMouseOffset;
|
||||
private double oldDuration;
|
||||
private double oldVelocityMultiplier;
|
||||
private double desiredDistance;
|
||||
private bool isAdjustingLength;
|
||||
private bool adjustVelocityMomentary;
|
||||
|
||||
private void startAdjustingLength(DragStartEvent e)
|
||||
{
|
||||
isAdjustingLength = true;
|
||||
adjustVelocityMomentary = e.ShiftPressed;
|
||||
lengthAdjustMouseOffset = ToLocalSpace(e.ScreenSpaceMouseDownPosition) - HitObject.Position - HitObject.Path.PositionAt(1);
|
||||
oldDuration = HitObject.Path.Distance / HitObject.SliderVelocityMultiplier;
|
||||
oldVelocityMultiplier = HitObject.SliderVelocityMultiplier;
|
||||
changeHandler?.BeginChange();
|
||||
}
|
||||
|
||||
private void endAdjustLength()
|
||||
{
|
||||
trimExcessControlPoints(HitObject.Path);
|
||||
changeHandler?.EndChange();
|
||||
isAdjustingLength = false;
|
||||
}
|
||||
|
||||
private void adjustLength(MouseEvent e) => adjustLength(findClosestPathDistance(e), e.ShiftPressed);
|
||||
|
||||
private void adjustLength(double proposedDistance, bool adjustVelocity)
|
||||
{
|
||||
desiredDistance = proposedDistance;
|
||||
double proposedVelocity = oldVelocityMultiplier;
|
||||
|
||||
if (adjustVelocity)
|
||||
{
|
||||
proposedVelocity = proposedDistance / oldDuration;
|
||||
proposedDistance = MathHelper.Clamp(proposedDistance, 0.1 * oldDuration, 10 * oldDuration);
|
||||
}
|
||||
else
|
||||
{
|
||||
double minDistance = distanceSnapProvider?.GetBeatSnapDistanceAt(HitObject, false) * oldVelocityMultiplier ?? 1;
|
||||
// Add a small amount to the proposed distance to make it easier to snap to the full length of the slider.
|
||||
proposedDistance = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)proposedDistance + 1) ?? proposedDistance;
|
||||
proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
|
||||
}
|
||||
|
||||
if (Precision.AlmostEquals(proposedDistance, HitObject.Path.Distance) && Precision.AlmostEquals(proposedVelocity, HitObject.SliderVelocityMultiplier))
|
||||
return;
|
||||
|
||||
HitObject.SliderVelocityMultiplier = proposedVelocity;
|
||||
HitObject.Path.ExpectedDistance.Value = proposedDistance;
|
||||
editorBeatmap?.Update(HitObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trims control points from the end of the slider path which are not required to reach the expected end of the slider.
|
||||
/// </summary>
|
||||
/// <param name="sliderPath">The slider path to trim control points of.</param>
|
||||
private void trimExcessControlPoints(SliderPath sliderPath)
|
||||
{
|
||||
if (!sliderPath.ExpectedDistance.Value.HasValue)
|
||||
return;
|
||||
|
||||
double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray();
|
||||
int segmentIndex = 0;
|
||||
|
||||
for (int i = 1; i < sliderPath.ControlPoints.Count - 1; i++)
|
||||
{
|
||||
if (!sliderPath.ControlPoints[i].Type.HasValue) continue;
|
||||
|
||||
if (Precision.AlmostBigger(segmentEnds[segmentIndex], 1, 1E-3))
|
||||
{
|
||||
sliderPath.ControlPoints.RemoveRange(i + 1, sliderPath.ControlPoints.Count - i - 1);
|
||||
sliderPath.ControlPoints[^1].Type = null;
|
||||
break;
|
||||
}
|
||||
|
||||
segmentIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the expected distance value for which the slider end is closest to the mouse position.
|
||||
/// </summary>
|
||||
private double findClosestPathDistance(MouseEvent e)
|
||||
{
|
||||
const double step1 = 10;
|
||||
const double step2 = 0.1;
|
||||
const double longer_distance_bias = 0.01;
|
||||
|
||||
var desiredPosition = ToLocalSpace(e.ScreenSpaceMousePosition) - HitObject.Position - lengthAdjustMouseOffset;
|
||||
|
||||
if (!fullPathCache.IsValid)
|
||||
fullPathCache.Value = new SliderPath(HitObject.Path.ControlPoints.ToArray());
|
||||
|
||||
// Do a linear search to find the closest point on the path to the mouse position.
|
||||
double bestValue = 0;
|
||||
double minDistance = double.MaxValue;
|
||||
|
||||
for (double d = 0; d <= fullPathCache.Value.CalculatedDistance; d += step1)
|
||||
{
|
||||
double t = d / fullPathCache.Value.CalculatedDistance;
|
||||
double dist = Vector2.Distance(fullPathCache.Value.PositionAt(t), desiredPosition) - d * longer_distance_bias;
|
||||
|
||||
if (dist >= minDistance) continue;
|
||||
|
||||
minDistance = dist;
|
||||
bestValue = d;
|
||||
}
|
||||
|
||||
// Do another linear search to fine-tune the result.
|
||||
double maxValue = Math.Min(bestValue + step1, fullPathCache.Value.CalculatedDistance);
|
||||
|
||||
for (double d = bestValue - step1; d <= maxValue; d += step2)
|
||||
{
|
||||
double t = d / fullPathCache.Value.CalculatedDistance;
|
||||
double dist = Vector2.Distance(fullPathCache.Value.PositionAt(t), desiredPosition) - d * longer_distance_bias;
|
||||
|
||||
if (dist >= minDistance) continue;
|
||||
|
||||
minDistance = dist;
|
||||
bestValue = d;
|
||||
}
|
||||
|
||||
return bestValue;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
@ -255,9 +396,24 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isAdjustingLength && e.ShiftPressed != adjustVelocityMomentary)
|
||||
{
|
||||
adjustVelocityMomentary = e.ShiftPressed;
|
||||
adjustLength(desiredDistance, adjustVelocityMomentary);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void OnKeyUp(KeyUpEvent e)
|
||||
{
|
||||
if (!IsSelected || !isAdjustingLength || e.ShiftPressed == adjustVelocityMomentary) return;
|
||||
|
||||
adjustVelocityMomentary = e.ShiftPressed;
|
||||
adjustLength(desiredDistance, adjustVelocityMomentary);
|
||||
}
|
||||
|
||||
private PathControlPoint addControlPoint(Vector2 position)
|
||||
{
|
||||
position -= HitObject.Position;
|
||||
@ -326,6 +482,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
private void splitControlPoints(List<PathControlPoint> controlPointsToSplitAt)
|
||||
{
|
||||
if (editorBeatmap == null)
|
||||
return;
|
||||
|
||||
// Arbitrary gap in milliseconds to put between split slider pieces
|
||||
const double split_gap = 100;
|
||||
|
||||
@ -432,7 +591,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
new OsuMenuItem("Add control point", MenuItemType.Standard, () =>
|
||||
{
|
||||
changeHandler?.BeginChange();
|
||||
addControlPoint(rightClickPosition);
|
||||
addControlPoint(lastRightClickPosition);
|
||||
changeHandler?.EndChange();
|
||||
}),
|
||||
new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream),
|
||||
|
54
osu.Game.Rulesets.Osu/Edit/GenerateToolboxGroup.cs
Normal file
54
osu.Game.Rulesets.Osu/Edit/GenerateToolboxGroup.cs
Normal file
@ -0,0 +1,54 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Screens.Edit.Components;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public partial class GenerateToolboxGroup : EditorToolboxGroup
|
||||
{
|
||||
private readonly EditorToolButton polygonButton;
|
||||
|
||||
public GenerateToolboxGroup()
|
||||
: base("Generate")
|
||||
{
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(5),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
polygonButton = new EditorToolButton("Polygon",
|
||||
() => new SpriteIcon { Icon = FontAwesome.Solid.Spinner },
|
||||
() => new PolygonGenerationPopover()),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (e.Repeat) return false;
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.D:
|
||||
if (!e.ControlPressed || !e.ShiftPressed)
|
||||
return false;
|
||||
|
||||
polygonButton.TriggerClick();
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -54,18 +54,15 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
|
||||
=> base.CreateTernaryButtons()
|
||||
.Concat(DistanceSnapProvider.CreateTernaryButtons())
|
||||
.Concat(new[]
|
||||
{
|
||||
new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap })
|
||||
});
|
||||
.Append(new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap }))
|
||||
.Concat(DistanceSnapProvider.CreateTernaryButtons());
|
||||
|
||||
private BindableList<HitObject> selectedHitObjects;
|
||||
|
||||
private Bindable<HitObject> placementObject;
|
||||
|
||||
[Cached(typeof(IDistanceSnapProvider))]
|
||||
protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider();
|
||||
public readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider();
|
||||
|
||||
[Cached]
|
||||
protected readonly OsuGridToolboxGroup OsuGridToolboxGroup = new OsuGridToolboxGroup();
|
||||
@ -110,6 +107,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler,
|
||||
ScaleHandler = (OsuSelectionScaleHandler)BlueprintContainer.SelectionHandler.ScaleHandler,
|
||||
},
|
||||
new GenerateToolboxGroup(),
|
||||
FreehandSliderToolboxGroup
|
||||
}
|
||||
);
|
||||
|
193
osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs
Normal file
193
osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs
Normal file
@ -0,0 +1,193 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public partial class PolygonGenerationPopover : OsuPopover
|
||||
{
|
||||
private SliderWithTextBoxInput<double> distanceSnapInput = null!;
|
||||
private SliderWithTextBoxInput<int> offsetAngleInput = null!;
|
||||
private SliderWithTextBoxInput<int> repeatCountInput = null!;
|
||||
private SliderWithTextBoxInput<int> pointInput = null!;
|
||||
private RoundedButton commitButton = null!;
|
||||
|
||||
private readonly List<HitCircle> insertedCircles = new List<HitCircle>();
|
||||
private bool began;
|
||||
private bool committed;
|
||||
|
||||
[Resolved]
|
||||
private IBeatSnapProvider beatSnapProvider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private EditorClock editorClock { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IEditorChangeHandler? changeHandler { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private HitObjectComposer composer { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
Width = 220,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(20),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
distanceSnapInput = new SliderWithTextBoxInput<double>("Distance snap:")
|
||||
{
|
||||
Current = new BindableNumber<double>(1)
|
||||
{
|
||||
MinValue = 0.1,
|
||||
MaxValue = 6,
|
||||
Precision = 0.1,
|
||||
Value = ((OsuHitObjectComposer)composer).DistanceSnapProvider.DistanceSpacingMultiplier.Value,
|
||||
},
|
||||
Instantaneous = true
|
||||
},
|
||||
offsetAngleInput = new SliderWithTextBoxInput<int>("Offset angle:")
|
||||
{
|
||||
Current = new BindableNumber<int>
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 180,
|
||||
Precision = 1
|
||||
},
|
||||
Instantaneous = true
|
||||
},
|
||||
repeatCountInput = new SliderWithTextBoxInput<int>("Repeats:")
|
||||
{
|
||||
Current = new BindableNumber<int>(1)
|
||||
{
|
||||
MinValue = 1,
|
||||
MaxValue = 10,
|
||||
Precision = 1
|
||||
},
|
||||
Instantaneous = true
|
||||
},
|
||||
pointInput = new SliderWithTextBoxInput<int>("Vertices:")
|
||||
{
|
||||
Current = new BindableNumber<int>(3)
|
||||
{
|
||||
MinValue = 3,
|
||||
MaxValue = 10,
|
||||
Precision = 1,
|
||||
},
|
||||
Instantaneous = true
|
||||
},
|
||||
commitButton = new RoundedButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = "Create",
|
||||
Action = commit
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
began = true;
|
||||
|
||||
distanceSnapInput.Current.BindValueChanged(_ => tryCreatePolygon());
|
||||
offsetAngleInput.Current.BindValueChanged(_ => tryCreatePolygon());
|
||||
repeatCountInput.Current.BindValueChanged(_ => tryCreatePolygon());
|
||||
pointInput.Current.BindValueChanged(_ => tryCreatePolygon());
|
||||
tryCreatePolygon();
|
||||
}
|
||||
|
||||
private void tryCreatePolygon()
|
||||
{
|
||||
double startTime = beatSnapProvider.SnapTime(editorClock.CurrentTime);
|
||||
TimingControlPoint timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(startTime);
|
||||
double timeSpacing = timingPoint.BeatLength / editorBeatmap.BeatDivisor;
|
||||
IHasSliderVelocity lastWithSliderVelocity = editorBeatmap.HitObjects.Where(ho => ho.GetEndTime() <= startTime).OfType<IHasSliderVelocity>().LastOrDefault() ?? new Slider();
|
||||
double velocity = OsuHitObject.BASE_SCORING_DISTANCE * editorBeatmap.Difficulty.SliderMultiplier
|
||||
/ LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(lastWithSliderVelocity, timingPoint, OsuRuleset.SHORT_NAME);
|
||||
double length = distanceSnapInput.Current.Value * velocity * timeSpacing;
|
||||
float polygonRadius = (float)(length / (2 * Math.Sin(double.Pi / pointInput.Current.Value)));
|
||||
|
||||
editorBeatmap.RemoveRange(insertedCircles);
|
||||
insertedCircles.Clear();
|
||||
|
||||
var selectionHandler = (EditorSelectionHandler)composer.BlueprintContainer.SelectionHandler;
|
||||
bool first = true;
|
||||
|
||||
for (int i = 1; i <= pointInput.Current.Value * repeatCountInput.Current.Value; ++i)
|
||||
{
|
||||
float angle = float.DegreesToRadians(offsetAngleInput.Current.Value) + i * (2 * float.Pi / pointInput.Current.Value);
|
||||
var position = OsuPlayfield.BASE_SIZE / 2 + new Vector2(polygonRadius * float.Cos(angle), polygonRadius * float.Sin(angle));
|
||||
|
||||
var circle = new HitCircle
|
||||
{
|
||||
Position = position,
|
||||
StartTime = startTime,
|
||||
NewCombo = first && selectionHandler.SelectionNewComboState.Value == TernaryState.True,
|
||||
};
|
||||
// TODO: probably ensure samples also follow current ternary status (not trivial)
|
||||
circle.Samples.Add(circle.CreateHitSampleInfo());
|
||||
|
||||
if (position.X < 0 || position.Y < 0 || position.X > OsuPlayfield.BASE_SIZE.X || position.Y > OsuPlayfield.BASE_SIZE.Y)
|
||||
{
|
||||
commitButton.Enabled.Value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
insertedCircles.Add(circle);
|
||||
startTime = beatSnapProvider.SnapTime(startTime + timeSpacing);
|
||||
|
||||
first = false;
|
||||
}
|
||||
|
||||
editorBeatmap.AddRange(insertedCircles);
|
||||
commitButton.Enabled.Value = true;
|
||||
}
|
||||
|
||||
private void commit()
|
||||
{
|
||||
changeHandler?.EndChange();
|
||||
committed = true;
|
||||
Hide();
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
base.PopOut();
|
||||
|
||||
if (began && !committed)
|
||||
{
|
||||
editorBeatmap.RemoveRange(insertedCircles);
|
||||
changeHandler?.EndChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using JetBrains.Annotations;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
@ -162,6 +163,10 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
[JsonIgnore]
|
||||
public SliderTailCircle TailCircle { get; protected set; }
|
||||
|
||||
[JsonIgnore]
|
||||
[CanBeNull]
|
||||
public SliderRepeat LastRepeat { get; protected set; }
|
||||
|
||||
public Slider()
|
||||
{
|
||||
SamplesBindable.CollectionChanged += (_, _) => UpdateNestedSamples();
|
||||
@ -225,7 +230,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
break;
|
||||
|
||||
case SliderEventType.Repeat:
|
||||
AddNested(new SliderRepeat(this)
|
||||
AddNested(LastRepeat = new SliderRepeat(this)
|
||||
{
|
||||
RepeatIndex = e.SpanIndex,
|
||||
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
|
||||
@ -248,6 +253,9 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
|
||||
if (TailCircle != null)
|
||||
TailCircle.Position = EndPosition;
|
||||
|
||||
if (LastRepeat != null)
|
||||
LastRepeat.Position = RepeatCount % 2 == 0 ? Position : Position + Path.PositionAt(1);
|
||||
}
|
||||
|
||||
protected void UpdateNestedSamples()
|
||||
|
@ -359,5 +359,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
|
||||
return adjustedDifficulty;
|
||||
}
|
||||
|
||||
public override bool EditorShowScrollSpeed => false;
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu
|
||||
{
|
||||
public class OsuSkinComponentLookup : GameplaySkinComponentLookup<OsuSkinComponents>
|
||||
public class OsuSkinComponentLookup : SkinComponentLookup<OsuSkinComponents>
|
||||
{
|
||||
public OsuSkinComponentLookup(OsuSkinComponents component)
|
||||
: base(component)
|
||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
{
|
||||
switch (lookup)
|
||||
{
|
||||
case GameplaySkinComponentLookup<HitResult> resultComponent:
|
||||
case SkinComponentLookup<HitResult> resultComponent:
|
||||
HitResult result = resultComponent.Component;
|
||||
|
||||
// This should eventually be moved to a skin setting, when supported.
|
||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
{
|
||||
switch (lookup)
|
||||
{
|
||||
case GameplaySkinComponentLookup<HitResult> resultComponent:
|
||||
case SkinComponentLookup<HitResult> resultComponent:
|
||||
HitResult result = resultComponent.Component;
|
||||
|
||||
switch (result)
|
||||
|
@ -44,23 +44,19 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
switch (lookup)
|
||||
{
|
||||
case SkinComponentsContainerLookup containerLookup:
|
||||
case GlobalSkinnableContainerLookup containerLookup:
|
||||
// Only handle per ruleset defaults here.
|
||||
if (containerLookup.Ruleset == null)
|
||||
return base.GetDrawableComponent(lookup);
|
||||
|
||||
// Skin has configuration.
|
||||
if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d)
|
||||
return d;
|
||||
|
||||
// we don't have enough assets to display these components (this is especially the case on a "beatmap" skin).
|
||||
if (!IsProvidingLegacyResources)
|
||||
return null;
|
||||
|
||||
// Our own ruleset components default.
|
||||
switch (containerLookup.Target)
|
||||
switch (containerLookup.Lookup)
|
||||
{
|
||||
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
|
||||
case GlobalSkinnableContainers.MainHUDComponents:
|
||||
return new DefaultSkinComponentsContainer(container =>
|
||||
{
|
||||
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
|
||||
@ -69,10 +65,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
// set the anchor to top right so that it won't squash to the return button to the top
|
||||
keyCounter.Anchor = Anchor.CentreRight;
|
||||
keyCounter.Origin = Anchor.CentreRight;
|
||||
keyCounter.X = 0;
|
||||
// 340px is the default height inherit from stable
|
||||
keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y;
|
||||
keyCounter.Origin = Anchor.TopRight;
|
||||
keyCounter.Position = new Vector2(0, -40) * 1.6f;
|
||||
}
|
||||
|
||||
var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault();
|
||||
|
@ -38,6 +38,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
private double timeOffset;
|
||||
private float time;
|
||||
|
||||
/// <summary>
|
||||
/// The scale used on creation of a new trail part.
|
||||
/// </summary>
|
||||
public Vector2 NewPartScale = Vector2.One;
|
||||
|
||||
private Anchor trailOrigin = Anchor.Centre;
|
||||
|
||||
protected Anchor TrailOrigin
|
||||
@ -188,6 +193,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
{
|
||||
parts[currentIndex].Position = localSpacePosition;
|
||||
parts[currentIndex].Time = time + 1;
|
||||
parts[currentIndex].Scale = NewPartScale;
|
||||
++parts[currentIndex].InvalidationID;
|
||||
|
||||
currentIndex = (currentIndex + 1) % max_sprites;
|
||||
@ -199,6 +205,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
{
|
||||
public Vector2 Position;
|
||||
public float Time;
|
||||
public Vector2 Scale;
|
||||
public long InvalidationID;
|
||||
}
|
||||
|
||||
@ -280,7 +287,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y)),
|
||||
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y),
|
||||
TexturePosition = textureRect.BottomLeft,
|
||||
TextureRect = new Vector4(0, 0, 1, 1),
|
||||
Colour = DrawColourInfo.Colour.BottomLeft.Linear,
|
||||
@ -289,7 +296,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X), part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y)),
|
||||
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y),
|
||||
TexturePosition = textureRect.BottomRight,
|
||||
TextureRect = new Vector4(0, 0, 1, 1),
|
||||
Colour = DrawColourInfo.Colour.BottomRight.Linear,
|
||||
@ -298,7 +305,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X), part.Position.Y - texture.DisplayHeight * originPosition.Y),
|
||||
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y),
|
||||
TexturePosition = textureRect.TopRight,
|
||||
TextureRect = new Vector4(0, 0, 1, 1),
|
||||
Colour = DrawColourInfo.Colour.TopRight.Linear,
|
||||
@ -307,7 +314,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X, part.Position.Y - texture.DisplayHeight * originPosition.Y),
|
||||
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y),
|
||||
TexturePosition = textureRect.TopLeft,
|
||||
TextureRect = new Vector4(0, 0, 1, 1),
|
||||
Colour = DrawColourInfo.Colour.TopLeft.Linear,
|
||||
|
@ -31,6 +31,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
private SkinnableCursor skinnableCursor => (SkinnableCursor)cursorSprite.Drawable;
|
||||
|
||||
/// <summary>
|
||||
/// The current expanded scale of the cursor.
|
||||
/// </summary>
|
||||
public Vector2 CurrentExpandedScale => skinnableCursor.ExpandTarget?.Scale ?? Vector2.One;
|
||||
|
||||
public IBindable<float> CursorScale => cursorScale;
|
||||
|
||||
private readonly Bindable<float> cursorScale = new BindableFloat(1);
|
||||
|
@ -23,14 +23,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
public new OsuCursor ActiveCursor => (OsuCursor)base.ActiveCursor;
|
||||
|
||||
protected override Drawable CreateCursor() => new OsuCursor();
|
||||
|
||||
protected override Container<Drawable> Content => fadeContainer;
|
||||
|
||||
private readonly Container<Drawable> fadeContainer;
|
||||
|
||||
private readonly Bindable<bool> showTrail = new Bindable<bool>(true);
|
||||
|
||||
private readonly Drawable cursorTrail;
|
||||
private readonly SkinnableDrawable cursorTrail;
|
||||
|
||||
private readonly CursorRippleVisualiser rippleVisualiser;
|
||||
|
||||
@ -39,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
InternalChild = fadeContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new[]
|
||||
Children = new CompositeDrawable[]
|
||||
{
|
||||
cursorTrail = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling),
|
||||
rippleVisualiser = new CursorRippleVisualiser(),
|
||||
@ -79,6 +78,14 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
ActiveCursor.Contract();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (cursorTrail.Drawable is CursorTrail trail)
|
||||
trail.NewPartScale = ActiveCursor.CurrentExpandedScale;
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
|
||||
{
|
||||
switch (e.Action)
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
@ -35,9 +36,11 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
{
|
||||
OsuResumeOverlayInputBlocker? inputBlocker = null;
|
||||
|
||||
if (drawableRuleset != null)
|
||||
var drawableOsuRuleset = (DrawableOsuRuleset?)drawableRuleset;
|
||||
|
||||
if (drawableOsuRuleset != null)
|
||||
{
|
||||
var osuPlayfield = (OsuPlayfield)drawableRuleset.Playfield;
|
||||
var osuPlayfield = drawableOsuRuleset.Playfield;
|
||||
osuPlayfield.AttachResumeOverlayInputBlocker(inputBlocker = new OsuResumeOverlayInputBlocker());
|
||||
}
|
||||
|
||||
@ -45,13 +48,14 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
{
|
||||
Child = clickToResumeCursor = new OsuClickToResumeCursor
|
||||
{
|
||||
ResumeRequested = () =>
|
||||
ResumeRequested = action =>
|
||||
{
|
||||
// since the user had to press a button to tap the resume cursor,
|
||||
// block that press event from potentially reaching a hit circle that's behind the cursor.
|
||||
// we cannot do this from OsuClickToResumeCursor directly since we're in a different input manager tree than the gameplay one,
|
||||
// so we rely on a dedicated input blocking component that's implanted in there to do that for us.
|
||||
if (inputBlocker != null)
|
||||
// note this only matters when the user didn't pause while they were holding the same key that they are resuming with.
|
||||
if (inputBlocker != null && !drawableOsuRuleset.AsNonNull().KeyBindingInputManager.PressedActions.Contains(action))
|
||||
inputBlocker.BlockNextPress = true;
|
||||
|
||||
Resume();
|
||||
@ -94,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
{
|
||||
public override bool HandlePositionalInput => true;
|
||||
|
||||
public Action? ResumeRequested;
|
||||
public Action<OsuAction>? ResumeRequested;
|
||||
private Container scaleTransitionContainer = null!;
|
||||
|
||||
public OsuClickToResumeCursor()
|
||||
@ -136,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
return false;
|
||||
|
||||
scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint);
|
||||
ResumeRequested?.Invoke();
|
||||
ResumeRequested?.Invoke(e.Action);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
|
||||
{
|
||||
switch (lookup)
|
||||
{
|
||||
case GameplaySkinComponentLookup<HitResult> resultComponent:
|
||||
case SkinComponentLookup<HitResult> resultComponent:
|
||||
// This should eventually be moved to a skin setting, when supported.
|
||||
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
|
||||
return Drawable.Empty();
|
||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
|
||||
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
|
||||
{
|
||||
if (lookup is GameplaySkinComponentLookup<HitResult>)
|
||||
if (lookup is SkinComponentLookup<HitResult>)
|
||||
{
|
||||
// if a taiko skin is providing explosion sprites, hide the judgements completely
|
||||
if (hasExplosion.Value)
|
||||
|
@ -5,7 +5,7 @@ using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko
|
||||
{
|
||||
public class TaikoSkinComponentLookup : GameplaySkinComponentLookup<TaikoSkinComponents>
|
||||
public class TaikoSkinComponentLookup : SkinComponentLookup<TaikoSkinComponents>
|
||||
{
|
||||
public TaikoSkinComponentLookup(TaikoSkinComponents component)
|
||||
: base(component)
|
||||
|
@ -1,6 +1,7 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
@ -286,5 +287,62 @@ namespace osu.Game.Tests.NonVisual
|
||||
|
||||
Assert.That(cpi.TimingPoints[0].BeatLength, Is.Not.EqualTo(cpiCopy.TimingPoints[0].BeatLength));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBinarySearchEmptyList()
|
||||
{
|
||||
Assert.That(ControlPointInfo.BinarySearch(Array.Empty<TimingControlPoint>(), 0, EqualitySelection.FirstFound), Is.EqualTo(-1));
|
||||
Assert.That(ControlPointInfo.BinarySearch(Array.Empty<TimingControlPoint>(), 0, EqualitySelection.Leftmost), Is.EqualTo(-1));
|
||||
Assert.That(ControlPointInfo.BinarySearch(Array.Empty<TimingControlPoint>(), 0, EqualitySelection.Rightmost), Is.EqualTo(-1));
|
||||
}
|
||||
|
||||
[TestCase(new[] { 1 }, 0, -1)]
|
||||
[TestCase(new[] { 1 }, 1, 0)]
|
||||
[TestCase(new[] { 1 }, 2, -2)]
|
||||
[TestCase(new[] { 1, 3 }, 0, -1)]
|
||||
[TestCase(new[] { 1, 3 }, 1, 0)]
|
||||
[TestCase(new[] { 1, 3 }, 2, -2)]
|
||||
[TestCase(new[] { 1, 3 }, 3, 1)]
|
||||
[TestCase(new[] { 1, 3 }, 4, -3)]
|
||||
public void TestBinarySearchUniqueScenarios(int[] values, int search, int expectedIndex)
|
||||
{
|
||||
var items = values.Select(t => new TimingControlPoint { Time = t }).ToArray();
|
||||
Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.FirstFound), Is.EqualTo(expectedIndex));
|
||||
Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.Leftmost), Is.EqualTo(expectedIndex));
|
||||
Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.Rightmost), Is.EqualTo(expectedIndex));
|
||||
}
|
||||
|
||||
[TestCase(new[] { 1, 1 }, 1, 0)]
|
||||
[TestCase(new[] { 1, 2, 2 }, 2, 1)]
|
||||
[TestCase(new[] { 1, 2, 2, 2 }, 2, 1)]
|
||||
[TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 2)]
|
||||
[TestCase(new[] { 1, 2, 2, 3 }, 2, 1)]
|
||||
public void TestBinarySearchFirstFoundDuplicateScenarios(int[] values, int search, int expectedIndex)
|
||||
{
|
||||
var items = values.Select(t => new TimingControlPoint { Time = t }).ToArray();
|
||||
Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.FirstFound), Is.EqualTo(expectedIndex));
|
||||
}
|
||||
|
||||
[TestCase(new[] { 1, 1 }, 1, 0)]
|
||||
[TestCase(new[] { 1, 2, 2 }, 2, 1)]
|
||||
[TestCase(new[] { 1, 2, 2, 2 }, 2, 1)]
|
||||
[TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 1)]
|
||||
[TestCase(new[] { 1, 2, 2, 3 }, 2, 1)]
|
||||
public void TestBinarySearchLeftMostDuplicateScenarios(int[] values, int search, int expectedIndex)
|
||||
{
|
||||
var items = values.Select(t => new TimingControlPoint { Time = t }).ToArray();
|
||||
Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.Leftmost), Is.EqualTo(expectedIndex));
|
||||
}
|
||||
|
||||
[TestCase(new[] { 1, 1 }, 1, 1)]
|
||||
[TestCase(new[] { 1, 2, 2 }, 2, 2)]
|
||||
[TestCase(new[] { 1, 2, 2, 2 }, 2, 3)]
|
||||
[TestCase(new[] { 1, 2, 2, 2, 3 }, 2, 3)]
|
||||
[TestCase(new[] { 1, 2, 2, 3 }, 2, 2)]
|
||||
public void TestBinarySearchRightMostDuplicateScenarios(int[] values, int search, int expectedIndex)
|
||||
{
|
||||
var items = values.Select(t => new TimingControlPoint { Time = t }).ToArray();
|
||||
Assert.That(ControlPointInfo.BinarySearch(items, search, EqualitySelection.Rightmost), Is.EqualTo(expectedIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -537,7 +537,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
[TestCaseSource(nameof(correct_date_query_examples))]
|
||||
public void TestValidDateQueries(string dateQuery)
|
||||
{
|
||||
string query = $"played<{dateQuery} time";
|
||||
string query = $"lastplayed<{dateQuery} time";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual(true, filterCriteria.LastPlayed.HasFilter);
|
||||
@ -571,7 +571,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
[Test]
|
||||
public void TestGreaterDateQuery()
|
||||
{
|
||||
const string query = "played>50";
|
||||
const string query = "lastplayed>50";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.That(filterCriteria.LastPlayed.Max, Is.Not.Null);
|
||||
@ -584,7 +584,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
[Test]
|
||||
public void TestLowerDateQuery()
|
||||
{
|
||||
const string query = "played<50";
|
||||
const string query = "lastplayed<50";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.That(filterCriteria.LastPlayed.Max, Is.Null);
|
||||
@ -597,7 +597,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
[Test]
|
||||
public void TestBothSidesDateQuery()
|
||||
{
|
||||
const string query = "played>3M played<1y6M";
|
||||
const string query = "lastplayed>3M lastplayed<1y6M";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.That(filterCriteria.LastPlayed.Min, Is.Not.Null);
|
||||
@ -611,7 +611,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
[Test]
|
||||
public void TestEqualDateQuery()
|
||||
{
|
||||
const string query = "played=50";
|
||||
const string query = "lastplayed=50";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual(false, filterCriteria.LastPlayed.HasFilter);
|
||||
@ -620,11 +620,34 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
[Test]
|
||||
public void TestOutOfRangeDateQuery()
|
||||
{
|
||||
const string query = "played<10000y";
|
||||
const string query = "lastplayed<10000y";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual(true, filterCriteria.LastPlayed.HasFilter);
|
||||
Assert.AreEqual(DateTimeOffset.MinValue.AddMilliseconds(1), filterCriteria.LastPlayed.Min);
|
||||
}
|
||||
|
||||
private static readonly object[] played_query_tests =
|
||||
{
|
||||
new object[] { "0", DateTimeOffset.MinValue, true },
|
||||
new object[] { "0", DateTimeOffset.Now, false },
|
||||
new object[] { "false", DateTimeOffset.MinValue, true },
|
||||
new object[] { "false", DateTimeOffset.Now, false },
|
||||
|
||||
new object[] { "1", DateTimeOffset.MinValue, false },
|
||||
new object[] { "1", DateTimeOffset.Now, true },
|
||||
new object[] { "true", DateTimeOffset.MinValue, false },
|
||||
new object[] { "true", DateTimeOffset.Now, true },
|
||||
};
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(played_query_tests))]
|
||||
public void TestPlayedQuery(string query, DateTimeOffset reference, bool matched)
|
||||
{
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, $"played={query}");
|
||||
Assert.AreEqual(true, filterCriteria.LastPlayed.HasFilter);
|
||||
Assert.AreEqual(matched, filterCriteria.LastPlayed.IsInRange(reference));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -257,7 +257,7 @@ namespace osu.Game.Tests.Online
|
||||
{
|
||||
}
|
||||
|
||||
protected override string Target => null;
|
||||
protected override string Target => string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
BIN
osu.Game.Tests/Resources/Archives/argon-invalid-drawable.osk
Normal file
BIN
osu.Game.Tests/Resources/Archives/argon-invalid-drawable.osk
Normal file
Binary file not shown.
@ -12,6 +12,7 @@ using osu.Framework.IO.Stores;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Play.HUD.HitErrorMeters;
|
||||
using osu.Game.Skinning;
|
||||
@ -107,7 +108,7 @@ namespace osu.Game.Tests.Skins
|
||||
var skin = new TestSkin(new SkinInfo(), null, storage);
|
||||
|
||||
Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
|
||||
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(9));
|
||||
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(9));
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,8 +121,20 @@ namespace osu.Game.Tests.Skins
|
||||
var skin = new TestSkin(new SkinInfo(), null, storage);
|
||||
|
||||
Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
|
||||
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(10));
|
||||
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(PlayerName)));
|
||||
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(10));
|
||||
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(PlayerName)));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDeserialiseInvalidDrawables()
|
||||
{
|
||||
using (var stream = TestResources.OpenResource("Archives/argon-invalid-drawable.osk"))
|
||||
using (var storage = new ZipArchiveReader(stream))
|
||||
{
|
||||
var skin = new TestSkin(new SkinInfo(), null, storage);
|
||||
|
||||
Assert.That(skin.LayoutInfos.Any(kvp => kvp.Value.AllDrawables.Any(d => d.Type == typeof(StarFountain))), Is.False);
|
||||
}
|
||||
}
|
||||
|
||||
@ -134,10 +147,10 @@ namespace osu.Game.Tests.Skins
|
||||
var skin = new TestSkin(new SkinInfo(), null, storage);
|
||||
|
||||
Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
|
||||
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(6));
|
||||
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.SongSelect].AllDrawables.ToArray(), Has.Length.EqualTo(1));
|
||||
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(6));
|
||||
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.SongSelect].AllDrawables.ToArray(), Has.Length.EqualTo(1));
|
||||
|
||||
var skinnableInfo = skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.SongSelect].AllDrawables.First();
|
||||
var skinnableInfo = skin.LayoutInfos[GlobalSkinnableContainers.SongSelect].AllDrawables.First();
|
||||
|
||||
Assert.That(skinnableInfo.Type, Is.EqualTo(typeof(SkinnableSprite)));
|
||||
Assert.That(skinnableInfo.Settings.First().Key, Is.EqualTo("sprite_name"));
|
||||
@ -148,10 +161,10 @@ namespace osu.Game.Tests.Skins
|
||||
using (var storage = new ZipArchiveReader(stream))
|
||||
{
|
||||
var skin = new TestSkin(new SkinInfo(), null, storage);
|
||||
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(8));
|
||||
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(UnstableRateCounter)));
|
||||
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(ColourHitErrorMeter)));
|
||||
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(LegacySongProgress)));
|
||||
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(8));
|
||||
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(UnstableRateCounter)));
|
||||
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(ColourHitErrorMeter)));
|
||||
Assert.That(skin.LayoutInfos[GlobalSkinnableContainers.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(LegacySongProgress)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,7 @@ using osu.Framework.Screens;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
@ -48,13 +49,18 @@ namespace osu.Game.Tests.Visual.Background
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
{
|
||||
DetachedBeatmapStore detachedBeatmapStore;
|
||||
|
||||
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
|
||||
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
|
||||
Dependencies.Cache(new OsuConfigManager(LocalStorage));
|
||||
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore());
|
||||
Dependencies.Cache(Realm);
|
||||
|
||||
manager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||
|
||||
Add(detachedBeatmapStore);
|
||||
|
||||
Beatmap.SetDefault();
|
||||
}
|
||||
|
||||
|
@ -2,19 +2,21 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Metadata;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.OnlinePlay.DailyChallenge;
|
||||
using osu.Game.Tests.Visual.Metadata;
|
||||
using osu.Game.Tests.Visual.OnlinePlay;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
using CreateRoomRequest = osu.Game.Online.Rooms.CreateRoomRequest;
|
||||
|
||||
namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
@ -27,63 +29,61 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
[Cached(typeof(INotificationOverlay))]
|
||||
private NotificationOverlay notificationOverlay = new NotificationOverlay();
|
||||
|
||||
private Room room = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
base.Content.Add(notificationOverlay);
|
||||
base.Content.Add(metadataClient);
|
||||
Add(notificationOverlay);
|
||||
Add(metadataClient);
|
||||
|
||||
// add button to observe for daily challenge changes and perform its logic.
|
||||
Add(new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D));
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Solo]
|
||||
public void TestDailyChallenge()
|
||||
{
|
||||
var room = new Room
|
||||
{
|
||||
RoomID = { Value = 1234 },
|
||||
Name = { Value = "Daily Challenge: June 4, 2024" },
|
||||
Playlist =
|
||||
{
|
||||
new PlaylistItem(CreateAPIBeatmapSet().Beatmaps.First())
|
||||
{
|
||||
RequiredMods = [new APIMod(new OsuModTraceable())],
|
||||
AllowedMods = [new APIMod(new OsuModDoubleTime())]
|
||||
}
|
||||
},
|
||||
EndDate = { Value = DateTimeOffset.Now.AddHours(12) },
|
||||
Category = { Value = RoomCategory.DailyChallenge }
|
||||
};
|
||||
|
||||
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
|
||||
AddStep("push screen", () => LoadScreen(new Screens.OnlinePlay.DailyChallenge.DailyChallengeIntro(room)));
|
||||
startChallenge(1234);
|
||||
AddStep("push screen", () => LoadScreen(new DailyChallengeIntro(room)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNotifications()
|
||||
public void TestPlayIntroOnceFlag()
|
||||
{
|
||||
var room = new Room
|
||||
startChallenge(1234);
|
||||
AddStep("set intro played flag", () => Dependencies.Get<SessionStatics>().SetValue(Static.DailyChallengeIntroPlayed, true));
|
||||
|
||||
startChallenge(1235);
|
||||
|
||||
AddAssert("intro played flag reset", () => Dependencies.Get<SessionStatics>().Get<bool>(Static.DailyChallengeIntroPlayed), () => Is.False);
|
||||
|
||||
AddStep("push screen", () => LoadScreen(new DailyChallengeIntro(room)));
|
||||
AddUntilStep("intro played flag set", () => Dependencies.Get<SessionStatics>().Get<bool>(Static.DailyChallengeIntroPlayed), () => Is.True);
|
||||
}
|
||||
|
||||
private void startChallenge(int roomId)
|
||||
{
|
||||
AddStep("add room", () =>
|
||||
{
|
||||
RoomID = { Value = 1234 },
|
||||
Name = { Value = "Daily Challenge: June 4, 2024" },
|
||||
Playlist =
|
||||
API.Perform(new CreateRoomRequest(room = new Room
|
||||
{
|
||||
new PlaylistItem(TestResources.CreateTestBeatmapSetInfo().Beatmaps.First())
|
||||
RoomID = { Value = roomId },
|
||||
Name = { Value = "Daily Challenge: June 4, 2024" },
|
||||
Playlist =
|
||||
{
|
||||
RequiredMods = [new APIMod(new OsuModTraceable())],
|
||||
AllowedMods = [new APIMod(new OsuModDoubleTime())]
|
||||
}
|
||||
},
|
||||
EndDate = { Value = DateTimeOffset.Now.AddHours(12) },
|
||||
Category = { Value = RoomCategory.DailyChallenge }
|
||||
};
|
||||
|
||||
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
|
||||
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 });
|
||||
|
||||
Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
|
||||
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
|
||||
AddUntilStep("wait for screen", () => screen.IsCurrentScreen());
|
||||
AddStep("daily challenge ended", () => metadataClient.DailyChallengeInfo.Value = null);
|
||||
new PlaylistItem(CreateAPIBeatmap(new OsuRuleset().RulesetInfo))
|
||||
{
|
||||
RequiredMods = [new APIMod(new OsuModTraceable())],
|
||||
AllowedMods = [new APIMod(new OsuModDoubleTime())]
|
||||
}
|
||||
},
|
||||
StartDate = { Value = DateTimeOffset.Now },
|
||||
EndDate = { Value = DateTimeOffset.Now.AddHours(24) },
|
||||
Category = { Value = RoomCategory.DailyChallenge }
|
||||
}));
|
||||
});
|
||||
AddStep("signal client", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = roomId }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,10 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
beatmap.ControlPointInfo.Add(100000, new TimingControlPoint { BeatLength = 100 });
|
||||
beatmap.ControlPointInfo.Add(50000, new DifficultyControlPoint { SliderVelocity = 2 });
|
||||
beatmap.ControlPointInfo.Add(80000, new EffectControlPoint { KiaiMode = true });
|
||||
beatmap.ControlPointInfo.Add(110000, new EffectControlPoint { KiaiMode = false });
|
||||
beatmap.BeatmapInfo.Bookmarks = new[] { 75000, 125000 };
|
||||
beatmap.Breaks.Add(new ManualBreakPeriod(90000, 120000));
|
||||
|
||||
editorBeatmap = new EditorBeatmap(beatmap);
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using Humanizer;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@ -307,6 +308,46 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
hitObjectNodeHasSampleVolume(0, 1, 10);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSamplePointSeek()
|
||||
{
|
||||
AddStep("add slider", () =>
|
||||
{
|
||||
EditorBeatmap.Clear();
|
||||
EditorBeatmap.Add(new Slider
|
||||
{
|
||||
Position = new Vector2(256, 256),
|
||||
StartTime = 0,
|
||||
Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), new PathControlPoint(new Vector2(250, 0)) }),
|
||||
Samples =
|
||||
{
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL)
|
||||
},
|
||||
NodeSamples =
|
||||
{
|
||||
new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) },
|
||||
new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) },
|
||||
},
|
||||
RepeatCount = 1
|
||||
});
|
||||
});
|
||||
|
||||
seekSamplePiece(-1);
|
||||
editorTimeIs(0);
|
||||
samplePopoverIsOpen();
|
||||
seekSamplePiece(-1);
|
||||
editorTimeIs(0);
|
||||
samplePopoverIsOpen();
|
||||
seekSamplePiece(1);
|
||||
editorTimeIs(406);
|
||||
seekSamplePiece(1);
|
||||
editorTimeIs(813);
|
||||
seekSamplePiece(1);
|
||||
editorTimeIs(1627);
|
||||
seekSamplePiece(1);
|
||||
editorTimeIs(1627);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHotkeysMultipleSelectionWithSameSampleBank()
|
||||
{
|
||||
@ -548,6 +589,63 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
hitObjectNodeHasSamples(2, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHotkeysUnifySliderSamplesAndNodeSamples()
|
||||
{
|
||||
AddStep("add slider", () =>
|
||||
{
|
||||
EditorBeatmap.Clear();
|
||||
EditorBeatmap.Add(new Slider
|
||||
{
|
||||
Position = new Vector2(256, 256),
|
||||
StartTime = 1000,
|
||||
Path = new SliderPath(new[] { new PathControlPoint(Vector2.Zero), new PathControlPoint(new Vector2(250, 0)) }),
|
||||
Samples =
|
||||
{
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT),
|
||||
new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_DRUM),
|
||||
},
|
||||
NodeSamples = new List<IList<HitSampleInfo>>
|
||||
{
|
||||
new List<HitSampleInfo>
|
||||
{
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_DRUM),
|
||||
new HitSampleInfo(HitSampleInfo.HIT_CLAP, bank: HitSampleInfo.BANK_DRUM),
|
||||
},
|
||||
new List<HitSampleInfo>
|
||||
{
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, bank: HitSampleInfo.BANK_SOFT),
|
||||
new HitSampleInfo(HitSampleInfo.HIT_WHISTLE, bank: HitSampleInfo.BANK_SOFT),
|
||||
},
|
||||
}
|
||||
});
|
||||
});
|
||||
AddStep("select everything", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
|
||||
|
||||
AddStep("set soft bank", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.LShift);
|
||||
InputManager.Key(Key.E);
|
||||
InputManager.ReleaseKey(Key.LShift);
|
||||
});
|
||||
|
||||
hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
|
||||
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
|
||||
hitObjectNodeHasSampleBank(0, 0, HitSampleInfo.BANK_SOFT);
|
||||
hitObjectNodeHasSamples(0, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP);
|
||||
hitObjectNodeHasSampleBank(0, 1, HitSampleInfo.BANK_SOFT);
|
||||
hitObjectNodeHasSamples(0, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
|
||||
|
||||
AddStep("unify whistle addition", () => InputManager.Key(Key.W));
|
||||
|
||||
hitObjectHasSampleBank(0, HitSampleInfo.BANK_SOFT);
|
||||
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
|
||||
hitObjectNodeHasSampleBank(0, 0, HitSampleInfo.BANK_SOFT);
|
||||
hitObjectNodeHasSamples(0, 0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_CLAP, HitSampleInfo.HIT_WHISTLE);
|
||||
hitObjectNodeHasSampleBank(0, 1, HitSampleInfo.BANK_SOFT);
|
||||
hitObjectNodeHasSamples(0, 1, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_WHISTLE);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSelectingObjectDoesNotMutateSamples()
|
||||
{
|
||||
@ -569,7 +667,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () =>
|
||||
{
|
||||
var samplePiece = this.ChildrenOfType<SamplePointPiece>().Single(piece => piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex));
|
||||
var samplePiece = this.ChildrenOfType<SamplePointPiece>().Single(piece => piece is not NodeSamplePointPiece && piece.HitObject == EditorBeatmap.HitObjects.ElementAt(objectIndex));
|
||||
|
||||
InputManager.MoveMouseTo(samplePiece);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
@ -583,6 +681,21 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
private void seekSamplePiece(int direction) => AddStep($"seek sample piece {direction}", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.PressKey(Key.ShiftLeft);
|
||||
InputManager.Key(direction < 1 ? Key.Left : Key.Right);
|
||||
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
|
||||
private void samplePopoverIsOpen() => AddUntilStep("sample popover is open", () =>
|
||||
{
|
||||
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().SingleOrDefault(o => o.IsPresent);
|
||||
return popover != null;
|
||||
});
|
||||
|
||||
private void samplePopoverHasNoFocus() => AddUntilStep("sample popover textbox not focused", () =>
|
||||
{
|
||||
var popover = this.ChildrenOfType<SamplePointPiece.SampleEditPopover>().SingleOrDefault();
|
||||
@ -727,5 +840,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
|
||||
return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank);
|
||||
});
|
||||
|
||||
private void editorTimeIs(double time) => AddAssert($"editor time is {time}", () => Precision.AlmostEquals(EditorClock.CurrentTimeAccurate, time, 1));
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
() => Is.EqualTo(1));
|
||||
|
||||
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
|
||||
AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
|
||||
AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
|
||||
|
||||
addStepClickLink("00:00:000 (1)", waitForSeek: false);
|
||||
AddUntilStep("received 'must be in edit'",
|
||||
@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddUntilStep("Wait for song select", () =>
|
||||
Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
||||
&& songSelect.IsLoaded
|
||||
&& songSelect.BeatmapSetsLoaded
|
||||
);
|
||||
AddStep("Switch ruleset", () => Game.Ruleset.Value = ruleset);
|
||||
AddStep("Open editor for ruleset", () =>
|
||||
|
@ -96,32 +96,6 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCommitPlacementViaGlobalAction()
|
||||
{
|
||||
Playfield playfield = null!;
|
||||
|
||||
AddStep("select slider placement tool", () => InputManager.Key(Key.Number3));
|
||||
AddStep("move mouse to top left of playfield", () =>
|
||||
{
|
||||
playfield = this.ChildrenOfType<Playfield>().Single();
|
||||
var location = (3 * playfield.ScreenSpaceDrawQuad.TopLeft + playfield.ScreenSpaceDrawQuad.BottomRight) / 4;
|
||||
InputManager.MoveMouseTo(location);
|
||||
});
|
||||
AddStep("begin placement", () => InputManager.Click(MouseButton.Left));
|
||||
AddStep("move mouse to bottom right of playfield", () =>
|
||||
{
|
||||
var location = (playfield.ScreenSpaceDrawQuad.TopLeft + 3 * playfield.ScreenSpaceDrawQuad.BottomRight) / 4;
|
||||
InputManager.MoveMouseTo(location);
|
||||
});
|
||||
AddStep("confirm via global action", () =>
|
||||
{
|
||||
globalActionContainer.TriggerPressed(GlobalAction.Select);
|
||||
globalActionContainer.TriggerReleased(GlobalAction.Select);
|
||||
});
|
||||
AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAbortPlacementViaGlobalAction()
|
||||
{
|
||||
@ -272,11 +246,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
var location = (playfield.ScreenSpaceDrawQuad.TopLeft + 3 * playfield.ScreenSpaceDrawQuad.BottomRight) / 4;
|
||||
InputManager.MoveMouseTo(location);
|
||||
});
|
||||
AddStep("confirm via global action", () =>
|
||||
{
|
||||
globalActionContainer.TriggerPressed(GlobalAction.Select);
|
||||
globalActionContainer.TriggerReleased(GlobalAction.Select);
|
||||
});
|
||||
AddStep("confirm via right click", () => InputManager.Click(MouseButton.Right));
|
||||
AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1));
|
||||
|
||||
AddAssert("slider samples have drum bank", () => EditorBeatmap.HitObjects[0].Samples.All(s => s.Bank == HitSampleInfo.BANK_DRUM));
|
||||
|
@ -114,40 +114,6 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTrackingCurrentTimeWhileRunning()
|
||||
{
|
||||
AddStep("Select first effect point", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(Child.ChildrenOfType<EffectRowAttribute>().First());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 54670);
|
||||
AddUntilStep("Ensure seeked to correct time", () => EditorClock.CurrentTimeAccurate == 54670);
|
||||
|
||||
AddStep("Seek to just before next point", () => EditorClock.Seek(69000));
|
||||
AddStep("Start clock", () => EditorClock.Start());
|
||||
|
||||
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTrackingCurrentTimeWhilePaused()
|
||||
{
|
||||
AddStep("Select first effect point", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(Child.ChildrenOfType<EffectRowAttribute>().First());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 54670);
|
||||
AddUntilStep("Ensure seeked to correct time", () => EditorClock.CurrentTimeAccurate == 54670);
|
||||
|
||||
AddStep("Seek to later", () => EditorClock.Seek(80000));
|
||||
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScrollControlGroupIntoView()
|
||||
{
|
||||
|
@ -37,8 +37,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
public void TestEmptyLegacyBeatmapSkinFallsBack()
|
||||
{
|
||||
CreateSkinTest(TrianglesSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null));
|
||||
AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinComponentsContainer>().All(c => c.ComponentsLoaded));
|
||||
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinComponentsContainerLookup.TargetArea.MainHUDComponents, skinManager.CurrentSkin.Value));
|
||||
AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinnableContainer>().All(c => c.ComponentsLoaded));
|
||||
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(GlobalSkinnableContainers.MainHUDComponents, skinManager.CurrentSkin.Value));
|
||||
}
|
||||
|
||||
protected void CreateSkinTest(SkinInfo gameCurrentSkin, Func<ISkin> getBeatmapSkin)
|
||||
@ -53,9 +53,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
});
|
||||
}
|
||||
|
||||
protected bool AssertComponentsFromExpectedSource(SkinComponentsContainerLookup.TargetArea target, ISkin expectedSource)
|
||||
protected bool AssertComponentsFromExpectedSource(GlobalSkinnableContainers target, ISkin expectedSource)
|
||||
{
|
||||
var targetContainer = Player.ChildrenOfType<SkinComponentsContainer>().First(s => s.Lookup.Target == target);
|
||||
var targetContainer = Player.ChildrenOfType<SkinnableContainer>().First(s => s.Lookup.Lookup == target);
|
||||
var actualComponentsContainer = targetContainer.ChildrenOfType<Container>().SingleOrDefault(c => c.Parent == targetContainer);
|
||||
|
||||
if (actualComponentsContainer == null)
|
||||
@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
var actualInfo = actualComponentsContainer.CreateSerialisedInfo();
|
||||
|
||||
var expectedComponentsContainer = expectedSource.GetDrawableComponent(new SkinComponentsContainerLookup(target)) as Container;
|
||||
var expectedComponentsContainer = expectedSource.GetDrawableComponent(new GlobalSkinnableContainerLookup(target)) as Container;
|
||||
if (expectedComponentsContainer == null)
|
||||
return false;
|
||||
|
||||
|
@ -7,9 +7,13 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
@ -28,14 +32,19 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
public TestSceneBreakTracker()
|
||||
{
|
||||
AddRange(new Drawable[]
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = Color4.White,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
breakTracker = new TestBreakTracker(),
|
||||
breakOverlay = new BreakOverlay(true, null)
|
||||
breakOverlay = new BreakOverlay(true, new ScoreProcessor(new OsuRuleset()))
|
||||
{
|
||||
ProcessCustomClock = false,
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new TrackVirtual(60000), false, false);
|
||||
|
||||
// best way to check without exposing.
|
||||
private Drawable hideTarget => hudOverlay.ChildrenOfType<SkinComponentsContainer>().First();
|
||||
private Drawable hideTarget => hudOverlay.ChildrenOfType<SkinnableContainer>().First();
|
||||
private Drawable keyCounterContent => hudOverlay.ChildrenOfType<KeyCounterDisplay>().First().ChildrenOfType<Drawable>().Skip(1).First();
|
||||
|
||||
public TestSceneHUDOverlay()
|
||||
@ -174,6 +174,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
holdForMenu.Action += () => activated = true;
|
||||
});
|
||||
|
||||
AddStep("set hold button always visible", () => localConfig.SetValue(OsuSetting.AlwaysShowHoldForMenuButton, true));
|
||||
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
|
||||
AddUntilStep("hidetarget is hidden", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0));
|
||||
|
||||
@ -214,6 +215,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
progress.ChildrenOfType<ArgonSongProgressBar>().Single().OnSeek += _ => seeked = true;
|
||||
});
|
||||
|
||||
AddStep("set hold button always visible", () => localConfig.SetValue(OsuSetting.AlwaysShowHoldForMenuButton, true));
|
||||
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
|
||||
AddUntilStep("hidetarget is hidden", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0));
|
||||
|
||||
@ -240,8 +242,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
createNew();
|
||||
|
||||
AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().Single().Alpha == 0);
|
||||
AddUntilStep("wait for hud load", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().All(c => c.ComponentsLoaded));
|
||||
AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType<SkinnableContainer>().Single().Alpha == 0);
|
||||
AddUntilStep("wait for hud load", () => hudOverlay.ChildrenOfType<SkinnableContainer>().All(c => c.ComponentsLoaded));
|
||||
|
||||
AddStep("bind on update", () =>
|
||||
{
|
||||
@ -258,10 +260,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
createNew();
|
||||
|
||||
AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().Single().Alpha == 0);
|
||||
AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType<SkinnableContainer>().Single().Alpha == 0);
|
||||
|
||||
AddStep("reload components", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().Single().Reload());
|
||||
AddUntilStep("skinnable components loaded", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().Single().ComponentsLoaded);
|
||||
AddStep("reload components", () => hudOverlay.ChildrenOfType<SkinnableContainer>().Single().Reload());
|
||||
AddUntilStep("skinnable components loaded", () => hudOverlay.ChildrenOfType<SkinnableContainer>().Single().ComponentsLoaded);
|
||||
}
|
||||
|
||||
private void createNew(Action<HUDOverlay>? action = null)
|
||||
|
@ -1,13 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
@ -21,11 +21,19 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
protected override double TimePerAction => 100; // required for the early exit test, since hold-to-confirm delay is 200ms
|
||||
|
||||
private HoldForMenuButton holdForMenuButton;
|
||||
private HoldForMenuButton holdForMenuButton = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("set button always on", () =>
|
||||
{
|
||||
config.SetValue(OsuSetting.AlwaysShowHoldForMenuButton, true);
|
||||
});
|
||||
|
||||
AddStep("create button", () =>
|
||||
{
|
||||
exitAction = false;
|
||||
|
@ -27,6 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[TestCase(2000, 0)]
|
||||
[TestCase(3000, first_hit_object - 3000)]
|
||||
[TestCase(10000, first_hit_object - 10000)]
|
||||
[FlakyTest]
|
||||
public void TestLeadInProducesCorrectStartTime(double leadIn, double expectedStartTime)
|
||||
{
|
||||
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo)
|
||||
@ -41,6 +42,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[TestCase(0, 0)]
|
||||
[TestCase(-1000, -1000)]
|
||||
[TestCase(-10000, -10000)]
|
||||
[FlakyTest]
|
||||
public void TestStoryboardProducesCorrectStartTimeSimpleAlpha(double firstStoryboardEvent, double expectedStartTime)
|
||||
{
|
||||
var storyboard = new Storyboard();
|
||||
@ -64,6 +66,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[TestCase(0, 0, true)]
|
||||
[TestCase(-1000, -1000, true)]
|
||||
[TestCase(-10000, -10000, true)]
|
||||
[FlakyTest]
|
||||
public void TestStoryboardProducesCorrectStartTimeFadeInAfterOtherEvents(double firstStoryboardEvent, double expectedStartTime, bool addEventToLoop)
|
||||
{
|
||||
const double loop_start_time = -20000;
|
||||
|
@ -320,6 +320,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestExitViaHoldToExit()
|
||||
{
|
||||
AddStep("set hold button always visible", () => LocalConfig.SetValue(OsuSetting.AlwaysShowHoldForMenuButton, true));
|
||||
|
||||
AddStep("exit", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(Player.HUDOverlay.HoldToQuit.First(c => c is HoldToConfirmContainer));
|
||||
|
@ -47,6 +47,16 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||
StartTime = 5000,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||
StartTime = 10000,
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||
StartTime = 15000,
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -256,7 +266,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOsuRegisterInputFromPressingOrangeCursorButPressIsBlocked()
|
||||
public void TestOsuHitCircleNotReceivingInputOnResume()
|
||||
{
|
||||
KeyCounter counter = null!;
|
||||
|
||||
@ -281,19 +291,82 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddAssert("button is released in kbc", () => !Player.DrawableRuleset.Playfield.FindClosestParent<OsuInputManager>()!.PressedActions.Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOsuHitCircleNotReceivingInputOnResume_PauseWhileHoldingSameKey()
|
||||
{
|
||||
KeyCounter counter = null!;
|
||||
|
||||
loadPlayer(() => new OsuRuleset());
|
||||
AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<OsuAction> actionTrigger && actionTrigger.Action == OsuAction.LeftButton));
|
||||
|
||||
AddStep("press Z", () => InputManager.PressKey(Key.Z));
|
||||
AddAssert("circle hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(1));
|
||||
|
||||
AddStep("pause", () => Player.Pause());
|
||||
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
|
||||
|
||||
AddStep("resume", () => Player.Resume());
|
||||
AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single()));
|
||||
AddStep("press Z to resume", () => InputManager.PressKey(Key.Z));
|
||||
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
|
||||
|
||||
checkKey(() => counter, 1, false);
|
||||
|
||||
seekTo(5000);
|
||||
|
||||
AddStep("press Z", () => InputManager.PressKey(Key.Z));
|
||||
|
||||
checkKey(() => counter, 2, true);
|
||||
AddAssert("circle hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(2));
|
||||
|
||||
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
|
||||
checkKey(() => counter, 2, false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOsuHitCircleNotReceivingInputOnResume_PauseWhileHoldingOtherKey()
|
||||
{
|
||||
loadPlayer(() => new OsuRuleset());
|
||||
|
||||
AddStep("press X", () => InputManager.PressKey(Key.X));
|
||||
AddAssert("circle hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(1));
|
||||
|
||||
seekTo(5000);
|
||||
|
||||
AddStep("pause", () => Player.Pause());
|
||||
AddStep("release X", () => InputManager.ReleaseKey(Key.X));
|
||||
|
||||
AddStep("resume", () => Player.Resume());
|
||||
AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single()));
|
||||
AddStep("press Z to resume", () => InputManager.PressKey(Key.Z));
|
||||
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
|
||||
|
||||
AddAssert("circle not hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(1));
|
||||
|
||||
AddStep("press X", () => InputManager.PressKey(Key.X));
|
||||
AddStep("release X", () => InputManager.ReleaseKey(Key.X));
|
||||
|
||||
AddAssert("circle hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(2));
|
||||
}
|
||||
|
||||
private void loadPlayer(Func<Ruleset> createRuleset)
|
||||
{
|
||||
AddStep("set ruleset", () => currentRuleset = createRuleset());
|
||||
AddStep("load player", LoadPlayer);
|
||||
AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1);
|
||||
AddUntilStep("wait for hud", () => Player.HUDOverlay.ChildrenOfType<SkinComponentsContainer>().All(s => s.ComponentsLoaded));
|
||||
AddUntilStep("wait for hud", () => Player.HUDOverlay.ChildrenOfType<SkinnableContainer>().All(s => s.ComponentsLoaded));
|
||||
|
||||
AddStep("seek to gameplay", () => Player.GameplayClockContainer.Seek(0));
|
||||
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(0).Within(500));
|
||||
seekTo(0);
|
||||
AddAssert("not in break", () => !Player.IsBreakTime.Value);
|
||||
AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield));
|
||||
}
|
||||
|
||||
private void seekTo(double time)
|
||||
{
|
||||
AddStep($"seek to {time}ms", () => Player.GameplayClockContainer.Seek(time));
|
||||
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(time).Within(500));
|
||||
}
|
||||
|
||||
private void checkKey(Func<KeyCounter> counter, int count, bool active)
|
||||
{
|
||||
AddAssert($"key count = {count}", () => counter().CountPresses.Value, () => Is.EqualTo(count));
|
||||
|
@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Resolved]
|
||||
private SkinManager skins { get; set; } = null!;
|
||||
|
||||
private SkinComponentsContainer targetContainer => Player.ChildrenOfType<SkinComponentsContainer>().First();
|
||||
private SkinnableContainer targetContainer => Player.ChildrenOfType<SkinnableContainer>().First();
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
AddStep("Add big black boxes", () =>
|
||||
{
|
||||
var target = Player.ChildrenOfType<SkinComponentsContainer>().First();
|
||||
var target = Player.ChildrenOfType<SkinnableContainer>().First();
|
||||
target.Add(box1 = new BigBlackBox
|
||||
{
|
||||
Position = new Vector2(-90),
|
||||
@ -200,14 +200,14 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestUndoEditHistory()
|
||||
{
|
||||
SkinComponentsContainer firstTarget = null!;
|
||||
SkinnableContainer firstTarget = null!;
|
||||
TestSkinEditorChangeHandler changeHandler = null!;
|
||||
byte[] defaultState = null!;
|
||||
IEnumerable<ISerialisableDrawable> testComponents = null!;
|
||||
|
||||
AddStep("Load necessary things", () =>
|
||||
{
|
||||
firstTarget = Player.ChildrenOfType<SkinComponentsContainer>().First();
|
||||
firstTarget = Player.ChildrenOfType<SkinnableContainer>().First();
|
||||
changeHandler = new TestSkinEditorChangeHandler(firstTarget);
|
||||
|
||||
changeHandler.SaveState();
|
||||
@ -377,11 +377,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
() => Is.EqualTo(3));
|
||||
}
|
||||
|
||||
private SkinComponentsContainer globalHUDTarget => Player.ChildrenOfType<SkinComponentsContainer>()
|
||||
.Single(c => c.Lookup.Target == SkinComponentsContainerLookup.TargetArea.MainHUDComponents && c.Lookup.Ruleset == null);
|
||||
private SkinnableContainer globalHUDTarget => Player.ChildrenOfType<SkinnableContainer>()
|
||||
.Single(c => c.Lookup.Lookup == GlobalSkinnableContainers.MainHUDComponents && c.Lookup.Ruleset == null);
|
||||
|
||||
private SkinComponentsContainer rulesetHUDTarget => Player.ChildrenOfType<SkinComponentsContainer>()
|
||||
.Single(c => c.Lookup.Target == SkinComponentsContainerLookup.TargetArea.MainHUDComponents && c.Lookup.Ruleset != null);
|
||||
private SkinnableContainer rulesetHUDTarget => Player.ChildrenOfType<SkinnableContainer>()
|
||||
.Single(c => c.Lookup.Lookup == GlobalSkinnableContainers.MainHUDComponents && c.Lookup.Ruleset != null);
|
||||
|
||||
[Test]
|
||||
public void TestMigrationArgon()
|
||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestToggleEditor()
|
||||
{
|
||||
var skinComponentsContainer = new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.SongSelect));
|
||||
var skinComponentsContainer = new SkinnableContainer(new GlobalSkinnableContainerLookup(GlobalSkinnableContainers.SongSelect));
|
||||
|
||||
AddStep("show available components", () => SetContents(_ => new SkinComponentToolbox(skinComponentsContainer, null)
|
||||
{
|
||||
|
@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
private IEnumerable<HUDOverlay> hudOverlays => CreatedDrawables.OfType<HUDOverlay>();
|
||||
|
||||
// best way to check without exposing.
|
||||
private Drawable hideTarget => hudOverlay.ChildrenOfType<SkinComponentsContainer>().First();
|
||||
private Drawable hideTarget => hudOverlay.ChildrenOfType<SkinnableContainer>().First();
|
||||
private Drawable keyCounterFlow => hudOverlay.ChildrenOfType<KeyCounterDisplay>().First().ChildrenOfType<FillFlowContainer<KeyCounter>>().Single();
|
||||
|
||||
public TestSceneSkinnableHUDOverlay()
|
||||
@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
});
|
||||
AddUntilStep("HUD overlay loaded", () => hudOverlay.IsAlive);
|
||||
AddUntilStep("components container loaded",
|
||||
() => hudOverlay.ChildrenOfType<SkinComponentsContainer>().Any(scc => scc.ComponentsLoaded));
|
||||
() => hudOverlay.ChildrenOfType<SkinnableContainer>().Any(scc => scc.ComponentsLoaded));
|
||||
}
|
||||
|
||||
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
|
||||
|
@ -4,17 +4,17 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Menus
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneStarFountain : OsuTestScene
|
||||
{
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
[Test]
|
||||
public void TestMenu()
|
||||
{
|
||||
AddStep("make fountains", () =>
|
||||
{
|
||||
@ -34,11 +34,7 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPew()
|
||||
{
|
||||
AddRepeatStep("activate fountains sometimes", () =>
|
||||
{
|
||||
foreach (var fountain in Children.OfType<StarFountain>())
|
||||
@ -48,5 +44,34 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGameplay()
|
||||
{
|
||||
AddStep("make fountains", () =>
|
||||
{
|
||||
Children = new[]
|
||||
{
|
||||
new KiaiGameplayFountains.GameplayStarFountain
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
X = 75,
|
||||
},
|
||||
new KiaiGameplayFountains.GameplayStarFountain
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
X = -75,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
AddStep("activate fountains", () =>
|
||||
{
|
||||
((StarFountain)Children[0]).Shoot(1);
|
||||
((StarFountain)Children[1]).Shoot(-1);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -97,6 +97,7 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
public void TestTransientUserStatisticsDisplay()
|
||||
{
|
||||
AddStep("Log in", () => dummyAPI.Login("wang", "jang"));
|
||||
|
||||
AddStep("Gain", () =>
|
||||
{
|
||||
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
|
||||
@ -113,6 +114,7 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
PP = 1357
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("Loss", () =>
|
||||
{
|
||||
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
|
||||
@ -129,7 +131,9 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
PP = 1234
|
||||
});
|
||||
});
|
||||
AddStep("No change", () =>
|
||||
|
||||
// Tests flooring logic works as expected.
|
||||
AddStep("Tiny increase in PP", () =>
|
||||
{
|
||||
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
|
||||
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate(
|
||||
@ -137,14 +141,32 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
new UserStatistics
|
||||
{
|
||||
GlobalRank = 111_111,
|
||||
PP = 1357
|
||||
PP = 1357.6m
|
||||
},
|
||||
new UserStatistics
|
||||
{
|
||||
GlobalRank = 111_111,
|
||||
PP = 1357
|
||||
PP = 1358.1m
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("No change 1", () =>
|
||||
{
|
||||
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
|
||||
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate(
|
||||
new ScoreInfo(),
|
||||
new UserStatistics
|
||||
{
|
||||
GlobalRank = 111_111,
|
||||
PP = 1357m
|
||||
},
|
||||
new UserStatistics
|
||||
{
|
||||
GlobalRank = 111_111,
|
||||
PP = 1357.1m
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("Was null", () =>
|
||||
{
|
||||
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
|
||||
@ -161,6 +183,7 @@ namespace osu.Game.Tests.Visual.Menus
|
||||
PP = 1357
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("Became null", () =>
|
||||
{
|
||||
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
@ -45,9 +46,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
{
|
||||
DetachedBeatmapStore detachedBeatmapStore;
|
||||
|
||||
Dependencies.Cache(new RealmRulesetStore(Realm));
|
||||
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
|
||||
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore());
|
||||
Dependencies.Cache(Realm);
|
||||
|
||||
Add(detachedBeatmapStore);
|
||||
}
|
||||
|
||||
public override void SetUpSteps()
|
||||
|
@ -19,6 +19,7 @@ using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
@ -65,9 +66,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
{
|
||||
DetachedBeatmapStore detachedBeatmapStore;
|
||||
|
||||
Dependencies.Cache(new RealmRulesetStore(Realm));
|
||||
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default));
|
||||
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore());
|
||||
Dependencies.Cache(Realm);
|
||||
|
||||
Add(detachedBeatmapStore);
|
||||
}
|
||||
|
||||
public override void SetUpSteps()
|
||||
|
@ -45,11 +45,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
{
|
||||
DetachedBeatmapStore detachedBeatmapStore;
|
||||
|
||||
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
|
||||
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
|
||||
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore());
|
||||
Dependencies.Cache(Realm);
|
||||
|
||||
importedBeatmapSet = manager.Import(TestResources.CreateTestBeatmapSetInfo(8, rulesets.AvailableRulesets.ToArray()));
|
||||
|
||||
Add(detachedBeatmapStore);
|
||||
}
|
||||
|
||||
public override void SetUpSteps()
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@ -33,13 +34,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
{
|
||||
DetachedBeatmapStore detachedBeatmapStore;
|
||||
|
||||
Dependencies.Cache(new RealmRulesetStore(Realm));
|
||||
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
|
||||
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore());
|
||||
Dependencies.Cache(Realm);
|
||||
|
||||
var beatmapSet = TestResources.CreateTestBeatmapSetInfo();
|
||||
|
||||
manager.Import(beatmapSet);
|
||||
|
||||
Add(detachedBeatmapStore);
|
||||
}
|
||||
|
||||
public override void SetUpSteps()
|
||||
|
@ -165,16 +165,19 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Solo]
|
||||
public void TestEditorGameplayTestAlwaysUsesOriginalRuleset()
|
||||
{
|
||||
prepareBeatmap();
|
||||
|
||||
AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo);
|
||||
AddStep("switch ruleset at song select", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo);
|
||||
|
||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||
AddStep("test gameplay", () => getEditor().TestGameplay());
|
||||
|
||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||
AddAssert("editor ruleset is osu!", () => Game.Ruleset.Value, () => Is.EqualTo(new OsuRuleset().RulesetInfo));
|
||||
|
||||
AddStep("test gameplay", () => getEditor().TestGameplay());
|
||||
AddUntilStep("wait for player", () =>
|
||||
{
|
||||
// notifications may fire at almost any inopportune time and cause annoying test failures.
|
||||
@ -183,8 +186,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
Game.CloseAllOverlays();
|
||||
return Game.ScreenStack.CurrentScreen is EditorPlayer editorPlayer && editorPlayer.IsLoaded;
|
||||
});
|
||||
|
||||
AddAssert("current ruleset is osu!", () => Game.Ruleset.Value.Equals(new OsuRuleset().RulesetInfo));
|
||||
AddAssert("gameplay ruleset is osu!", () => Game.Ruleset.Value, () => Is.EqualTo(new OsuRuleset().RulesetInfo));
|
||||
|
||||
AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(PlaySongSelect).Yield()));
|
||||
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
|
||||
@ -352,7 +354,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddUntilStep("wait for song select",
|
||||
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
||||
&& songSelect.IsLoaded);
|
||||
&& songSelect.BeatmapSetsLoaded);
|
||||
}
|
||||
|
||||
private void openEditor()
|
||||
|
@ -176,6 +176,12 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
|
||||
private void confirmBeatmapInSongSelect(Func<BeatmapSetInfo> getImport)
|
||||
{
|
||||
AddUntilStep("wait for carousel loaded", () =>
|
||||
{
|
||||
var songSelect = (Screens.Select.SongSelect)Game.ScreenStack.CurrentScreen;
|
||||
return songSelect.ChildrenOfType<BeatmapCarousel>().SingleOrDefault()?.IsLoaded == true;
|
||||
});
|
||||
|
||||
AddUntilStep("beatmap in song select", () =>
|
||||
{
|
||||
var songSelect = (Screens.Select.SongSelect)Game.ScreenStack.CurrentScreen;
|
||||
@ -187,7 +193,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
{
|
||||
AddStep("present beatmap", () => Game.PresentBeatmap(getImport()));
|
||||
|
||||
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.IsLoaded);
|
||||
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.BeatmapSetsLoaded);
|
||||
AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapSetInfo.OnlineID, () => Is.EqualTo(getImport().OnlineID));
|
||||
AddAssert("correct ruleset selected", () => Game.Ruleset.Value, () => Is.EqualTo(getImport().Beatmaps.First().Ruleset));
|
||||
}
|
||||
@ -197,7 +203,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
Predicate<BeatmapInfo> pred = b => b.OnlineID == importedID * 1024 + 2;
|
||||
AddStep("present difficulty", () => Game.PresentBeatmap(getImport(), pred));
|
||||
|
||||
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.IsLoaded);
|
||||
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect songSelect && songSelect.BeatmapSetsLoaded);
|
||||
AddUntilStep("correct beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineID, () => Is.EqualTo(importedID * 1024 + 2));
|
||||
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.OnlineID, () => Is.EqualTo(expectedRulesetOnlineID ?? getImport().Beatmaps.First().Ruleset.OnlineID));
|
||||
}
|
||||
|
@ -1035,9 +1035,11 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
[Test]
|
||||
public void TestTouchScreenDetectionInGame()
|
||||
{
|
||||
BeatmapSetInfo beatmapSet = null;
|
||||
|
||||
PushAndConfirm(() => new TestPlaySongSelect());
|
||||
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
|
||||
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
|
||||
AddStep("import beatmap", () => beatmapSet = BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely());
|
||||
AddUntilStep("wait for selected", () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet));
|
||||
AddStep("select", () => InputManager.Key(Key.Enter));
|
||||
|
||||
Player player = null;
|
||||
|
@ -336,13 +336,13 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
});
|
||||
|
||||
AddStep("change to triangles skin", () => Game.Dependencies.Get<SkinManager>().SetSkinFromConfiguration(SkinInfo.TRIANGLES_SKIN.ToString()));
|
||||
AddUntilStep("components loaded", () => Game.ChildrenOfType<SkinComponentsContainer>().All(c => c.ComponentsLoaded));
|
||||
AddUntilStep("components loaded", () => Game.ChildrenOfType<SkinnableContainer>().All(c => c.ComponentsLoaded));
|
||||
// sort of implicitly relies on song select not being skinnable.
|
||||
// TODO: revisit if the above ever changes
|
||||
AddUntilStep("skin changed", () => !skinEditor.ChildrenOfType<SkinBlueprint>().Any());
|
||||
|
||||
AddStep("change back to modified skin", () => Game.Dependencies.Get<SkinManager>().SetSkinFromConfiguration(editedSkinId.ToString()));
|
||||
AddUntilStep("components loaded", () => Game.ChildrenOfType<SkinComponentsContainer>().All(c => c.ComponentsLoaded));
|
||||
AddUntilStep("components loaded", () => Game.ChildrenOfType<SkinnableContainer>().All(c => c.ComponentsLoaded));
|
||||
AddUntilStep("changes saved", () => skinEditor.ChildrenOfType<SkinBlueprint>().Any());
|
||||
}
|
||||
|
||||
|
@ -446,7 +446,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
AddStep("Show overlay with channel 1", () =>
|
||||
{
|
||||
channelManager.JoinChannel(testChannel1);
|
||||
channelManager.CurrentChannel.Value = channelManager.JoinChannel(testChannel1);
|
||||
chatOverlay.Show();
|
||||
});
|
||||
waitForChannel1Visible();
|
||||
@ -462,7 +462,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
AddStep("Show overlay with channel 1", () =>
|
||||
{
|
||||
channelManager.JoinChannel(testChannel1);
|
||||
channelManager.CurrentChannel.Value = channelManager.JoinChannel(testChannel1);
|
||||
chatOverlay.Show();
|
||||
});
|
||||
waitForChannel1Visible();
|
||||
|
@ -17,6 +17,7 @@ using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays.Comments;
|
||||
using osu.Game.Overlays.Comments.Buttons;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
@ -58,6 +59,11 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123));
|
||||
AddUntilStep("show more button hidden",
|
||||
() => commentsContainer.ChildrenOfType<CommentsShowMoreButton>().Single().Alpha == 0);
|
||||
|
||||
if (withPinned)
|
||||
AddAssert("pinned comment replies collapsed", () => commentsContainer.ChildrenOfType<ShowRepliesButton>().First().Expanded.Value, () => Is.False);
|
||||
else
|
||||
AddAssert("first comment replies expanded", () => commentsContainer.ChildrenOfType<ShowRepliesButton>().First().Expanded.Value, () => Is.True);
|
||||
}
|
||||
|
||||
[TestCase(false)]
|
||||
@ -302,7 +308,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
bundle.Comments.Add(new Comment
|
||||
{
|
||||
Id = 20,
|
||||
Message = "Reply to pinned comment",
|
||||
Message = "Reply to pinned comment initially hidden",
|
||||
LegacyName = "AbandonedUser",
|
||||
CreatedAt = DateTimeOffset.Now,
|
||||
VotesCount = 0,
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -11,6 +12,7 @@ using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Profile;
|
||||
using osu.Game.Overlays.Profile.Header.Components;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
@ -60,5 +62,12 @@ namespace osu.Game.Tests.Visual.Online
|
||||
change.Invoke(User.Value!.User.DailyChallengeStatistics);
|
||||
User.Value = new UserProfileData(User.Value.User, User.Value.Ruleset);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlayCountRankingTier()
|
||||
{
|
||||
AddAssert("1 before silver", () => DailyChallengeStatsDisplay.TierForPlayCount(30) == RankingTier.Bronze);
|
||||
AddAssert("first silver", () => DailyChallengeStatsDisplay.TierForPlayCount(31) == RankingTier.Silver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
67
osu.Game.Tests/Visual/Ranking/TestSceneCollectionButton.cs
Normal file
67
osu.Game.Tests/Visual/Ranking/TestSceneCollectionButton.cs
Normal file
@ -0,0 +1,67 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Ranking
|
||||
{
|
||||
public partial class TestSceneCollectionButton : OsuManualInputManagerTestScene
|
||||
{
|
||||
private CollectionButton? collectionButton;
|
||||
private readonly BeatmapInfo beatmapInfo = new BeatmapInfo { OnlineID = 88 };
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("create button", () => Child = new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Child = collectionButton = new CollectionButton(beatmapInfo)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCollectionButton()
|
||||
{
|
||||
AddStep("click collection button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(collectionButton!);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("collection popover is visible", () => this.ChildrenOfType<CollectionPopover>().Single().State.Value == Visibility.Visible);
|
||||
|
||||
AddStep("click outside popover", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(ScreenSpaceDrawQuad.TopLeft);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("collection popover is hidden", () => this.ChildrenOfType<CollectionPopover>().Single().State.Value == Visibility.Hidden);
|
||||
|
||||
AddStep("click collection button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(collectionButton!);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddStep("press escape", () => InputManager.Key(Key.Escape));
|
||||
|
||||
AddAssert("collection popover is hidden", () => this.ChildrenOfType<CollectionPopover>().Single().State.Value == Visibility.Hidden);
|
||||
}
|
||||
}
|
||||
}
|
82
osu.Game.Tests/Visual/Ranking/TestSceneFavouriteButton.cs
Normal file
82
osu.Game.Tests/Visual/Ranking/TestSceneFavouriteButton.cs
Normal file
@ -0,0 +1,82 @@
|
||||
// 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.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Screens.Ranking;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Ranking
|
||||
{
|
||||
public partial class TestSceneFavouriteButton : OsuTestScene
|
||||
{
|
||||
private FavouriteButton? favourite;
|
||||
|
||||
private readonly BeatmapSetInfo beatmapSetInfo = new BeatmapSetInfo { OnlineID = 88 };
|
||||
private readonly BeatmapSetInfo invalidBeatmapSetInfo = new BeatmapSetInfo();
|
||||
|
||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("create button", () => Child = favourite = new FavouriteButton(beatmapSetInfo)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
|
||||
AddStep("register request handling", () => dummyAPI.HandleRequest = request =>
|
||||
{
|
||||
if (!(request is GetBeatmapSetRequest beatmapSetRequest)) return false;
|
||||
|
||||
beatmapSetRequest.TriggerSuccess(new APIBeatmapSet
|
||||
{
|
||||
OnlineID = beatmapSetRequest.ID,
|
||||
HasFavourited = false,
|
||||
FavouriteCount = 0,
|
||||
});
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLoggedOutIn()
|
||||
{
|
||||
AddStep("log out", () => API.Logout());
|
||||
checkEnabled(false);
|
||||
AddStep("log in", () =>
|
||||
{
|
||||
API.Login("test", "test");
|
||||
((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
|
||||
});
|
||||
checkEnabled(true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInvalidBeatmap()
|
||||
{
|
||||
AddStep("make beatmap invalid", () => Child = favourite = new FavouriteButton(invalidBeatmapSetInfo)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
AddStep("log in", () =>
|
||||
{
|
||||
API.Login("test", "test");
|
||||
((DummyAPIAccess)API).AuthenticateSecondFactor("abcdefgh");
|
||||
});
|
||||
checkEnabled(false);
|
||||
}
|
||||
|
||||
private void checkEnabled(bool expected)
|
||||
{
|
||||
AddAssert("is " + (expected ? "enabled" : "disabled"), () => favourite!.Enabled.Value == expected);
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
@ -52,11 +53,11 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
createCarousel(new List<BeatmapSetInfo>());
|
||||
|
||||
AddStep("filter to ruleset 0", () => carousel.Filter(new FilterCriteria
|
||||
AddStep("filter to ruleset 0", () => carousel.FilterImmediately(new FilterCriteria
|
||||
{
|
||||
Ruleset = rulesets.AvailableRulesets.ElementAt(0),
|
||||
AllowConvertedBeatmaps = true,
|
||||
}, false));
|
||||
}));
|
||||
|
||||
AddStep("add mixed ruleset beatmapset", () =>
|
||||
{
|
||||
@ -78,11 +79,11 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
&& visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item)!.BeatmapInfo.Ruleset.OnlineID == 0) == 1;
|
||||
});
|
||||
|
||||
AddStep("filter to ruleset 1", () => carousel.Filter(new FilterCriteria
|
||||
AddStep("filter to ruleset 1", () => carousel.FilterImmediately(new FilterCriteria
|
||||
{
|
||||
Ruleset = rulesets.AvailableRulesets.ElementAt(1),
|
||||
AllowConvertedBeatmaps = true,
|
||||
}, false));
|
||||
}));
|
||||
|
||||
AddUntilStep("wait for filtered difficulties", () =>
|
||||
{
|
||||
@ -93,11 +94,11 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
&& visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item)!.BeatmapInfo.Ruleset.OnlineID == 1) == 1;
|
||||
});
|
||||
|
||||
AddStep("filter to ruleset 2", () => carousel.Filter(new FilterCriteria
|
||||
AddStep("filter to ruleset 2", () => carousel.FilterImmediately(new FilterCriteria
|
||||
{
|
||||
Ruleset = rulesets.AvailableRulesets.ElementAt(2),
|
||||
AllowConvertedBeatmaps = true,
|
||||
}, false));
|
||||
}));
|
||||
|
||||
AddUntilStep("wait for filtered difficulties", () =>
|
||||
{
|
||||
@ -344,7 +345,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
// basic filtering
|
||||
setSelected(1, 1);
|
||||
|
||||
AddStep("Filter", () => carousel.Filter(new FilterCriteria { SearchText = carousel.BeatmapSets.ElementAt(2).Metadata.Title }, false));
|
||||
AddStep("Filter", () => carousel.FilterImmediately(new FilterCriteria { SearchText = carousel.BeatmapSets.ElementAt(2).Metadata.Title }));
|
||||
checkVisibleItemCount(diff: false, count: 1);
|
||||
checkVisibleItemCount(diff: true, count: 3);
|
||||
waitForSelection(3, 1);
|
||||
@ -360,13 +361,13 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
// test filtering some difficulties (and keeping current beatmap set selected).
|
||||
|
||||
setSelected(1, 2);
|
||||
AddStep("Filter some difficulties", () => carousel.Filter(new FilterCriteria { SearchText = "Normal" }, false));
|
||||
AddStep("Filter some difficulties", () => carousel.FilterImmediately(new FilterCriteria { SearchText = "Normal" }));
|
||||
waitForSelection(1, 1);
|
||||
|
||||
AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
|
||||
AddStep("Un-filter", () => carousel.FilterImmediately(new FilterCriteria()));
|
||||
waitForSelection(1, 1);
|
||||
|
||||
AddStep("Filter all", () => carousel.Filter(new FilterCriteria { SearchText = "Dingo" }, false));
|
||||
AddStep("Filter all", () => carousel.FilterImmediately(new FilterCriteria { SearchText = "Dingo" }));
|
||||
|
||||
checkVisibleItemCount(false, 0);
|
||||
checkVisibleItemCount(true, 0);
|
||||
@ -378,7 +379,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
advanceSelection(false);
|
||||
AddAssert("Selection is null", () => currentSelection == null);
|
||||
|
||||
AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
|
||||
AddStep("Un-filter", () => carousel.FilterImmediately(new FilterCriteria()));
|
||||
|
||||
AddAssert("Selection is non-null", () => currentSelection != null);
|
||||
|
||||
@ -399,7 +400,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
setSelected(1, 3);
|
||||
|
||||
AddStep("Apply a range filter", () => carousel.Filter(new FilterCriteria
|
||||
AddStep("Apply a range filter", () => carousel.FilterImmediately(new FilterCriteria
|
||||
{
|
||||
SearchText = searchText,
|
||||
StarDifficulty = new FilterCriteria.OptionalRange<double>
|
||||
@ -408,7 +409,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
Max = 5.5,
|
||||
IsLowerInclusive = true
|
||||
}
|
||||
}, false));
|
||||
}));
|
||||
|
||||
// should reselect the buffered selection.
|
||||
waitForSelection(3, 2);
|
||||
@ -445,13 +446,13 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddAssert("ensure repeat", () => selectedSets.Contains(carousel.SelectedBeatmapSet));
|
||||
|
||||
AddStep("Add set with 100 difficulties", () => carousel.UpdateBeatmapSet(TestResources.CreateTestBeatmapSetInfo(100, rulesets.AvailableRulesets.ToArray())));
|
||||
AddStep("Filter Extra", () => carousel.Filter(new FilterCriteria { SearchText = "Extra 10" }, false));
|
||||
AddStep("Filter Extra", () => carousel.FilterImmediately(new FilterCriteria { SearchText = "Extra 10" }));
|
||||
checkInvisibleDifficultiesUnselectable();
|
||||
checkInvisibleDifficultiesUnselectable();
|
||||
checkInvisibleDifficultiesUnselectable();
|
||||
checkInvisibleDifficultiesUnselectable();
|
||||
checkInvisibleDifficultiesUnselectable();
|
||||
AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
|
||||
AddStep("Un-filter", () => carousel.FilterImmediately(new FilterCriteria()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -527,7 +528,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
loadBeatmaps(setCount: local_set_count, diffCount: local_diff_count);
|
||||
|
||||
AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false));
|
||||
AddStep("Sort by difficulty", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Difficulty }));
|
||||
|
||||
checkVisibleItemCount(false, local_set_count * local_diff_count);
|
||||
|
||||
@ -566,7 +567,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
loadBeatmaps(sets, () => new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) });
|
||||
|
||||
AddStep("Set non-empty mode filter", () =>
|
||||
carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1) }, false));
|
||||
carousel.FilterImmediately(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1) }));
|
||||
|
||||
AddAssert("Something is selected", () => carousel.SelectedBeatmapInfo != null);
|
||||
}
|
||||
@ -601,7 +602,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
loadBeatmaps(sets);
|
||||
|
||||
AddStep("Sort by date submitted", () => carousel.Filter(new FilterCriteria { Sort = SortMode.DateSubmitted }, false));
|
||||
AddStep("Sort by date submitted", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.DateSubmitted }));
|
||||
checkVisibleItemCount(diff: false, count: 10);
|
||||
checkVisibleItemCount(diff: true, count: 5);
|
||||
|
||||
@ -610,11 +611,11 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddAssert("rest are at start", () => carousel.Items.OfType<DrawableCarouselBeatmapSet>().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted != null).Count(),
|
||||
() => Is.EqualTo(6));
|
||||
|
||||
AddStep("Sort by date submitted and string", () => carousel.Filter(new FilterCriteria
|
||||
AddStep("Sort by date submitted and string", () => carousel.FilterImmediately(new FilterCriteria
|
||||
{
|
||||
Sort = SortMode.DateSubmitted,
|
||||
SearchText = zzz_string
|
||||
}, false));
|
||||
}));
|
||||
checkVisibleItemCount(diff: false, count: 5);
|
||||
checkVisibleItemCount(diff: true, count: 5);
|
||||
|
||||
@ -658,10 +659,10 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
loadBeatmaps(sets);
|
||||
|
||||
AddStep("Sort by author", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Author }, false));
|
||||
AddStep("Sort by author", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Author }));
|
||||
AddAssert($"Check {zzz_uppercase} is last", () => carousel.BeatmapSets.Last().Metadata.Author.Username == zzz_uppercase);
|
||||
AddAssert($"Check {zzz_lowercase} is second last", () => carousel.BeatmapSets.SkipLast(1).Last().Metadata.Author.Username == zzz_lowercase);
|
||||
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
|
||||
AddStep("Sort by artist", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Artist }));
|
||||
AddAssert($"Check {zzz_uppercase} is last", () => carousel.BeatmapSets.Last().Metadata.Artist == zzz_uppercase);
|
||||
AddAssert($"Check {zzz_lowercase} is second last", () => carousel.BeatmapSets.SkipLast(1).Last().Metadata.Artist == zzz_lowercase);
|
||||
}
|
||||
@ -703,7 +704,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
loadBeatmaps(sets);
|
||||
|
||||
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
|
||||
AddStep("Sort by artist", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Artist }));
|
||||
AddAssert("Check last item", () =>
|
||||
{
|
||||
var lastItem = carousel.BeatmapSets.Last();
|
||||
@ -746,10 +747,10 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
loadBeatmaps(sets);
|
||||
|
||||
AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
|
||||
AddStep("Sort by title", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Title }));
|
||||
AddAssert("Items remain in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending);
|
||||
|
||||
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
|
||||
AddStep("Sort by artist", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Artist }));
|
||||
AddAssert("Items remain in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending);
|
||||
}
|
||||
|
||||
@ -786,7 +787,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
loadBeatmaps(sets);
|
||||
|
||||
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
|
||||
AddStep("Sort by artist", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Artist }));
|
||||
|
||||
AddAssert("Items in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending);
|
||||
AddStep("Save order", () => originalOrder = carousel.BeatmapSets.Select(s => s.ID).ToArray());
|
||||
@ -796,7 +797,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder));
|
||||
|
||||
AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
|
||||
AddStep("Sort by title", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Title }));
|
||||
AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder));
|
||||
}
|
||||
|
||||
@ -833,7 +834,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
loadBeatmaps(sets);
|
||||
|
||||
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
|
||||
AddStep("Sort by artist", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Artist }));
|
||||
|
||||
AddAssert("Items in descending added order", () => carousel.BeatmapSets.Select(s => s.DateAdded), () => Is.Ordered.Descending);
|
||||
AddStep("Save order", () => originalOrder = carousel.BeatmapSets.Select(s => s.ID).ToArray());
|
||||
@ -858,7 +859,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder));
|
||||
|
||||
AddStep("Sort by title", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false));
|
||||
AddStep("Sort by title", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Title }));
|
||||
AddAssert("Order didn't change", () => carousel.BeatmapSets.Select(s => s.ID), () => Is.EqualTo(originalOrder));
|
||||
}
|
||||
|
||||
@ -885,12 +886,12 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
loadBeatmaps(sets);
|
||||
|
||||
AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false));
|
||||
AddStep("Sort by difficulty", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Difficulty }));
|
||||
|
||||
checkVisibleItemCount(false, local_set_count * local_diff_count);
|
||||
checkVisibleItemCount(true, 1);
|
||||
|
||||
AddStep("Filter to normal", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Normal" }, false));
|
||||
AddStep("Filter to normal", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Normal" }));
|
||||
checkVisibleItemCount(false, local_set_count);
|
||||
checkVisibleItemCount(true, 1);
|
||||
|
||||
@ -901,7 +902,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
.Count(p => ((CarouselBeatmapSet)p.Item)!.Beatmaps.Single().BeatmapInfo.DifficultyName.StartsWith("Normal", StringComparison.Ordinal)) == local_set_count;
|
||||
});
|
||||
|
||||
AddStep("Filter to insane", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Insane" }, false));
|
||||
AddStep("Filter to insane", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Insane" }));
|
||||
checkVisibleItemCount(false, local_set_count);
|
||||
checkVisibleItemCount(true, 1);
|
||||
|
||||
@ -1022,7 +1023,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
carousel.UpdateBeatmapSet(testMixed);
|
||||
});
|
||||
AddStep("filter to ruleset 0", () =>
|
||||
carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) }, false));
|
||||
carousel.FilterImmediately(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) }));
|
||||
AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testMixed.Beatmaps[1], false));
|
||||
AddAssert("unfiltered beatmap not selected", () => carousel.SelectedBeatmapInfo?.Ruleset.OnlineID == 0);
|
||||
|
||||
@ -1068,12 +1069,12 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
AddStep("Toggle non-matching filter", () =>
|
||||
{
|
||||
carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false);
|
||||
carousel.FilterImmediately(new FilterCriteria { SearchText = Guid.NewGuid().ToString() });
|
||||
});
|
||||
|
||||
AddStep("Restore no filter", () =>
|
||||
{
|
||||
carousel.Filter(new FilterCriteria(), false);
|
||||
carousel.FilterImmediately(new FilterCriteria());
|
||||
eagerSelectedIDs.Add(carousel.SelectedBeatmapSet!.ID);
|
||||
});
|
||||
}
|
||||
@ -1097,7 +1098,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
loadBeatmaps(manySets);
|
||||
|
||||
AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false));
|
||||
AddStep("Sort by difficulty", () => carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Difficulty }));
|
||||
|
||||
advanceSelection(direction: 1, diff: false);
|
||||
|
||||
@ -1105,12 +1106,12 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
AddStep("Toggle non-matching filter", () =>
|
||||
{
|
||||
carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false);
|
||||
carousel.FilterImmediately(new FilterCriteria { SearchText = Guid.NewGuid().ToString() });
|
||||
});
|
||||
|
||||
AddStep("Restore no filter", () =>
|
||||
{
|
||||
carousel.Filter(new FilterCriteria(), false);
|
||||
carousel.FilterImmediately(new FilterCriteria());
|
||||
eagerSelectedIDs.Add(carousel.SelectedBeatmapSet!.ID);
|
||||
});
|
||||
}
|
||||
@ -1185,7 +1186,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
AddStep($"Set ruleset to {rulesetInfo.ShortName}", () =>
|
||||
{
|
||||
carousel.Filter(new FilterCriteria { Ruleset = rulesetInfo, Sort = SortMode.Title }, false);
|
||||
carousel.FilterImmediately(new FilterCriteria { Ruleset = rulesetInfo, Sort = SortMode.Title });
|
||||
});
|
||||
waitForSelection(i + 1, 1);
|
||||
}
|
||||
@ -1223,12 +1224,12 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
setSelected(i, 1);
|
||||
AddStep("Set ruleset to taiko", () =>
|
||||
{
|
||||
carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1), Sort = SortMode.Title }, false);
|
||||
carousel.FilterImmediately(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1), Sort = SortMode.Title });
|
||||
});
|
||||
waitForSelection(i - 1, 1);
|
||||
AddStep("Remove ruleset filter", () =>
|
||||
{
|
||||
carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false);
|
||||
carousel.FilterImmediately(new FilterCriteria { Sort = SortMode.Title });
|
||||
});
|
||||
}
|
||||
|
||||
@ -1268,26 +1269,23 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
}
|
||||
}
|
||||
|
||||
createCarousel(beatmapSets, c =>
|
||||
createCarousel(beatmapSets, initialCriteria, c =>
|
||||
{
|
||||
carouselAdjust?.Invoke(c);
|
||||
|
||||
carousel.Filter(initialCriteria?.Invoke() ?? new FilterCriteria());
|
||||
carousel.BeatmapSetsChanged = () => changed = true;
|
||||
carousel.BeatmapSets = beatmapSets;
|
||||
carouselAdjust?.Invoke(c);
|
||||
});
|
||||
|
||||
AddUntilStep("Wait for load", () => changed);
|
||||
}
|
||||
|
||||
private void createCarousel(List<BeatmapSetInfo> beatmapSets, Action<BeatmapCarousel> carouselAdjust = null, Container target = null)
|
||||
private void createCarousel(List<BeatmapSetInfo> beatmapSets, [CanBeNull] Func<FilterCriteria> initialCriteria = null, Action<BeatmapCarousel> carouselAdjust = null, Container target = null)
|
||||
{
|
||||
AddStep("Create carousel", () =>
|
||||
{
|
||||
selectedSets.Clear();
|
||||
eagerSelectedIDs.Clear();
|
||||
|
||||
carousel = new TestBeatmapCarousel
|
||||
carousel = new TestBeatmapCarousel(initialCriteria?.Invoke() ?? new FilterCriteria())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
@ -1389,6 +1387,11 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
private partial class TestBeatmapCarousel : BeatmapCarousel
|
||||
{
|
||||
public TestBeatmapCarousel(FilterCriteria criteria)
|
||||
: base(criteria)
|
||||
{
|
||||
}
|
||||
|
||||
public bool PendingFilterTask => PendingFilter != null;
|
||||
|
||||
public IEnumerable<DrawableCarouselItem> Items
|
||||
@ -1410,6 +1413,12 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void FilterImmediately(FilterCriteria newCriteria)
|
||||
{
|
||||
Filter(newCriteria);
|
||||
FlushPendingFilterOperations();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,16 +56,20 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
{
|
||||
DetachedBeatmapStore detachedBeatmapStore;
|
||||
|
||||
// These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install.
|
||||
// At a point we have isolated interactive test runs enough, this can likely be removed.
|
||||
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
|
||||
Dependencies.Cache(Realm);
|
||||
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, defaultBeatmap = Beatmap.Default));
|
||||
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore());
|
||||
|
||||
Dependencies.Cache(music = new MusicController());
|
||||
|
||||
// required to get bindables attached
|
||||
Add(music);
|
||||
Add(detachedBeatmapStore);
|
||||
|
||||
Dependencies.Cache(config = new OsuConfigManager(LocalStorage));
|
||||
}
|
||||
@ -242,7 +246,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
createSongSelect();
|
||||
|
||||
AddAssert("filter count is 1", () => songSelect?.FilterCount == 1);
|
||||
AddAssert("filter count is 0", () => songSelect?.FilterCount, () => Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -362,7 +366,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
AddStep("return", () => songSelect!.MakeCurrent());
|
||||
AddUntilStep("wait for current", () => songSelect!.IsCurrentScreen());
|
||||
AddAssert("filter count is 1", () => songSelect!.FilterCount == 1);
|
||||
AddAssert("filter count is 0", () => songSelect!.FilterCount, () => Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -382,7 +386,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
AddStep("return", () => songSelect!.MakeCurrent());
|
||||
AddUntilStep("wait for current", () => songSelect!.IsCurrentScreen());
|
||||
AddAssert("filter count is 2", () => songSelect!.FilterCount == 2);
|
||||
AddAssert("filter count is 1", () => songSelect!.FilterCount, () => Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -1270,11 +1274,11 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
// Mod that is guaranteed to never re-filter.
|
||||
AddStep("add non-filterable mod", () => SelectedMods.Value = new Mod[] { new OsuModCinema() });
|
||||
AddAssert("filter count is 1", () => songSelect!.FilterCount, () => Is.EqualTo(1));
|
||||
AddAssert("filter count is 0", () => songSelect!.FilterCount, () => Is.EqualTo(0));
|
||||
|
||||
// Removing the mod should still not re-filter.
|
||||
AddStep("remove non-filterable mod", () => SelectedMods.Value = Array.Empty<Mod>());
|
||||
AddAssert("filter count is 1", () => songSelect!.FilterCount, () => Is.EqualTo(1));
|
||||
AddAssert("filter count is 0", () => songSelect!.FilterCount, () => Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -1286,35 +1290,35 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
// Change to mania ruleset.
|
||||
AddStep("filter to mania ruleset", () => Ruleset.Value = rulesets.AvailableRulesets.First(r => r.OnlineID == 3));
|
||||
AddAssert("filter count is 2", () => songSelect!.FilterCount, () => Is.EqualTo(2));
|
||||
AddAssert("filter count is 2", () => songSelect!.FilterCount, () => Is.EqualTo(1));
|
||||
|
||||
// Apply a mod, but this should NOT re-filter because there's no search text.
|
||||
AddStep("add filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey3() });
|
||||
AddAssert("filter count is 2", () => songSelect!.FilterCount, () => Is.EqualTo(2));
|
||||
AddAssert("filter count is 1", () => songSelect!.FilterCount, () => Is.EqualTo(1));
|
||||
|
||||
// Set search text. Should re-filter.
|
||||
AddStep("set search text to match mods", () => songSelect!.FilterControl.CurrentTextSearch.Value = "keys=3");
|
||||
AddAssert("filter count is 3", () => songSelect!.FilterCount, () => Is.EqualTo(3));
|
||||
AddAssert("filter count is 2", () => songSelect!.FilterCount, () => Is.EqualTo(2));
|
||||
|
||||
// Change filterable mod. Should re-filter.
|
||||
AddStep("change new filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey5() });
|
||||
AddAssert("filter count is 4", () => songSelect!.FilterCount, () => Is.EqualTo(4));
|
||||
AddAssert("filter count is 3", () => songSelect!.FilterCount, () => Is.EqualTo(3));
|
||||
|
||||
// Add non-filterable mod. Should NOT re-filter.
|
||||
AddStep("apply non-filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModNoFail(), new ManiaModKey5() });
|
||||
AddAssert("filter count is 4", () => songSelect!.FilterCount, () => Is.EqualTo(4));
|
||||
AddAssert("filter count is 3", () => songSelect!.FilterCount, () => Is.EqualTo(3));
|
||||
|
||||
// Remove filterable mod. Should re-filter.
|
||||
AddStep("remove filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModNoFail() });
|
||||
AddAssert("filter count is 5", () => songSelect!.FilterCount, () => Is.EqualTo(5));
|
||||
AddAssert("filter count is 4", () => songSelect!.FilterCount, () => Is.EqualTo(4));
|
||||
|
||||
// Remove non-filterable mod. Should NOT re-filter.
|
||||
AddStep("remove filterable mod", () => SelectedMods.Value = Array.Empty<Mod>());
|
||||
AddAssert("filter count is 5", () => songSelect!.FilterCount, () => Is.EqualTo(5));
|
||||
AddAssert("filter count is 4", () => songSelect!.FilterCount, () => Is.EqualTo(4));
|
||||
|
||||
// Add filterable mod. Should re-filter.
|
||||
AddStep("add filterable mod", () => SelectedMods.Value = new Mod[] { new ManiaModKey3() });
|
||||
AddAssert("filter count is 6", () => songSelect!.FilterCount, () => Is.EqualTo(6));
|
||||
AddAssert("filter count is 5", () => songSelect!.FilterCount, () => Is.EqualTo(5));
|
||||
}
|
||||
|
||||
private void waitForInitialSelection()
|
||||
@ -1397,8 +1401,6 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
public Action? StartRequested;
|
||||
|
||||
public new Bindable<RulesetInfo> Ruleset => base.Ruleset;
|
||||
|
||||
public new FilterControl FilterControl => base.FilterControl;
|
||||
|
||||
public WorkingBeatmap CurrentBeatmap => Beatmap.Value;
|
||||
@ -1408,18 +1410,18 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
public new void PresentScore(ScoreInfo score) => base.PresentScore(score);
|
||||
|
||||
public int FilterCount;
|
||||
|
||||
protected override bool OnStart()
|
||||
{
|
||||
StartRequested?.Invoke();
|
||||
return base.OnStart();
|
||||
}
|
||||
|
||||
public int FilterCount;
|
||||
|
||||
protected override void ApplyFilterToCarousel(FilterCriteria criteria)
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
FilterCount++;
|
||||
base.ApplyFilterToCarousel(criteria);
|
||||
FilterControl.FilterChanged += _ => FilterCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -246,7 +246,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
private BeatmapCarousel createCarousel()
|
||||
{
|
||||
return carousel = new BeatmapCarousel
|
||||
return carousel = new BeatmapCarousel(new FilterCriteria())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
BeatmapSets = new List<BeatmapSetInfo>
|
||||
|
@ -103,15 +103,79 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
foreach (var notification in notificationOverlay.AllNotifications)
|
||||
notification.Close(runFlingAnimation: false);
|
||||
});
|
||||
|
||||
AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null));
|
||||
AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero);
|
||||
|
||||
AddStep("hide button's parent", () => buttonContainer.Hide());
|
||||
|
||||
AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo
|
||||
{
|
||||
RoomID = 1234,
|
||||
}));
|
||||
AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDailyChallengeButtonOldChallenge()
|
||||
{
|
||||
AddStep("set up API", () => dummyAPI.HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case GetRoomRequest getRoomRequest:
|
||||
if (getRoomRequest.RoomId != 1234)
|
||||
return false;
|
||||
|
||||
var beatmap = CreateAPIBeatmap();
|
||||
beatmap.OnlineID = 1001;
|
||||
getRoomRequest.TriggerSuccess(new Room
|
||||
{
|
||||
RoomID = { Value = 1234 },
|
||||
Playlist =
|
||||
{
|
||||
new PlaylistItem(beatmap)
|
||||
},
|
||||
StartDate = { Value = DateTimeOffset.Now.AddMinutes(-50) },
|
||||
EndDate = { Value = DateTimeOffset.Now.AddSeconds(30) }
|
||||
});
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
NotificationOverlay notificationOverlay = null!;
|
||||
|
||||
AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null));
|
||||
AddStep("add content", () =>
|
||||
{
|
||||
notificationOverlay = new NotificationOverlay();
|
||||
Children = new Drawable[]
|
||||
{
|
||||
notificationOverlay,
|
||||
new DependencyProvidingContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
CachedDependencies = [(typeof(INotificationOverlay), notificationOverlay)],
|
||||
Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
ButtonSystemState = ButtonSystemState.TopLevel,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo
|
||||
{
|
||||
RoomID = 1234
|
||||
}));
|
||||
AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,8 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@ -157,6 +159,27 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
checkExpanded(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDraggingKeepsPanelExpanded()
|
||||
{
|
||||
AddStep("add customisable mod", () =>
|
||||
{
|
||||
SelectedMods.Value = new[] { new OsuModDoubleTime() };
|
||||
panel.Enabled.Value = true;
|
||||
});
|
||||
|
||||
AddStep("hover header", () => InputManager.MoveMouseTo(header));
|
||||
checkExpanded(true);
|
||||
|
||||
AddStep("hover slider bar nub", () => InputManager.MoveMouseTo(panel.ChildrenOfType<OsuSliderBar<double>>().First().ChildrenOfType<Nub>().Single()));
|
||||
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
|
||||
AddStep("drag outside", () => InputManager.MoveMouseTo(Vector2.Zero));
|
||||
checkExpanded(true);
|
||||
|
||||
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
checkExpanded(false);
|
||||
}
|
||||
|
||||
private void checkExpanded(bool expanded)
|
||||
{
|
||||
AddUntilStep(expanded ? "is expanded" : "not expanded", () => panel.ExpandedState.Value,
|
||||
|
@ -2,6 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.Select;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
@ -48,5 +50,16 @@ namespace osu.Game.Beatmaps
|
||||
}
|
||||
|
||||
private static string getVersionString(IBeatmapInfo beatmapInfo) => string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? string.Empty : $"[{beatmapInfo.DifficultyName}]";
|
||||
|
||||
/// <summary>
|
||||
/// Get the beatmap info page URL, or <c>null</c> if unavailable.
|
||||
/// </summary>
|
||||
public static string? GetOnlineURL(this IBeatmapInfo beatmapInfo, IAPIProvider api, IRulesetInfo? ruleset = null)
|
||||
{
|
||||
if (beatmapInfo.OnlineID <= 0 || beatmapInfo.BeatmapSet == null)
|
||||
return null;
|
||||
|
||||
return $@"{api.WebsiteRootUrl}/beatmapsets/{beatmapInfo.BeatmapSet.OnlineID}#{ruleset?.ShortName ?? beatmapInfo.Ruleset.ShortName}/{beatmapInfo.OnlineID}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ using System.Linq;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Rulesets;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
@ -29,5 +31,19 @@ namespace osu.Game.Beatmaps
|
||||
/// <param name="filename">The name of the file to get the storage path of.</param>
|
||||
public static RealmNamedFileUsage? GetFile(this IHasRealmFiles model, string filename) =>
|
||||
model.Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
/// <summary>
|
||||
/// Get the beatmapset info page URL, or <c>null</c> if unavailable.
|
||||
/// </summary>
|
||||
public static string? GetOnlineURL(this IBeatmapSetInfo beatmapSetInfo, IAPIProvider api, IRulesetInfo? ruleset = null)
|
||||
{
|
||||
if (beatmapSetInfo.OnlineID <= 0)
|
||||
return null;
|
||||
|
||||
if (ruleset != null)
|
||||
return $@"{api.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}#{ruleset.ShortName}";
|
||||
|
||||
return $@"{api.WebsiteRootUrl}/beatmapsets/{beatmapSetInfo.OnlineID}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -74,6 +74,19 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
[NotNull]
|
||||
public TimingControlPoint TimingPointAt(double time) => BinarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : TimingControlPoint.DEFAULT);
|
||||
|
||||
/// <summary>
|
||||
/// Finds the first timing point that is active strictly after <paramref name="time"/>, or null if no such point exists.
|
||||
/// </summary>
|
||||
/// <param name="time">The time after which to find the timing control point.</param>
|
||||
/// <returns>The timing control point.</returns>
|
||||
[CanBeNull]
|
||||
public TimingControlPoint TimingPointAfter(double time)
|
||||
{
|
||||
int index = BinarySearch(TimingPoints, time, EqualitySelection.Rightmost);
|
||||
index = index < 0 ? ~index : index + 1;
|
||||
return index < TimingPoints.Count ? TimingPoints[index] : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the maximum BPM represented by any timing control point.
|
||||
/// </summary>
|
||||
@ -156,7 +169,14 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
public double GetClosestSnappedTime(double time, int beatDivisor, double? referenceTime = null)
|
||||
{
|
||||
var timingPoint = TimingPointAt(referenceTime ?? time);
|
||||
return getClosestSnappedTime(timingPoint, time, beatDivisor);
|
||||
double snappedTime = getClosestSnappedTime(timingPoint, time, beatDivisor);
|
||||
|
||||
if (referenceTime.HasValue)
|
||||
return snappedTime;
|
||||
|
||||
// If there is a timing point right after the given time, we should check if it is closer than the snapped time and snap to it.
|
||||
var timingPointAfter = TimingPointAfter(time);
|
||||
return timingPointAfter is null || Math.Abs(time - snappedTime) < Math.Abs(time - timingPointAfter.Time) ? snappedTime : timingPointAfter.Time;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -230,17 +250,40 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(list);
|
||||
|
||||
if (list.Count == 0)
|
||||
return null;
|
||||
int index = BinarySearch(list, time, EqualitySelection.Rightmost);
|
||||
|
||||
if (index < 0)
|
||||
index = ~index - 1;
|
||||
|
||||
return index >= 0 ? list[index] : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binary searches one of the control point lists to find the active control point at <paramref name="time"/>.
|
||||
/// </summary>
|
||||
/// <param name="list">The list to search.</param>
|
||||
/// <param name="time">The time to find the control point at.</param>
|
||||
/// <param name="equalitySelection">Determines which index to return if there are multiple exact matches.</param>
|
||||
/// <returns>The index of the control point at <paramref name="time"/>. Will return the complement of the index of the control point after <paramref name="time"/> if no exact match is found.</returns>
|
||||
public static int BinarySearch<T>(IReadOnlyList<T> list, double time, EqualitySelection equalitySelection)
|
||||
where T : class, IControlPoint
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(list);
|
||||
|
||||
int n = list.Count;
|
||||
|
||||
if (n == 0)
|
||||
return -1;
|
||||
|
||||
if (time < list[0].Time)
|
||||
return null;
|
||||
return -1;
|
||||
|
||||
if (time >= list[^1].Time)
|
||||
return list[^1];
|
||||
if (time > list[^1].Time)
|
||||
return ~n;
|
||||
|
||||
int l = 0;
|
||||
int r = list.Count - 2;
|
||||
int r = n - 1;
|
||||
bool equalityFound = false;
|
||||
|
||||
while (l <= r)
|
||||
{
|
||||
@ -251,11 +294,37 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
else if (list[pivot].Time > time)
|
||||
r = pivot - 1;
|
||||
else
|
||||
return list[pivot];
|
||||
{
|
||||
equalityFound = true;
|
||||
|
||||
switch (equalitySelection)
|
||||
{
|
||||
case EqualitySelection.Leftmost:
|
||||
r = pivot - 1;
|
||||
break;
|
||||
|
||||
case EqualitySelection.Rightmost:
|
||||
l = pivot + 1;
|
||||
break;
|
||||
|
||||
default:
|
||||
case EqualitySelection.FirstFound:
|
||||
return pivot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// l will be the first control point with Time > time, but we want the one before it
|
||||
return list[l - 1];
|
||||
if (!equalityFound) return ~l;
|
||||
|
||||
switch (equalitySelection)
|
||||
{
|
||||
case EqualitySelection.Leftmost:
|
||||
return l;
|
||||
|
||||
default:
|
||||
case EqualitySelection.Rightmost:
|
||||
return l - 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -328,4 +397,11 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
return controlPointInfo;
|
||||
}
|
||||
}
|
||||
|
||||
public enum EqualitySelection
|
||||
{
|
||||
FirstFound,
|
||||
Leftmost,
|
||||
Rightmost
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Screens.Edit;
|
||||
|
||||
namespace osu.Game.Beatmaps.Formats
|
||||
{
|
||||
@ -336,7 +337,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
break;
|
||||
|
||||
case @"BeatDivisor":
|
||||
beatmap.BeatmapInfo.BeatDivisor = Parsing.ParseInt(pair.Value);
|
||||
beatmap.BeatmapInfo.BeatDivisor = Math.Clamp(Parsing.ParseInt(pair.Value), BindableBeatDivisor.MINIMUM_DIVISOR, BindableBeatDivisor.MAXIMUM_DIVISOR);
|
||||
break;
|
||||
|
||||
case @"GridSize":
|
||||
|
@ -80,6 +80,8 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public bool TryLookup(BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata)
|
||||
{
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
|
||||
if (!Available)
|
||||
{
|
||||
onlineMetadata = null;
|
||||
@ -94,43 +96,21 @@ namespace osu.Game.Beatmaps
|
||||
return false;
|
||||
}
|
||||
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
|
||||
try
|
||||
{
|
||||
using (var db = new SqliteConnection(string.Concat(@"Data Source=", storage.GetFullPath(@"online.db", true))))
|
||||
{
|
||||
db.Open();
|
||||
|
||||
using (var cmd = db.CreateCommand())
|
||||
switch (getCacheVersion(db))
|
||||
{
|
||||
cmd.CommandText =
|
||||
@"SELECT beatmapset_id, beatmap_id, approved, user_id, checksum, last_update FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineID OR filename = @Path";
|
||||
case 1:
|
||||
// will eventually become irrelevant due to the monthly recycling of local caches
|
||||
// can be removed 20250221
|
||||
return queryCacheVersion1(db, beatmapInfo, out onlineMetadata);
|
||||
|
||||
cmd.Parameters.Add(new SqliteParameter(@"@MD5Hash", beatmapInfo.MD5Hash));
|
||||
cmd.Parameters.Add(new SqliteParameter(@"@OnlineID", beatmapInfo.OnlineID));
|
||||
cmd.Parameters.Add(new SqliteParameter(@"@Path", beatmapInfo.Path));
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
if (reader.Read())
|
||||
{
|
||||
logForModel(beatmapInfo.BeatmapSet, $@"Cached local retrieval for {beatmapInfo}.");
|
||||
|
||||
onlineMetadata = new OnlineBeatmapMetadata
|
||||
{
|
||||
BeatmapSetID = reader.GetInt32(0),
|
||||
BeatmapID = reader.GetInt32(1),
|
||||
BeatmapStatus = (BeatmapOnlineStatus)reader.GetByte(2),
|
||||
BeatmapSetStatus = (BeatmapOnlineStatus)reader.GetByte(2),
|
||||
AuthorID = reader.GetInt32(3),
|
||||
MD5Hash = reader.GetString(4),
|
||||
LastUpdated = reader.GetDateTimeOffset(5),
|
||||
// TODO: DateSubmitted and DateRanked are not provided by local cache.
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
case 2:
|
||||
return queryCacheVersion2(db, beatmapInfo, out onlineMetadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -211,6 +191,115 @@ namespace osu.Game.Beatmaps
|
||||
});
|
||||
}
|
||||
|
||||
private int getCacheVersion(SqliteConnection connection)
|
||||
{
|
||||
using (var cmd = connection.CreateCommand())
|
||||
{
|
||||
cmd.CommandText = @"SELECT COUNT(1) FROM `sqlite_master` WHERE `type` = 'table' AND `name` = 'schema_version'";
|
||||
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
if (!reader.Read())
|
||||
throw new InvalidOperationException("Error when attempting to check for existence of `schema_version` table.");
|
||||
|
||||
// No versioning table means that this is the very first version of the schema.
|
||||
if (reader.GetInt32(0) == 0)
|
||||
return 1;
|
||||
}
|
||||
|
||||
using (var cmd = connection.CreateCommand())
|
||||
{
|
||||
cmd.CommandText = @"SELECT `number` FROM `schema_version`";
|
||||
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
if (!reader.Read())
|
||||
throw new InvalidOperationException("Error when attempting to query schema version.");
|
||||
|
||||
return reader.GetInt32(0);
|
||||
}
|
||||
}
|
||||
|
||||
private bool queryCacheVersion1(SqliteConnection db, BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata)
|
||||
{
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
|
||||
using var cmd = db.CreateCommand();
|
||||
|
||||
cmd.CommandText =
|
||||
@"SELECT beatmapset_id, beatmap_id, approved, user_id, checksum, last_update FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineID OR filename = @Path";
|
||||
|
||||
cmd.Parameters.Add(new SqliteParameter(@"@MD5Hash", beatmapInfo.MD5Hash));
|
||||
cmd.Parameters.Add(new SqliteParameter(@"@OnlineID", beatmapInfo.OnlineID));
|
||||
cmd.Parameters.Add(new SqliteParameter(@"@Path", beatmapInfo.Path));
|
||||
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
if (reader.Read())
|
||||
{
|
||||
logForModel(beatmapInfo.BeatmapSet, $@"Cached local retrieval for {beatmapInfo} (cache version 1).");
|
||||
|
||||
onlineMetadata = new OnlineBeatmapMetadata
|
||||
{
|
||||
BeatmapSetID = reader.GetInt32(0),
|
||||
BeatmapID = reader.GetInt32(1),
|
||||
BeatmapStatus = (BeatmapOnlineStatus)reader.GetByte(2),
|
||||
BeatmapSetStatus = (BeatmapOnlineStatus)reader.GetByte(2),
|
||||
AuthorID = reader.GetInt32(3),
|
||||
MD5Hash = reader.GetString(4),
|
||||
LastUpdated = reader.GetDateTimeOffset(5),
|
||||
// TODO: DateSubmitted and DateRanked are not provided by local cache in this version.
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
onlineMetadata = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool queryCacheVersion2(SqliteConnection db, BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata)
|
||||
{
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
|
||||
using var cmd = db.CreateCommand();
|
||||
|
||||
cmd.CommandText =
|
||||
"""
|
||||
SELECT `b`.`beatmapset_id`, `b`.`beatmap_id`, `b`.`approved`, `b`.`user_id`, `b`.`checksum`, `b`.`last_update`, `s`.`submit_date`, `s`.`approved_date`
|
||||
FROM `osu_beatmaps` AS `b`
|
||||
JOIN `osu_beatmapsets` AS `s` ON `s`.`beatmapset_id` = `b`.`beatmapset_id`
|
||||
WHERE `b`.`checksum` = @MD5Hash OR `b`.`beatmap_id` = @OnlineID OR `b`.`filename` = @Path
|
||||
""";
|
||||
|
||||
cmd.Parameters.Add(new SqliteParameter(@"@MD5Hash", beatmapInfo.MD5Hash));
|
||||
cmd.Parameters.Add(new SqliteParameter(@"@OnlineID", beatmapInfo.OnlineID));
|
||||
cmd.Parameters.Add(new SqliteParameter(@"@Path", beatmapInfo.Path));
|
||||
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
if (reader.Read())
|
||||
{
|
||||
logForModel(beatmapInfo.BeatmapSet, $@"Cached local retrieval for {beatmapInfo} (cache version 2).");
|
||||
|
||||
onlineMetadata = new OnlineBeatmapMetadata
|
||||
{
|
||||
BeatmapSetID = reader.GetInt32(0),
|
||||
BeatmapID = reader.GetInt32(1),
|
||||
BeatmapStatus = (BeatmapOnlineStatus)reader.GetByte(2),
|
||||
BeatmapSetStatus = (BeatmapOnlineStatus)reader.GetByte(2),
|
||||
AuthorID = reader.GetInt32(3),
|
||||
MD5Hash = reader.GetString(4),
|
||||
LastUpdated = reader.GetDateTimeOffset(5),
|
||||
DateSubmitted = reader.GetDateTimeOffset(6),
|
||||
DateRanked = reader.GetDateTimeOffset(7),
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
onlineMetadata = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void log(string message)
|
||||
=> Logger.Log($@"[{nameof(LocalCachedBeatmapMetadataSource)}] {message}", LoggingTarget.Database);
|
||||
|
||||
|
@ -205,6 +205,8 @@ namespace osu.Game.Configuration
|
||||
|
||||
SetDefault(OsuSetting.EditorTimelineShowTimingChanges, true);
|
||||
SetDefault(OsuSetting.EditorTimelineShowTicks, true);
|
||||
|
||||
SetDefault(OsuSetting.AlwaysShowHoldForMenuButton, false);
|
||||
}
|
||||
|
||||
protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup)
|
||||
@ -429,5 +431,6 @@ namespace osu.Game.Configuration
|
||||
HideCountryFlags,
|
||||
EditorTimelineShowTimingChanges,
|
||||
EditorTimelineShowTicks,
|
||||
AlwaysShowHoldForMenuButton
|
||||
}
|
||||
}
|
||||
|
@ -80,5 +80,11 @@ namespace osu.Game.Configuration
|
||||
/// Stores the local user's last score (can be completed or aborted).
|
||||
/// </summary>
|
||||
LastLocalUserScore,
|
||||
|
||||
/// <summary>
|
||||
/// Whether the intro animation for the daily challenge screen has been played once.
|
||||
/// This is reset when a new challenge is up.
|
||||
/// </summary>
|
||||
DailyChallengeIntroPlayed,
|
||||
}
|
||||
}
|
||||
|
163
osu.Game/Database/DetachedBeatmapStore.cs
Normal file
163
osu.Game/Database/DetachedBeatmapStore.cs
Normal file
@ -0,0 +1,163 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public partial class DetachedBeatmapStore : Component
|
||||
{
|
||||
private readonly ManualResetEventSlim loaded = new ManualResetEventSlim();
|
||||
|
||||
private readonly BindableList<BeatmapSetInfo> detachedBeatmapSets = new BindableList<BeatmapSetInfo>();
|
||||
|
||||
private IDisposable? realmSubscription;
|
||||
|
||||
private readonly Queue<OperationArgs> pendingOperations = new Queue<OperationArgs>();
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
public IBindableList<BeatmapSetInfo> GetDetachedBeatmaps(CancellationToken? cancellationToken)
|
||||
{
|
||||
loaded.Wait(cancellationToken ?? CancellationToken.None);
|
||||
return detachedBeatmapSets.GetBoundCopy();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
realmSubscription = realm.RegisterForNotifications(r => r.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected), beatmapSetsChanged);
|
||||
}
|
||||
|
||||
private void beatmapSetsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes)
|
||||
{
|
||||
if (changes == null)
|
||||
{
|
||||
if (detachedBeatmapSets.Count > 0 && sender.Count == 0)
|
||||
{
|
||||
// Usually we'd reset stuff here, but doing so triggers a silly flow which ends up deadlocking realm.
|
||||
// Additionally, user should not be at song select when realm is blocking all operations in the first place.
|
||||
//
|
||||
// Note that due to the catch-up logic below, once operations are restored we will still be in a roughly
|
||||
// correct state. The only things that this return will change is the carousel will not empty *during* the blocking
|
||||
// operation.
|
||||
return;
|
||||
}
|
||||
|
||||
// Detaching beatmaps takes some time, so let's make sure it doesn't run on the update thread.
|
||||
var frozenSets = sender.Freeze();
|
||||
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
realm.Run(_ =>
|
||||
{
|
||||
var detached = frozenSets.Detach();
|
||||
|
||||
detachedBeatmapSets.Clear();
|
||||
detachedBeatmapSets.AddRange(detached);
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
loaded.Set();
|
||||
}
|
||||
}, TaskCreationOptions.LongRunning).FireAndForget();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (int i in changes.DeletedIndices.OrderDescending())
|
||||
{
|
||||
pendingOperations.Enqueue(new OperationArgs
|
||||
{
|
||||
Type = OperationType.Remove,
|
||||
Index = i,
|
||||
});
|
||||
}
|
||||
|
||||
foreach (int i in changes.InsertedIndices)
|
||||
{
|
||||
pendingOperations.Enqueue(new OperationArgs
|
||||
{
|
||||
Type = OperationType.Insert,
|
||||
BeatmapSet = sender[i].Detach(),
|
||||
Index = i,
|
||||
});
|
||||
}
|
||||
|
||||
foreach (int i in changes.NewModifiedIndices)
|
||||
{
|
||||
pendingOperations.Enqueue(new OperationArgs
|
||||
{
|
||||
Type = OperationType.Update,
|
||||
BeatmapSet = sender[i].Detach(),
|
||||
Index = i,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// We can't start processing operations until we have finished detaching the initial list.
|
||||
if (!loaded.IsSet)
|
||||
return;
|
||||
|
||||
// If this ever leads to performance issues, we could dequeue a limited number of operations per update frame.
|
||||
while (pendingOperations.TryDequeue(out var op))
|
||||
{
|
||||
switch (op.Type)
|
||||
{
|
||||
case OperationType.Insert:
|
||||
detachedBeatmapSets.Insert(op.Index, op.BeatmapSet!);
|
||||
break;
|
||||
|
||||
case OperationType.Update:
|
||||
detachedBeatmapSets.ReplaceRange(op.Index, 1, new[] { op.BeatmapSet! });
|
||||
break;
|
||||
|
||||
case OperationType.Remove:
|
||||
detachedBeatmapSets.RemoveAt(op.Index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
loaded.Set();
|
||||
loaded.Dispose();
|
||||
realmSubscription?.Dispose();
|
||||
}
|
||||
|
||||
private record OperationArgs
|
||||
{
|
||||
public OperationType Type;
|
||||
public BeatmapSetInfo? BeatmapSet;
|
||||
public int Index;
|
||||
}
|
||||
|
||||
private enum OperationType
|
||||
{
|
||||
Insert,
|
||||
Update,
|
||||
Remove
|
||||
}
|
||||
}
|
||||
}
|
@ -10,9 +10,6 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.OSD;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -25,13 +22,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
private Color4 hoverColour;
|
||||
|
||||
[Resolved]
|
||||
private GameHost host { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private Clipboard clipboard { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OnScreenDisplay? onScreenDisplay { get; set; }
|
||||
private OsuGame? game { get; set; }
|
||||
|
||||
private readonly SpriteIcon linkIcon;
|
||||
|
||||
@ -71,7 +62,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
if (Link != null)
|
||||
host.OpenUrlExternally(Link);
|
||||
game?.OpenUrlExternally(Link);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -85,8 +76,8 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
if (Link != null)
|
||||
{
|
||||
items.Add(new OsuMenuItem("Open", MenuItemType.Highlighted, () => host.OpenUrlExternally(Link)));
|
||||
items.Add(new OsuMenuItem("Copy URL", MenuItemType.Standard, copyUrl));
|
||||
items.Add(new OsuMenuItem("Open", MenuItemType.Highlighted, () => game?.OpenUrlExternally(Link)));
|
||||
items.Add(new OsuMenuItem("Copy link", MenuItemType.Standard, copyUrl));
|
||||
}
|
||||
|
||||
return items.ToArray();
|
||||
@ -95,11 +86,9 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
private void copyUrl()
|
||||
{
|
||||
if (Link != null)
|
||||
{
|
||||
clipboard.SetText(Link);
|
||||
onScreenDisplay?.Display(new CopyUrlToast());
|
||||
}
|
||||
if (Link == null) return;
|
||||
|
||||
game?.CopyUrlToClipboard(Link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -147,6 +147,10 @@ namespace osu.Game.Input.Bindings
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Left }, GlobalAction.EditorSeekToPreviousHitObject),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Right }, GlobalAction.EditorSeekToNextHitObject),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.Left }, GlobalAction.EditorSeekToPreviousSamplePoint),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.Right }, GlobalAction.EditorSeekToNextSamplePoint),
|
||||
};
|
||||
|
||||
private static IEnumerable<KeyBinding> editorTestPlayKeyBindings => new[]
|
||||
@ -456,6 +460,18 @@ namespace osu.Game.Input.Bindings
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTestPlayQuickExitToCurrentTime))]
|
||||
EditorTestPlayQuickExitToCurrentTime,
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSeekToPreviousHitObject))]
|
||||
EditorSeekToPreviousHitObject,
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSeekToNextHitObject))]
|
||||
EditorSeekToNextHitObject,
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSeekToPreviousSamplePoint))]
|
||||
EditorSeekToPreviousSamplePoint,
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSeekToNextSamplePoint))]
|
||||
EditorSeekToNextSamplePoint,
|
||||
}
|
||||
|
||||
public enum GlobalActionCategory
|
||||
|
@ -84,6 +84,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString AlwaysShowGameplayLeaderboard => new TranslatableString(getKey(@"gameplay_leaderboard"), @"Always show gameplay leaderboard");
|
||||
|
||||
/// <summary>
|
||||
/// "Always show hold for menu button"
|
||||
/// </summary>
|
||||
public static LocalisableString AlwaysShowHoldForMenuButton => new TranslatableString(getKey(@"always_show_hold_for_menu_button"), @"Always show hold for menu button");
|
||||
|
||||
/// <summary>
|
||||
/// "Always play first combo break sound"
|
||||
/// </summary>
|
||||
|
@ -404,6 +404,26 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString DecreaseModSpeed => new TranslatableString(getKey(@"decrease_mod_speed"), @"Decrease mod speed");
|
||||
|
||||
/// <summary>
|
||||
/// "Seek to previous hit object"
|
||||
/// </summary>
|
||||
public static LocalisableString EditorSeekToPreviousHitObject => new TranslatableString(getKey(@"editor_seek_to_previous_hit_object"), @"Seek to previous hit object");
|
||||
|
||||
/// <summary>
|
||||
/// "Seek to next hit object"
|
||||
/// </summary>
|
||||
public static LocalisableString EditorSeekToNextHitObject => new TranslatableString(getKey(@"editor_seek_to_next_hit_object"), @"Seek to next hit object");
|
||||
|
||||
/// <summary>
|
||||
/// "Seek to previous sample point"
|
||||
/// </summary>
|
||||
public static LocalisableString EditorSeekToPreviousSamplePoint => new TranslatableString(getKey(@"editor_seek_to_previous_sample_point"), @"Seek to previous sample point");
|
||||
|
||||
/// <summary>
|
||||
/// "Seek to next sample point"
|
||||
/// </summary>
|
||||
public static LocalisableString EditorSeekToNextSamplePoint => new TranslatableString(getKey(@"editor_seek_to_next_sample_point"), @"Seek to next sample point");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -34,11 +34,6 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString ProhibitedInteractDuringMigration => new TranslatableString(getKey(@"prohibited_interact_during_migration"), @"Please avoid interacting with the game!");
|
||||
|
||||
/// <summary>
|
||||
/// "Some files couldn't be cleaned up during migration. Clicking this notification will open the folder so you can manually clean things up."
|
||||
/// </summary>
|
||||
public static LocalisableString FailedCleanupNotification => new TranslatableString(getKey(@"failed_cleanup_notification"), @"Some files couldn't be cleaned up during migration. Clicking this notification will open the folder so you can manually clean things up.");
|
||||
|
||||
/// <summary>
|
||||
/// "Please select a new location"
|
||||
/// </summary>
|
||||
|
@ -45,9 +45,9 @@ namespace osu.Game.Localisation
|
||||
public static LocalisableString SkinSaved => new TranslatableString(getKey(@"skin_saved"), @"Skin saved");
|
||||
|
||||
/// <summary>
|
||||
/// "URL copied"
|
||||
/// "Link copied to clipboard"
|
||||
/// </summary>
|
||||
public static LocalisableString UrlCopied => new TranslatableString(getKey(@"url_copied"), @"URL copied");
|
||||
public static LocalisableString UrlCopied => new TranslatableString(getKey(@"url_copied"), @"Link copied to clipboard");
|
||||
|
||||
/// <summary>
|
||||
/// "Speed changed to {0:N2}x"
|
||||
|
@ -159,7 +159,7 @@ namespace osu.Game.Online.API
|
||||
|
||||
private void onTokenChanged(ValueChangedEvent<OAuthToken> e) => config.SetValue(OsuSetting.Token, config.Get<bool>(OsuSetting.SavePassword) ? authentication.TokenString : string.Empty);
|
||||
|
||||
internal new void Schedule(Action action) => base.Schedule(action);
|
||||
void IAPIProvider.Schedule(Action action) => base.Schedule(action);
|
||||
|
||||
public string AccessToken => authentication.RequestAccessToken();
|
||||
|
||||
@ -385,7 +385,8 @@ namespace osu.Game.Online.API
|
||||
{
|
||||
try
|
||||
{
|
||||
request.Perform(this);
|
||||
request.AttachAPI(this);
|
||||
request.Perform();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -483,7 +484,8 @@ namespace osu.Game.Online.API
|
||||
{
|
||||
try
|
||||
{
|
||||
req.Perform(this);
|
||||
req.AttachAPI(this);
|
||||
req.Perform();
|
||||
|
||||
if (req.CompletionState != APIRequestCompletionState.Completed)
|
||||
return false;
|
||||
@ -568,6 +570,8 @@ namespace osu.Game.Online.API
|
||||
{
|
||||
lock (queue)
|
||||
{
|
||||
request.AttachAPI(this);
|
||||
|
||||
if (state.Value == APIState.Offline)
|
||||
{
|
||||
request.Fail(new WebException(@"User not logged in"));
|
||||
|
@ -4,6 +4,7 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using osu.Framework.IO.Network;
|
||||
|
||||
@ -34,7 +35,11 @@ namespace osu.Game.Online.API
|
||||
return request;
|
||||
}
|
||||
|
||||
private void request_Progress(long current, long total) => API.Schedule(() => Progressed?.Invoke(current, total));
|
||||
private void request_Progress(long current, long total)
|
||||
{
|
||||
Debug.Assert(API != null);
|
||||
API.Schedule(() => Progressed?.Invoke(current, total));
|
||||
}
|
||||
|
||||
protected void TriggerSuccess(string filename)
|
||||
{
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user