mirror of
https://github.com/ppy/osu.git
synced 2025-01-12 18:23:04 +08:00
Merge remote-tracking branch 'upstream/master' into doubleclick
This commit is contained in:
commit
ba0c4df2a9
@ -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)
|
||||
|
@ -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 =>
|
||||
{
|
||||
|
@ -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))
|
||||
{
|
||||
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))
|
||||
{
|
||||
if (Anchor.HasFlag(Anchor.y1))
|
||||
return;
|
||||
|
||||
Anchor &= ~(Anchor.y0 | Anchor.y2);
|
||||
Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0;
|
||||
}
|
||||
|
||||
// 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;
|
||||
// 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:
|
||||
|
@ -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", () =>
|
||||
|
@ -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),
|
||||
|
@ -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();
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -598,6 +598,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()
|
||||
{
|
||||
|
@ -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()
|
||||
@ -242,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", () =>
|
||||
{
|
||||
@ -260,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)
|
||||
|
@ -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();
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -60,7 +60,7 @@ namespace osu.Game.Online.Chat
|
||||
},
|
||||
new PopupDialogCancelButton
|
||||
{
|
||||
Text = @"Copy URL to the clipboard",
|
||||
Text = @"Copy link",
|
||||
Action = copyExternalLinkAction
|
||||
},
|
||||
new PopupDialogCancelButton
|
||||
|
@ -54,6 +54,7 @@ using osu.Game.Overlays.BeatmapListing;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Overlays.Music;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Overlays.OSD;
|
||||
using osu.Game.Overlays.SkinEditor;
|
||||
using osu.Game.Overlays.Toolbar;
|
||||
using osu.Game.Overlays.Volume;
|
||||
@ -142,6 +143,8 @@ namespace osu.Game
|
||||
|
||||
private Container overlayOffsetContainer;
|
||||
|
||||
private OnScreenDisplay onScreenDisplay;
|
||||
|
||||
[Resolved]
|
||||
private FrameworkConfigManager frameworkConfig { get; set; }
|
||||
|
||||
@ -497,6 +500,12 @@ namespace osu.Game
|
||||
}
|
||||
});
|
||||
|
||||
public void CopyUrlToClipboard(string url) => waitForReady(() => onScreenDisplay, _ =>
|
||||
{
|
||||
dependencies.Get<Clipboard>().SetText(url);
|
||||
onScreenDisplay.Display(new CopyUrlToast());
|
||||
});
|
||||
|
||||
public void OpenUrlExternally(string url, bool forceBypassExternalUrlWarning = false) => waitForReady(() => externalLinkOpener, _ =>
|
||||
{
|
||||
bool isTrustedDomain;
|
||||
@ -1078,7 +1087,7 @@ namespace osu.Game
|
||||
|
||||
loadComponentSingleFile(volume = new VolumeOverlay(), leftFloatingOverlayContent.Add, true);
|
||||
|
||||
var onScreenDisplay = new OnScreenDisplay();
|
||||
onScreenDisplay = new OnScreenDisplay();
|
||||
|
||||
onScreenDisplay.BeginTracking(this, frameworkConfig);
|
||||
onScreenDisplay.BeginTracking(this, LocalConfig);
|
||||
|
@ -515,6 +515,12 @@ namespace osu.Game
|
||||
/// <returns>Whether a restart operation was queued.</returns>
|
||||
public virtual bool RestartAppWhenExited() => false;
|
||||
|
||||
/// <summary>
|
||||
/// Perform migration of user data to a specified path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to migrate to.</param>
|
||||
/// <returns>Whether migration succeeded to completion. If <c>false</c>, some files were left behind.</returns>
|
||||
/// <exception cref="TimeoutException"></exception>
|
||||
public bool Migrate(string path)
|
||||
{
|
||||
Logger.Log($@"Migrating osu! data from ""{Storage.GetFullPath(string.Empty)}"" to ""{path}""...");
|
||||
@ -542,10 +548,10 @@ namespace osu.Game
|
||||
if (!readyToRun.Wait(30000) || !success)
|
||||
throw new TimeoutException("Attempting to block for migration took too long.");
|
||||
|
||||
bool? cleanupSucceded = (Storage as OsuStorage)?.Migrate(Host.GetStorage(path));
|
||||
bool? cleanupSucceeded = (Storage as OsuStorage)?.Migrate(Host.GetStorage(path));
|
||||
|
||||
Logger.Log(@"Migration complete!");
|
||||
return cleanupSucceded != false;
|
||||
return cleanupSucceeded != false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
@ -200,7 +200,8 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
|
||||
private void updateExternalLink()
|
||||
{
|
||||
if (externalLink != null) externalLink.Link = $@"{api.WebsiteRootUrl}/beatmapsets/{BeatmapSet.Value?.OnlineID}#{Picker.Beatmap.Value?.Ruleset.ShortName}/{Picker.Beatmap.Value?.OnlineID}";
|
||||
if (externalLink != null)
|
||||
externalLink.Link = Picker.Beatmap.Value?.GetOnlineURL(api) ?? BeatmapSet.Value?.GetOnlineURL(api);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
@ -47,7 +47,7 @@ namespace osu.Game.Overlays.Comments
|
||||
|
||||
public readonly BindableList<DrawableComment> Replies = new BindableList<DrawableComment>();
|
||||
|
||||
private readonly BindableBool childrenExpanded = new BindableBool(true);
|
||||
private readonly BindableBool childrenExpanded;
|
||||
|
||||
private int currentPage;
|
||||
|
||||
@ -92,6 +92,8 @@ namespace osu.Game.Overlays.Comments
|
||||
{
|
||||
Comment = comment;
|
||||
Meta = meta;
|
||||
|
||||
childrenExpanded = new BindableBool(!comment.Pinned);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
@ -30,7 +30,8 @@ namespace osu.Game.Overlays
|
||||
|
||||
private const float border_width = 5;
|
||||
|
||||
private readonly Medal medal;
|
||||
public readonly Medal Medal;
|
||||
|
||||
private readonly Box background;
|
||||
private readonly Container backgroundStrip, particleContainer;
|
||||
private readonly BackgroundStrip leftStrip, rightStrip;
|
||||
@ -44,7 +45,7 @@ namespace osu.Game.Overlays
|
||||
|
||||
public MedalAnimation(Medal medal)
|
||||
{
|
||||
this.medal = medal;
|
||||
Medal = medal;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
Child = content = new Container
|
||||
@ -168,7 +169,7 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
LoadComponentAsync(drawableMedal = new DrawableMedal(medal)
|
||||
LoadComponentAsync(drawableMedal = new DrawableMedal(Medal)
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
|
@ -8,6 +8,7 @@ using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Online.API;
|
||||
@ -81,7 +82,10 @@ namespace osu.Game.Overlays
|
||||
};
|
||||
|
||||
var medalAnimation = new MedalAnimation(medal);
|
||||
|
||||
queuedMedals.Enqueue(medalAnimation);
|
||||
Logger.Log($"Queueing medal unlock for \"{medal.Name}\" ({queuedMedals.Count} to display)");
|
||||
|
||||
if (OverlayActivationMode.Value == OverlayActivation.All)
|
||||
Scheduler.AddOnce(Show);
|
||||
}
|
||||
@ -95,10 +99,12 @@ namespace osu.Game.Overlays
|
||||
|
||||
if (!queuedMedals.TryDequeue(out lastAnimation))
|
||||
{
|
||||
Logger.Log("All queued medals have been displayed!");
|
||||
Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.Log($"Preparing to display \"{lastAnimation.Medal.Name}\"");
|
||||
LoadComponentAsync(lastAnimation, medalContainer.Add);
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Configuration;
|
||||
@ -214,15 +215,24 @@ namespace osu.Game.Overlays.Mods
|
||||
this.panel = panel;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
private InputManager inputManager = null!;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
if (ExpandedState.Value is ModCustomisationPanelState.ExpandedByHover
|
||||
&& !ReceivePositionalInputAt(e.ScreenSpaceMousePosition))
|
||||
base.LoadComplete();
|
||||
inputManager = GetContainingInputManager()!;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (ExpandedState.Value == ModCustomisationPanelState.ExpandedByHover
|
||||
&& !ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position)
|
||||
&& inputManager.DraggedDrawable == null)
|
||||
{
|
||||
ExpandedState.Value = ModCustomisationPanelState.Collapsed;
|
||||
}
|
||||
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -115,7 +115,7 @@ namespace osu.Game.Overlays
|
||||
seekDelegate?.Cancel();
|
||||
seekDelegate = Schedule(() =>
|
||||
{
|
||||
if (beatmap.Disabled || !AllowTrackControl.Value)
|
||||
if (!AllowTrackControl.Value)
|
||||
return;
|
||||
|
||||
CurrentTrack.Seek(position);
|
||||
|
@ -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.Extensions.LocalisationExtensions;
|
||||
@ -11,9 +12,9 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Overlays.Profile.Header.Components
|
||||
{
|
||||
@ -107,15 +108,18 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
APIUserDailyChallengeStatistics stats = User.Value.User.DailyChallengeStatistics;
|
||||
|
||||
dailyPlayCount.Text = DailyChallengeStatsDisplayStrings.UnitDay(stats.PlayCount.ToLocalisableString("N0"));
|
||||
dailyPlayCount.Colour = colours.ForRankingTier(tierForPlayCount(stats.PlayCount));
|
||||
dailyPlayCount.Colour = colours.ForRankingTier(TierForPlayCount(stats.PlayCount));
|
||||
|
||||
TooltipContent = new DailyChallengeTooltipData(colourProvider, stats);
|
||||
|
||||
Show();
|
||||
|
||||
static RankingTier tierForPlayCount(int playCount) => DailyChallengeStatsTooltip.TierForDaily(playCount / 3);
|
||||
}
|
||||
|
||||
// Rounding up is needed here to ensure the overlay shows the same colour as osu-web for the play count.
|
||||
// This is because, for example, 31 / 3 > 10 in JavaScript because floats are used, while here it would
|
||||
// get truncated to 10 with an integer division and show a lower tier.
|
||||
public static RankingTier TierForPlayCount(int playCount) => DailyChallengeStatsTooltip.TierForDaily((int)Math.Ceiling(playCount / 3.0d));
|
||||
|
||||
public ITooltip<DailyChallengeTooltipData> GetCustomTooltip() => new DailyChallengeStatsTooltip();
|
||||
}
|
||||
}
|
||||
|
@ -6,17 +6,14 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Screens;
|
||||
using osuTK;
|
||||
|
||||
@ -29,15 +26,6 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
[Resolved(canBeNull: true)]
|
||||
private OsuGame game { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private INotificationOverlay notifications { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private Storage storage { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private GameHost host { get; set; }
|
||||
|
||||
public override bool AllowBackButton => false;
|
||||
|
||||
public override bool AllowExternalScreenChange => false;
|
||||
@ -99,8 +87,6 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
|
||||
Beatmap.Value = Beatmap.Default;
|
||||
|
||||
var originalStorage = new NativeStorage(storage.GetFullPath(string.Empty), host);
|
||||
|
||||
migrationTask = Task.Run(PerformMigration)
|
||||
.ContinueWith(task =>
|
||||
{
|
||||
@ -108,18 +94,6 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
{
|
||||
Logger.Error(task.Exception, $"Error during migration: {task.Exception?.Message}");
|
||||
}
|
||||
else if (!task.GetResultSafely())
|
||||
{
|
||||
notifications.Post(new SimpleNotification
|
||||
{
|
||||
Text = MaintenanceSettingsStrings.FailedCleanupNotification,
|
||||
Activated = () =>
|
||||
{
|
||||
originalStorage.PresentExternally();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Schedule(this.Exit);
|
||||
});
|
||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
public Action<Type>? RequestPlacement;
|
||||
|
||||
private readonly SkinComponentsContainer target;
|
||||
private readonly SkinnableContainer target;
|
||||
|
||||
private readonly RulesetInfo? ruleset;
|
||||
|
||||
@ -35,7 +35,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
/// </summary>
|
||||
/// <param name="target">The target. This is mainly used as a dependency source to find candidate components.</param>
|
||||
/// <param name="ruleset">A ruleset to filter components by. If null, only components which are not ruleset-specific will be included.</param>
|
||||
public SkinComponentToolbox(SkinComponentsContainer target, RulesetInfo? ruleset)
|
||||
public SkinComponentToolbox(SkinnableContainer target, RulesetInfo? ruleset)
|
||||
: base(ruleset == null ? SkinEditorStrings.Components : LocalisableString.Interpolate($"{SkinEditorStrings.Components} ({ruleset.Name})"))
|
||||
{
|
||||
this.target = target;
|
||||
|
@ -72,7 +72,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
|
||||
|
||||
private readonly Bindable<SkinComponentsContainerLookup?> selectedTarget = new Bindable<SkinComponentsContainerLookup?>();
|
||||
private readonly Bindable<GlobalSkinnableContainerLookup?> selectedTarget = new Bindable<GlobalSkinnableContainerLookup?>();
|
||||
|
||||
private bool hasBegunMutating;
|
||||
|
||||
@ -330,7 +330,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
}
|
||||
}
|
||||
|
||||
private void targetChanged(ValueChangedEvent<SkinComponentsContainerLookup?> target)
|
||||
private void targetChanged(ValueChangedEvent<GlobalSkinnableContainerLookup?> target)
|
||||
{
|
||||
foreach (var toolbox in componentsSidebar.OfType<SkinComponentToolbox>())
|
||||
toolbox.Expire();
|
||||
@ -360,7 +360,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new SettingsDropdown<SkinComponentsContainerLookup?>
|
||||
new SettingsDropdown<GlobalSkinnableContainerLookup?>
|
||||
{
|
||||
Items = availableTargets.Select(t => t.Lookup).Distinct(),
|
||||
Current = selectedTarget,
|
||||
@ -472,18 +472,18 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
settingsSidebar.Add(new SkinSettingsToolbox(component));
|
||||
}
|
||||
|
||||
private IEnumerable<SkinComponentsContainer> availableTargets => targetScreen.ChildrenOfType<SkinComponentsContainer>();
|
||||
private IEnumerable<SkinnableContainer> availableTargets => targetScreen.ChildrenOfType<SkinnableContainer>();
|
||||
|
||||
private SkinComponentsContainer? getFirstTarget() => availableTargets.FirstOrDefault();
|
||||
private SkinnableContainer? getFirstTarget() => availableTargets.FirstOrDefault();
|
||||
|
||||
private SkinComponentsContainer? getTarget(SkinComponentsContainerLookup? target)
|
||||
private SkinnableContainer? getTarget(GlobalSkinnableContainerLookup? target)
|
||||
{
|
||||
return availableTargets.FirstOrDefault(c => c.Lookup.Equals(target));
|
||||
}
|
||||
|
||||
private void revert()
|
||||
{
|
||||
SkinComponentsContainer[] targetContainers = availableTargets.ToArray();
|
||||
SkinnableContainer[] targetContainers = availableTargets.ToArray();
|
||||
|
||||
foreach (var t in targetContainers)
|
||||
{
|
||||
@ -555,7 +555,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
if (targetScreen?.IsLoaded != true)
|
||||
return;
|
||||
|
||||
SkinComponentsContainer[] targetContainers = availableTargets.ToArray();
|
||||
SkinnableContainer[] targetContainers = availableTargets.ToArray();
|
||||
|
||||
if (!targetContainers.All(c => c.ComponentsLoaded))
|
||||
return;
|
||||
@ -600,7 +600,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
public void BringSelectionToFront()
|
||||
{
|
||||
if (getTarget(selectedTarget.Value) is not SkinComponentsContainer target)
|
||||
if (getTarget(selectedTarget.Value) is not SkinnableContainer target)
|
||||
return;
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
@ -624,7 +624,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
public void SendSelectionToBack()
|
||||
{
|
||||
if (getTarget(selectedTarget.Value) is not SkinComponentsContainer target)
|
||||
if (getTarget(selectedTarget.Value) is not SkinnableContainer target)
|
||||
return;
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Overlays.Toolbar
|
||||
public Bindable<UserStatisticsUpdate?> LatestUpdate { get; } = new Bindable<UserStatisticsUpdate?>();
|
||||
|
||||
private Statistic<int> globalRank = null!;
|
||||
private Statistic<decimal> pp = null!;
|
||||
private Statistic<int> pp = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(UserStatisticsWatcher? userStatisticsWatcher)
|
||||
@ -43,7 +43,7 @@ namespace osu.Game.Overlays.Toolbar
|
||||
Children = new Drawable[]
|
||||
{
|
||||
globalRank = new Statistic<int>(UsersStrings.ShowRankGlobalSimple, @"#", Comparer<int>.Create((before, after) => before - after)),
|
||||
pp = new Statistic<decimal>(RankingsStrings.StatPerformance, string.Empty, Comparer<decimal>.Create((before, after) => Math.Sign(after - before))),
|
||||
pp = new Statistic<int>(RankingsStrings.StatPerformance, string.Empty, Comparer<int>.Create((before, after) => Math.Sign(after - before))),
|
||||
}
|
||||
};
|
||||
|
||||
@ -83,7 +83,7 @@ namespace osu.Game.Overlays.Toolbar
|
||||
}
|
||||
|
||||
if (update.After.PP != null)
|
||||
pp.Display(update.Before.PP ?? update.After.PP.Value, Math.Abs((update.After.PP - update.Before.PP) ?? 0M), update.After.PP.Value);
|
||||
pp.Display((int)(update.Before.PP ?? update.After.PP.Value), (int)Math.Abs(((int?)update.After.PP - (int?)update.Before.PP) ?? 0M), (int)update.After.PP.Value);
|
||||
|
||||
this.Delay(5000).FadeOut(500, Easing.OutQuint);
|
||||
});
|
||||
|
@ -163,7 +163,7 @@ namespace osu.Game.Rulesets.Judgements
|
||||
if (JudgementBody != null)
|
||||
RemoveInternal(JudgementBody, true);
|
||||
|
||||
AddInternal(JudgementBody = new SkinnableDrawable(new GameplaySkinComponentLookup<HitResult>(type), _ =>
|
||||
AddInternal(JudgementBody = new SkinnableDrawable(new SkinComponentLookup<HitResult>(type), _ =>
|
||||
CreateDefaultJudgement(type), confineMode: ConfineMode.NoScaling));
|
||||
|
||||
JudgementBody.OnSkinChanged += () =>
|
||||
|
@ -401,5 +401,10 @@ namespace osu.Game.Rulesets
|
||||
new DifficultySection(),
|
||||
new ColoursSection(),
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Can be overridden to avoid showing scroll speed changes in the editor.
|
||||
/// </summary>
|
||||
public virtual bool EditorShowScrollSpeed => true;
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
@ -139,7 +140,7 @@ namespace osu.Game.Rulesets.UI
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Alpha = 0,
|
||||
Font = OsuFont.Numeric.With(null, 22f),
|
||||
Font = OsuFont.Numeric.With(size: 22f, weight: FontWeight.Black),
|
||||
UseFullGlyphHeight = false,
|
||||
Text = mod.Acronym
|
||||
},
|
||||
@ -204,7 +205,7 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
private void updateColour()
|
||||
{
|
||||
modAcronym.Colour = modIcon.Colour = OsuColour.Gray(84);
|
||||
modAcronym.Colour = modIcon.Colour = Interpolation.ValueAt<Colour4>(0.1f, Colour4.Black, backgroundColour, 0, 1);
|
||||
|
||||
extendedText.Colour = background.Colour = Selected.Value ? backgroundColour.Lighten(0.2f) : backgroundColour;
|
||||
extendedBackground.Colour = Selected.Value ? backgroundColour.Darken(2.4f) : backgroundColour.Darken(2.8f);
|
||||
|
@ -64,8 +64,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
MaxValue = time_span_max
|
||||
};
|
||||
|
||||
ScrollVisualisationMethod IDrawableScrollingRuleset.VisualisationMethod => VisualisationMethod;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the player can change <see cref="TimeRange"/>.
|
||||
/// </summary>
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Configuration;
|
||||
|
||||
namespace osu.Game.Rulesets.UI.Scrolling
|
||||
{
|
||||
/// <summary>
|
||||
@ -10,8 +8,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
/// </summary>
|
||||
public interface IDrawableScrollingRuleset
|
||||
{
|
||||
ScrollVisualisationMethod VisualisationMethod { get; }
|
||||
|
||||
IScrollingInfo ScrollingInfo { get; }
|
||||
}
|
||||
}
|
||||
|
@ -145,18 +145,18 @@ namespace osu.Game.Screens.Edit
|
||||
{
|
||||
case 1:
|
||||
case 2:
|
||||
return new Vector2(0.6f, 0.9f);
|
||||
return new Vector2(1, 0.9f);
|
||||
|
||||
case 3:
|
||||
case 4:
|
||||
return new Vector2(0.5f, 0.8f);
|
||||
return new Vector2(0.8f, 0.8f);
|
||||
|
||||
case 6:
|
||||
case 8:
|
||||
return new Vector2(0.4f, 0.7f);
|
||||
return new Vector2(0.8f, 0.7f);
|
||||
|
||||
default:
|
||||
return new Vector2(0.3f, 0.6f);
|
||||
return new Vector2(0.8f, 0.6f);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,7 +79,14 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
|
||||
{
|
||||
ClearInternal();
|
||||
|
||||
AddInternal(new ControlPointVisualisation(effect));
|
||||
if (beatmap.BeatmapInfo.Ruleset.CreateInstance().EditorShowScrollSpeed)
|
||||
{
|
||||
AddInternal(new ControlPointVisualisation(effect)
|
||||
{
|
||||
// importantly, override the x position being set since we do that in the GroupVisualisation parent drawable.
|
||||
X = 0,
|
||||
});
|
||||
}
|
||||
|
||||
if (!kiai.Value)
|
||||
return;
|
||||
|
@ -526,7 +526,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
AlwaysDisplayed = alwaysDisplayed;
|
||||
Divisor = divisor;
|
||||
|
||||
Size = new Vector2(6f, 18) * BindableBeatDivisor.GetSize(divisor);
|
||||
Size = new Vector2(4, 18) * BindableBeatDivisor.GetSize(divisor);
|
||||
Alpha = alwaysDisplayed ? 1 : 0;
|
||||
|
||||
InternalChild = new Box { RelativeSizeAxes = Axes.Both };
|
||||
|
@ -229,7 +229,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
EditorBeatmap.PerformOnSelection(h =>
|
||||
{
|
||||
if (h.Samples.All(s => s.Bank == bankName))
|
||||
if (hasRelevantBank(h))
|
||||
return;
|
||||
|
||||
h.Samples = h.Samples.Select(s => s.With(newBank: bankName)).ToList();
|
||||
@ -269,9 +269,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
EditorBeatmap.PerformOnSelection(h =>
|
||||
{
|
||||
// Make sure there isn't already an existing sample
|
||||
if (h.Samples.Any(s => s.Name == sampleName))
|
||||
return;
|
||||
|
||||
if (h.Samples.All(s => s.Name != sampleName))
|
||||
h.Samples.Add(h.CreateHitSampleInfo(sampleName));
|
||||
|
||||
if (h is IHasRepeats hasRepeats)
|
||||
|
@ -2,9 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Overlays;
|
||||
@ -14,32 +12,28 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
public partial class CentreMarker : CompositeDrawable
|
||||
{
|
||||
private const float triangle_width = 8;
|
||||
|
||||
private const float bar_width = 1.6f;
|
||||
|
||||
public CentreMarker()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
Size = new Vector2(triangle_width, 1);
|
||||
|
||||
Anchor = Anchor.TopCentre;
|
||||
Origin = Anchor.TopCentre;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colours)
|
||||
{
|
||||
const float triangle_width = 8;
|
||||
const float bar_width = 2f;
|
||||
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
|
||||
Anchor = Anchor.TopCentre;
|
||||
Origin = Anchor.TopCentre;
|
||||
|
||||
Size = new Vector2(triangle_width, 1);
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
new Circle
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = bar_width,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Colour = ColourInfo.GradientVertical(colours.Colour2.Opacity(0.6f), colours.Colour2.Opacity(0)),
|
||||
Colour = colours.Colour2,
|
||||
},
|
||||
new Triangle
|
||||
{
|
||||
@ -47,6 +41,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
Origin = Anchor.BottomCentre,
|
||||
Size = new Vector2(triangle_width, triangle_width * 0.8f),
|
||||
Scale = new Vector2(1, -1),
|
||||
EdgeSmoothness = new Vector2(1, 0),
|
||||
Colour = colours.Colour2,
|
||||
},
|
||||
new Triangle
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Size = new Vector2(triangle_width, triangle_width * 0.8f),
|
||||
Scale = new Vector2(1, 1),
|
||||
Colour = colours.Colour2,
|
||||
},
|
||||
};
|
||||
|
@ -14,6 +14,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
@ -24,7 +25,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
public partial class Timeline : ZoomableScrollContainer, IPositionSnapProvider
|
||||
{
|
||||
private const float timeline_height = 80;
|
||||
private const float timeline_expanded_height = 94;
|
||||
|
||||
private readonly Drawable userContent;
|
||||
|
||||
@ -78,9 +78,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
private TimelineTickDisplay ticks = null!;
|
||||
|
||||
private TimelineControlPointDisplay controlPoints = null!;
|
||||
|
||||
private Container mainContent = null!;
|
||||
private TimelineTimingChangeDisplay controlPoints = null!;
|
||||
|
||||
private Bindable<float> waveformOpacity = null!;
|
||||
private Bindable<bool> controlPointsVisible = null!;
|
||||
@ -103,31 +101,34 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours, OsuConfigManager config)
|
||||
private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours, OverlayColourProvider colourProvider, OsuConfigManager config)
|
||||
{
|
||||
CentreMarker centreMarker;
|
||||
|
||||
// We don't want the centre marker to scroll
|
||||
AddInternal(centreMarker = new CentreMarker());
|
||||
|
||||
ticks = new TimelineTickDisplay
|
||||
{
|
||||
Padding = new MarginPadding { Vertical = 2, },
|
||||
};
|
||||
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
controlPoints = new TimelineControlPointDisplay
|
||||
ticks = new TimelineTickDisplay(),
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = timeline_expanded_height,
|
||||
Name = "zero marker",
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = TimelineTickDisplay.TICK_WIDTH / 2,
|
||||
Origin = Anchor.TopCentre,
|
||||
Colour = colourProvider.Background1,
|
||||
},
|
||||
ticks,
|
||||
mainContent = new Container
|
||||
controlPoints = new TimelineTimingChangeDisplay
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = timeline_height,
|
||||
Depth = float.MaxValue,
|
||||
Children = new[]
|
||||
{
|
||||
waveform = new WaveformGraph
|
||||
@ -138,16 +139,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
MidColour = colours.BlueDark,
|
||||
HighColour = colours.BlueDarker,
|
||||
},
|
||||
ticks.CreateProxy(),
|
||||
centreMarker.CreateProxy(),
|
||||
new Box
|
||||
{
|
||||
Name = "zero marker",
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = 2,
|
||||
Origin = Anchor.TopCentre,
|
||||
Colour = colours.YellowDarker,
|
||||
},
|
||||
ticks.CreateProxy(),
|
||||
userContent,
|
||||
}
|
||||
},
|
||||
@ -192,21 +185,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
controlPointsVisible.BindValueChanged(visible =>
|
||||
{
|
||||
if (visible.NewValue || alwaysShowControlPoints)
|
||||
{
|
||||
this.ResizeHeightTo(timeline_expanded_height, 200, Easing.OutQuint);
|
||||
mainContent.MoveToY(15, 200, Easing.OutQuint);
|
||||
|
||||
// delay the fade in else masking looks weird.
|
||||
controlPoints.Delay(180).FadeIn(400, Easing.OutQuint);
|
||||
}
|
||||
controlPoints.FadeIn(400, Easing.OutQuint);
|
||||
else
|
||||
{
|
||||
controlPoints.FadeOut(200, Easing.OutQuint);
|
||||
|
||||
// likewise, delay the resize until the fade is complete.
|
||||
this.Delay(180).ResizeHeightTo(timeline_height, 200, Easing.OutQuint);
|
||||
mainContent.Delay(180).MoveToY(0, 200, Easing.OutQuint);
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
|
@ -1,98 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
/// <summary>
|
||||
/// The part of the timeline that displays the control points.
|
||||
/// </summary>
|
||||
public partial class TimelineControlPointDisplay : TimelinePart<TimelineControlPointGroup>
|
||||
{
|
||||
[Resolved]
|
||||
private Timeline timeline { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The visible time/position range of the timeline.
|
||||
/// </summary>
|
||||
private (float min, float max) visibleRange = (float.MinValue, float.MaxValue);
|
||||
|
||||
private readonly Cached groupCache = new Cached();
|
||||
|
||||
private readonly IBindableList<ControlPointGroup> controlPointGroups = new BindableList<ControlPointGroup>();
|
||||
|
||||
protected override void LoadBeatmap(EditorBeatmap beatmap)
|
||||
{
|
||||
base.LoadBeatmap(beatmap);
|
||||
|
||||
controlPointGroups.UnbindAll();
|
||||
controlPointGroups.BindTo(beatmap.ControlPointInfo.Groups);
|
||||
controlPointGroups.BindCollectionChanged((_, _) => groupCache.Invalidate(), true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (DrawWidth <= 0) return;
|
||||
|
||||
(float, float) newRange = (
|
||||
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - TopPointPiece.WIDTH) / DrawWidth * Content.RelativeChildSize.X,
|
||||
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X) / DrawWidth * Content.RelativeChildSize.X);
|
||||
|
||||
if (visibleRange != newRange)
|
||||
{
|
||||
visibleRange = newRange;
|
||||
groupCache.Invalidate();
|
||||
}
|
||||
|
||||
if (!groupCache.IsValid)
|
||||
{
|
||||
recreateDrawableGroups();
|
||||
groupCache.Validate();
|
||||
}
|
||||
}
|
||||
|
||||
private void recreateDrawableGroups()
|
||||
{
|
||||
// Remove groups outside the visible range
|
||||
foreach (TimelineControlPointGroup drawableGroup in this)
|
||||
{
|
||||
if (!shouldBeVisible(drawableGroup.Group))
|
||||
drawableGroup.Expire();
|
||||
}
|
||||
|
||||
// Add remaining ones
|
||||
for (int i = 0; i < controlPointGroups.Count; i++)
|
||||
{
|
||||
var group = controlPointGroups[i];
|
||||
|
||||
if (!shouldBeVisible(group))
|
||||
continue;
|
||||
|
||||
bool alreadyVisible = false;
|
||||
|
||||
foreach (var g in this)
|
||||
{
|
||||
if (ReferenceEquals(g.Group, group))
|
||||
{
|
||||
alreadyVisible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (alreadyVisible)
|
||||
continue;
|
||||
|
||||
Add(new TimelineControlPointGroup(group));
|
||||
}
|
||||
}
|
||||
|
||||
private bool shouldBeVisible(ControlPointGroup group) => group.Time >= visibleRange.min && group.Time <= visibleRange.max;
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
public partial class TimelineControlPointGroup : CompositeDrawable
|
||||
{
|
||||
public readonly ControlPointGroup Group;
|
||||
|
||||
private readonly IBindableList<ControlPoint> controlPoints = new BindableList<ControlPoint>();
|
||||
|
||||
public TimelineControlPointGroup(ControlPointGroup group)
|
||||
{
|
||||
Group = group;
|
||||
|
||||
RelativePositionAxes = Axes.X;
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
AutoSizeAxes = Axes.X;
|
||||
|
||||
Origin = Anchor.TopLeft;
|
||||
|
||||
// offset visually to avoid overlapping timeline tick display.
|
||||
X = (float)group.Time + 6;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
controlPoints.BindTo(Group.ControlPoints);
|
||||
controlPoints.BindCollectionChanged((_, _) =>
|
||||
{
|
||||
ClearInternal();
|
||||
|
||||
foreach (var point in controlPoints)
|
||||
{
|
||||
switch (point)
|
||||
{
|
||||
case TimingControlPoint timingPoint:
|
||||
AddInternal(new TimingPointPiece(timingPoint));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
}
|
@ -30,7 +30,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
public partial class TimelineHitObjectBlueprint : SelectionBlueprint<HitObject>
|
||||
{
|
||||
private const float circle_size = 38;
|
||||
private const float circle_size = 32;
|
||||
|
||||
private Container? repeatsContainer;
|
||||
|
||||
|
@ -17,6 +17,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
public partial class TimelineTickDisplay : TimelinePart<PointVisualisation>
|
||||
{
|
||||
public const float TICK_WIDTH = 3;
|
||||
|
||||
// With current implementation every tick in the sub-tree should be visible, no need to check whether they are masked away.
|
||||
public override bool UpdateSubTreeMasking() => false;
|
||||
|
||||
@ -138,20 +140,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
// even though "bar lines" take up the full vertical space, we render them in two pieces because it allows for less anchor/origin churn.
|
||||
|
||||
Vector2 size = Vector2.One;
|
||||
|
||||
if (indexInBar != 0)
|
||||
size = BindableBeatDivisor.GetSize(divisor);
|
||||
var size = indexInBar == 0
|
||||
? new Vector2(1.3f, 1)
|
||||
: BindableBeatDivisor.GetSize(divisor);
|
||||
|
||||
var line = getNextUsableLine();
|
||||
line.X = xPos;
|
||||
|
||||
line.Anchor = Anchor.CentreLeft;
|
||||
line.Origin = Anchor.Centre;
|
||||
|
||||
line.Height = 0.6f + size.Y * 0.4f;
|
||||
line.Width = PointVisualisation.MAX_WIDTH * (0.6f + 0.4f * size.X);
|
||||
|
||||
line.Width = TICK_WIDTH * size.X;
|
||||
line.Height = size.Y;
|
||||
line.Colour = colour;
|
||||
}
|
||||
|
||||
@ -174,8 +171,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
Drawable getNextUsableLine()
|
||||
{
|
||||
PointVisualisation point;
|
||||
|
||||
if (drawableIndex >= Count)
|
||||
Add(point = new PointVisualisation(0));
|
||||
{
|
||||
Add(point = new PointVisualisation(0)
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
}
|
||||
else
|
||||
point = Children[drawableIndex];
|
||||
|
||||
|
@ -0,0 +1,161 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
/// <summary>
|
||||
/// The part of the timeline that displays the control points.
|
||||
/// </summary>
|
||||
public partial class TimelineTimingChangeDisplay : TimelinePart<TimelineTimingChangeDisplay.TimingPointPiece>
|
||||
{
|
||||
[Resolved]
|
||||
private Timeline timeline { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The visible time/position range of the timeline.
|
||||
/// </summary>
|
||||
private (float min, float max) visibleRange = (float.MinValue, float.MaxValue);
|
||||
|
||||
private readonly Cached groupCache = new Cached();
|
||||
|
||||
private ControlPointInfo controlPointInfo = null!;
|
||||
|
||||
protected override void LoadBeatmap(EditorBeatmap beatmap)
|
||||
{
|
||||
base.LoadBeatmap(beatmap);
|
||||
|
||||
beatmap.ControlPointInfo.ControlPointsChanged += () => groupCache.Invalidate();
|
||||
controlPointInfo = beatmap.ControlPointInfo;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (DrawWidth <= 0) return;
|
||||
|
||||
(float, float) newRange = (
|
||||
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - TimingPointPiece.WIDTH) / DrawWidth * Content.RelativeChildSize.X,
|
||||
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + TimingPointPiece.WIDTH) / DrawWidth * Content.RelativeChildSize.X);
|
||||
|
||||
if (visibleRange != newRange)
|
||||
{
|
||||
visibleRange = newRange;
|
||||
groupCache.Invalidate();
|
||||
}
|
||||
|
||||
if (!groupCache.IsValid)
|
||||
{
|
||||
recreateDrawableGroups();
|
||||
groupCache.Validate();
|
||||
}
|
||||
}
|
||||
|
||||
private void recreateDrawableGroups()
|
||||
{
|
||||
// Remove groups outside the visible range (or timing points which have since been removed from the beatmap).
|
||||
foreach (TimingPointPiece drawableGroup in this)
|
||||
{
|
||||
if (!controlPointInfo.TimingPoints.Contains(drawableGroup.Point) || !shouldBeVisible(drawableGroup.Point))
|
||||
drawableGroup.Expire();
|
||||
}
|
||||
|
||||
// Add remaining / new ones.
|
||||
foreach (TimingControlPoint t in controlPointInfo.TimingPoints)
|
||||
attemptAddTimingPoint(t);
|
||||
}
|
||||
|
||||
private void attemptAddTimingPoint(TimingControlPoint point)
|
||||
{
|
||||
if (!shouldBeVisible(point))
|
||||
return;
|
||||
|
||||
foreach (var child in this)
|
||||
{
|
||||
if (ReferenceEquals(child.Point, point))
|
||||
return;
|
||||
}
|
||||
|
||||
Add(new TimingPointPiece(point));
|
||||
}
|
||||
|
||||
private bool shouldBeVisible(TimingControlPoint point) => point.Time >= visibleRange.min && point.Time <= visibleRange.max;
|
||||
|
||||
public partial class TimingPointPiece : CompositeDrawable
|
||||
{
|
||||
public const float WIDTH = 16;
|
||||
|
||||
public readonly TimingControlPoint Point;
|
||||
|
||||
private readonly BindableNumber<double> beatLength;
|
||||
|
||||
protected OsuSpriteText Label { get; private set; } = null!;
|
||||
|
||||
public TimingPointPiece(TimingControlPoint timingPoint)
|
||||
{
|
||||
RelativePositionAxes = Axes.X;
|
||||
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
Width = WIDTH;
|
||||
|
||||
Origin = Anchor.TopRight;
|
||||
|
||||
Point = timingPoint;
|
||||
|
||||
beatLength = timingPoint.BeatLengthBindable.GetBoundCopy();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Point.GetRepresentingColour(colours),
|
||||
Masking = true,
|
||||
CornerRadius = TimelineTickDisplay.TICK_WIDTH / 2,
|
||||
Child = new Box
|
||||
{
|
||||
Colour = Color4.White,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
},
|
||||
Label = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Rotation = 90,
|
||||
Padding = new MarginPadding { Horizontal = 2 },
|
||||
Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold),
|
||||
}
|
||||
};
|
||||
|
||||
beatLength.BindValueChanged(beatLength =>
|
||||
{
|
||||
Label.Text = $"{60000 / beatLength.NewValue:n1} BPM";
|
||||
}, true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
X = (float)Point.Time;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
public partial class TimingPointPiece : TopPointPiece
|
||||
{
|
||||
private readonly BindableNumber<double> beatLength;
|
||||
|
||||
public TimingPointPiece(TimingControlPoint point)
|
||||
: base(point)
|
||||
{
|
||||
beatLength = point.BeatLengthBindable.GetBoundCopy();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
beatLength.BindValueChanged(beatLength =>
|
||||
{
|
||||
Label.Text = $"{60000 / beatLength.NewValue:n1} BPM";
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
public partial class TopPointPiece : CompositeDrawable
|
||||
{
|
||||
protected readonly ControlPoint Point;
|
||||
|
||||
protected OsuSpriteText Label { get; private set; } = null!;
|
||||
|
||||
public const float WIDTH = 80;
|
||||
|
||||
public TopPointPiece(ControlPoint point)
|
||||
{
|
||||
Point = point;
|
||||
Width = WIDTH;
|
||||
Height = 16;
|
||||
Margin = new MarginPadding { Vertical = 4 };
|
||||
|
||||
Origin = Anchor.TopCentre;
|
||||
Anchor = Anchor.TopCentre;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
const float corner_radius = 4;
|
||||
const float arrow_extension = 3;
|
||||
const float triangle_portion = 15;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
// This is a triangle, trust me.
|
||||
// Doing it this way looks okay. Doing it using Triangle primitive is basically impossible.
|
||||
new Container
|
||||
{
|
||||
Colour = Point.GetRepresentingColour(colours),
|
||||
X = -corner_radius,
|
||||
Size = new Vector2(triangle_portion * arrow_extension, Height),
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Masking = true,
|
||||
CornerRadius = Height,
|
||||
CornerExponent = 1.4f,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = Color4.White,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
}
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = WIDTH - triangle_portion,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Colour = Point.GetRepresentingColour(colours),
|
||||
Masking = true,
|
||||
CornerRadius = corner_radius,
|
||||
Child = new Box
|
||||
{
|
||||
Colour = Color4.White,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
},
|
||||
Label = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Padding = new MarginPadding(3),
|
||||
Font = OsuFont.Default.With(size: 14, weight: FontWeight.SemiBold),
|
||||
Colour = colours.B5,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -132,7 +132,7 @@ namespace osu.Game.Screens.Edit
|
||||
seekTime = timingPoint.Time + closestBeat * seekAmount;
|
||||
|
||||
// limit forward seeking to only up to the next timing point's start time.
|
||||
var nextTimingPoint = ControlPointInfo.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time);
|
||||
var nextTimingPoint = ControlPointInfo.TimingPointAfter(timingPoint.Time);
|
||||
if (seekTime > nextTimingPoint?.Time)
|
||||
seekTime = nextTimingPoint.Time;
|
||||
|
||||
|
@ -9,9 +9,9 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Timing
|
||||
@ -31,7 +31,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
private Bindable<ControlPointGroup?> selectedGroup { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colours)
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
@ -44,6 +44,26 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
Groups = { BindTarget = Beatmap.ControlPointInfo.Groups, },
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Margin = new MarginPadding(margins),
|
||||
Spacing = new Vector2(5),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new RoundedButton
|
||||
{
|
||||
Text = "Select closest to current time",
|
||||
Action = goToCurrentGroup,
|
||||
Size = new Vector2(220, 30),
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
},
|
||||
}
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.BottomRight,
|
||||
@ -60,6 +80,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
Action = delete,
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
BackgroundColour = colours.Red3,
|
||||
},
|
||||
addButton = new RoundedButton
|
||||
{
|
||||
@ -97,78 +118,18 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
base.Update();
|
||||
|
||||
trackActivePoint();
|
||||
|
||||
addButton.Enabled.Value = clock.CurrentTimeAccurate != selectedGroup.Value?.Time;
|
||||
}
|
||||
|
||||
private Type? trackedType;
|
||||
|
||||
/// <summary>
|
||||
/// Given the user has selected a control point group, we want to track any group which is
|
||||
/// active at the current point in time which matches the type the user has selected.
|
||||
///
|
||||
/// So if the user is currently looking at a timing point and seeks into the future, a
|
||||
/// future timing point would be automatically selected if it is now the new "current" point.
|
||||
/// </summary>
|
||||
private void trackActivePoint()
|
||||
{
|
||||
// For simplicity only match on the first type of the active control point.
|
||||
if (selectedGroup.Value == null)
|
||||
trackedType = null;
|
||||
else
|
||||
{
|
||||
switch (selectedGroup.Value.ControlPoints.Count)
|
||||
{
|
||||
// If the selected group has no control points, clear the tracked type.
|
||||
// Otherwise the user will be unable to select a group with no control points.
|
||||
case 0:
|
||||
trackedType = null;
|
||||
break;
|
||||
|
||||
// If the selected group only has one control point, update the tracking type.
|
||||
case 1:
|
||||
trackedType = selectedGroup.Value?.ControlPoints[0].GetType();
|
||||
break;
|
||||
|
||||
// If the selected group has more than one control point, choose the first as the tracking type
|
||||
// if we don't already have a singular tracked type.
|
||||
default:
|
||||
trackedType ??= selectedGroup.Value?.ControlPoints[0].GetType();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (trackedType != null)
|
||||
private void goToCurrentGroup()
|
||||
{
|
||||
double accurateTime = clock.CurrentTimeAccurate;
|
||||
|
||||
// We don't have an efficient way of looking up groups currently, only individual point types.
|
||||
// To improve the efficiency of this in the future, we should reconsider the overall structure of ControlPointInfo.
|
||||
var activeTimingPoint = Beatmap.ControlPointInfo.TimingPointAt(accurateTime);
|
||||
var activeEffectPoint = Beatmap.ControlPointInfo.EffectPointAt(accurateTime);
|
||||
|
||||
// Find the next group which has the same type as the selected one.
|
||||
ControlPointGroup? found = null;
|
||||
|
||||
for (int i = 0; i < Beatmap.ControlPointInfo.Groups.Count; i++)
|
||||
{
|
||||
var g = Beatmap.ControlPointInfo.Groups[i];
|
||||
|
||||
if (g.Time > accurateTime)
|
||||
continue;
|
||||
|
||||
for (int j = 0; j < g.ControlPoints.Count; j++)
|
||||
{
|
||||
if (g.ControlPoints[j].GetType() == trackedType)
|
||||
{
|
||||
found = g;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (found != null)
|
||||
selectedGroup.Value = found;
|
||||
}
|
||||
double latestActiveTime = Math.Max(activeTimingPoint.Time, activeEffectPoint.Time);
|
||||
selectedGroup.Value = Beatmap.ControlPointInfo.GroupAt(latestActiveTime);
|
||||
}
|
||||
|
||||
private void delete()
|
||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -27,10 +28,27 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
public BindableList<ControlPointGroup> Groups { get; } = new BindableList<ControlPointGroup>();
|
||||
|
||||
[Cached]
|
||||
private Bindable<TimingControlPoint?> activeTimingPoint { get; } = new Bindable<TimingControlPoint?>();
|
||||
|
||||
[Cached]
|
||||
private Bindable<EffectControlPoint?> activeEffectPoint { get; } = new Bindable<EffectControlPoint?>();
|
||||
|
||||
[Resolved]
|
||||
private EditorBeatmap beatmap { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private Bindable<ControlPointGroup?> selectedGroup { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private EditorClock editorClock { get; set; } = null!;
|
||||
|
||||
private const float timing_column_width = 300;
|
||||
private const float row_height = 25;
|
||||
private const float row_horizontal_padding = 20;
|
||||
|
||||
private ControlPointRowList list = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colours)
|
||||
{
|
||||
@ -65,7 +83,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Margin = new MarginPadding { Left = ControlPointTable.timing_column_width }
|
||||
Margin = new MarginPadding { Left = timing_column_width }
|
||||
},
|
||||
}
|
||||
},
|
||||
@ -73,7 +91,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Top = row_height },
|
||||
Child = new ControlPointRowList
|
||||
Child = list = new ControlPointRowList
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RowData = { BindTarget = Groups, },
|
||||
@ -82,28 +100,52 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
selectedGroup.BindValueChanged(_ => scrollToMostRelevantRow(force: true), true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
scrollToMostRelevantRow(force: false);
|
||||
}
|
||||
|
||||
private void scrollToMostRelevantRow(bool force)
|
||||
{
|
||||
double accurateTime = editorClock.CurrentTimeAccurate;
|
||||
|
||||
activeTimingPoint.Value = beatmap.ControlPointInfo.TimingPointAt(accurateTime);
|
||||
activeEffectPoint.Value = beatmap.ControlPointInfo.EffectPointAt(accurateTime);
|
||||
|
||||
double latestActiveTime = Math.Max(activeTimingPoint.Value?.Time ?? double.NegativeInfinity, activeEffectPoint.Value?.Time ?? double.NegativeInfinity);
|
||||
var groupToShow = selectedGroup.Value ?? beatmap.ControlPointInfo.GroupAt(latestActiveTime);
|
||||
list.ScrollTo(groupToShow, force);
|
||||
}
|
||||
|
||||
private partial class ControlPointRowList : VirtualisedListContainer<ControlPointGroup, DrawableControlGroup>
|
||||
{
|
||||
[Resolved]
|
||||
private Bindable<ControlPointGroup?> selectedGroup { get; set; } = null!;
|
||||
|
||||
public ControlPointRowList()
|
||||
: base(row_height, 50)
|
||||
{
|
||||
}
|
||||
|
||||
protected override ScrollContainer<Drawable> CreateScrollContainer() => new OsuScrollContainer();
|
||||
protected override ScrollContainer<Drawable> CreateScrollContainer() => new UserTrackingScrollContainer();
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
protected new UserTrackingScrollContainer Scroll => (UserTrackingScrollContainer)base.Scroll;
|
||||
|
||||
selectedGroup.BindValueChanged(val =>
|
||||
public void ScrollTo(ControlPointGroup group, bool force)
|
||||
{
|
||||
if (Scroll.UserScrolling && !force)
|
||||
return;
|
||||
|
||||
// can't use `.ScrollIntoView()` here because of the list virtualisation not giving
|
||||
// child items valid coordinates from the start, so ballpark something similar
|
||||
// using estimated row height.
|
||||
var row = Items.FlowingChildren.SingleOrDefault(item => item.Row.Equals(val.NewValue));
|
||||
var row = Items.FlowingChildren.SingleOrDefault(item => item.Row.Equals(group));
|
||||
|
||||
if (row == null)
|
||||
return;
|
||||
@ -115,7 +157,6 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
Scroll.ScrollTo(minPos);
|
||||
else if (maxPos > Scroll.Current + Scroll.DisplayableContent)
|
||||
Scroll.ScrollTo(maxPos - Scroll.DisplayableContent);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,13 +171,23 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
private readonly BindableWithCurrent<ControlPointGroup> current = new BindableWithCurrent<ControlPointGroup>();
|
||||
|
||||
private Box background = null!;
|
||||
private Box currentIndicator = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private Bindable<ControlPointGroup?> selectedGroup { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private Bindable<TimingControlPoint?> activeTimingPoint { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private Bindable<EffectControlPoint?> activeEffectPoint { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private EditorClock editorClock { get; set; } = null!;
|
||||
|
||||
@ -153,6 +204,12 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
Colour = colourProvider.Background1,
|
||||
Alpha = 0,
|
||||
},
|
||||
currentIndicator = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = 5,
|
||||
Alpha = 0,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@ -174,7 +231,9 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
selectedGroup.BindValueChanged(_ => updateState(), true);
|
||||
selectedGroup.BindValueChanged(_ => updateState());
|
||||
activeEffectPoint.BindValueChanged(_ => updateState());
|
||||
activeTimingPoint.BindValueChanged(_ => updateState(), true);
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
@ -213,12 +272,31 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
bool isSelected = selectedGroup.Value?.Equals(current.Value) == true;
|
||||
|
||||
bool hasCurrentTimingPoint = activeTimingPoint.Value != null && current.Value.ControlPoints.Contains(activeTimingPoint.Value);
|
||||
bool hasCurrentEffectPoint = activeEffectPoint.Value != null && current.Value.ControlPoints.Contains(activeEffectPoint.Value);
|
||||
|
||||
if (IsHovered || isSelected)
|
||||
background.FadeIn(100, Easing.OutQuint);
|
||||
else if (hasCurrentTimingPoint || hasCurrentEffectPoint)
|
||||
background.FadeTo(0.2f, 100, Easing.OutQuint);
|
||||
else
|
||||
background.FadeOut(100, Easing.OutQuint);
|
||||
|
||||
background.Colour = isSelected ? colourProvider.Colour3 : colourProvider.Background1;
|
||||
|
||||
if (hasCurrentTimingPoint || hasCurrentEffectPoint)
|
||||
{
|
||||
currentIndicator.FadeIn(100, Easing.OutQuint);
|
||||
|
||||
if (hasCurrentTimingPoint && hasCurrentEffectPoint)
|
||||
currentIndicator.Colour = ColourInfo.GradientVertical(activeTimingPoint.Value!.GetRepresentingColour(colours), activeEffectPoint.Value!.GetRepresentingColour(colours));
|
||||
else if (hasCurrentTimingPoint)
|
||||
currentIndicator.Colour = activeTimingPoint.Value!.GetRepresentingColour(colours);
|
||||
else
|
||||
currentIndicator.Colour = activeEffectPoint.Value!.GetRepresentingColour(colours);
|
||||
}
|
||||
else
|
||||
currentIndicator.FadeOut(100, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,9 +5,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
@ -38,8 +36,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
kiai.Current.BindValueChanged(_ => saveChanges());
|
||||
scrollSpeedSlider.Current.BindValueChanged(_ => saveChanges());
|
||||
|
||||
var drawableRuleset = Beatmap.BeatmapInfo.Ruleset.CreateInstance().CreateDrawableRulesetWith(Beatmap.PlayableBeatmap);
|
||||
if (drawableRuleset is not IDrawableScrollingRuleset scrollingRuleset || scrollingRuleset.VisualisationMethod == ScrollVisualisationMethod.Constant)
|
||||
if (!Beatmap.BeatmapInfo.Ruleset.CreateInstance().EditorShowScrollSpeed)
|
||||
scrollSpeedSlider.Hide();
|
||||
|
||||
void saveChanges()
|
||||
|
@ -15,6 +15,10 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes
|
||||
|
||||
private AttributeText kiaiModeBubble = null!;
|
||||
private AttributeText text = null!;
|
||||
private AttributeProgressBar progressBar = null!;
|
||||
|
||||
[Resolved]
|
||||
protected EditorBeatmap Beatmap { get; private set; } = null!;
|
||||
|
||||
public EffectRowAttribute(EffectControlPoint effect)
|
||||
: base(effect, "effect")
|
||||
@ -28,7 +32,7 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes
|
||||
{
|
||||
Content.AddRange(new Drawable[]
|
||||
{
|
||||
new AttributeProgressBar(Point)
|
||||
progressBar = new AttributeProgressBar(Point)
|
||||
{
|
||||
Current = scrollSpeed,
|
||||
},
|
||||
@ -36,6 +40,12 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes
|
||||
kiaiModeBubble = new AttributeText(Point) { Text = "kiai" },
|
||||
});
|
||||
|
||||
if (!Beatmap.BeatmapInfo.Ruleset.CreateInstance().EditorShowScrollSpeed)
|
||||
{
|
||||
text.Hide();
|
||||
progressBar.Hide();
|
||||
}
|
||||
|
||||
kiaiMode.BindValueChanged(enabled => kiaiModeBubble.FadeTo(enabled.NewValue ? 1 : 0), true);
|
||||
scrollSpeed.BindValueChanged(_ => updateText(), true);
|
||||
}
|
||||
|
@ -21,9 +21,11 @@ namespace osu.Game.Screens.Menu
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChild = spewer = new StarFountainSpewer();
|
||||
InternalChild = spewer = CreateSpewer();
|
||||
}
|
||||
|
||||
protected virtual StarFountainSpewer CreateSpewer() => new StarFountainSpewer();
|
||||
|
||||
public void Shoot(int direction) => spewer.Shoot(direction);
|
||||
|
||||
protected override void SkinChanged(ISkinSource skin)
|
||||
@ -38,17 +40,23 @@ namespace osu.Game.Screens.Menu
|
||||
private const int particle_duration_max = 1000;
|
||||
|
||||
private double? lastShootTime;
|
||||
private int lastShootDirection;
|
||||
|
||||
protected int LastShootDirection { get; private set; }
|
||||
|
||||
protected override float ParticleGravity => 800;
|
||||
|
||||
private const double shoot_duration = 800;
|
||||
protected virtual double ShootDuration => 800;
|
||||
|
||||
[Resolved]
|
||||
private ISkinSource skin { get; set; } = null!;
|
||||
|
||||
public StarFountainSpewer()
|
||||
: base(null, 240, particle_duration_max)
|
||||
: this(240)
|
||||
{
|
||||
}
|
||||
|
||||
protected StarFountainSpewer(int perSecond)
|
||||
: base(null, perSecond, particle_duration_max)
|
||||
{
|
||||
}
|
||||
|
||||
@ -67,16 +75,16 @@ namespace osu.Game.Screens.Menu
|
||||
StartAngle = getRandomVariance(4),
|
||||
EndAngle = getRandomVariance(2),
|
||||
EndScale = 2.2f + getRandomVariance(0.4f),
|
||||
Velocity = new Vector2(getCurrentAngle(), -1400 + getRandomVariance(100)),
|
||||
Velocity = new Vector2(GetCurrentAngle(), -1400 + getRandomVariance(100)),
|
||||
};
|
||||
}
|
||||
|
||||
private float getCurrentAngle()
|
||||
protected virtual float GetCurrentAngle()
|
||||
{
|
||||
const float x_velocity_from_direction = 500;
|
||||
const float x_velocity_random_variance = 60;
|
||||
const float x_velocity_from_direction = 500;
|
||||
|
||||
return lastShootDirection * x_velocity_from_direction * (float)(1 - 2 * (Clock.CurrentTime - lastShootTime!.Value) / shoot_duration) + getRandomVariance(x_velocity_random_variance);
|
||||
return LastShootDirection * x_velocity_from_direction * (float)(1 - 2 * (Clock.CurrentTime - lastShootTime!.Value) / ShootDuration) + getRandomVariance(x_velocity_random_variance);
|
||||
}
|
||||
|
||||
private ScheduledDelegate? deactivateDelegate;
|
||||
@ -86,10 +94,10 @@ namespace osu.Game.Screens.Menu
|
||||
Active.Value = true;
|
||||
|
||||
deactivateDelegate?.Cancel();
|
||||
deactivateDelegate = Scheduler.AddDelayed(() => Active.Value = false, shoot_duration);
|
||||
deactivateDelegate = Scheduler.AddDelayed(() => Active.Value = false, ShootDuration);
|
||||
|
||||
lastShootTime = Clock.CurrentTime;
|
||||
lastShootDirection = direction;
|
||||
LastShootDirection = direction;
|
||||
}
|
||||
|
||||
private static float getRandomVariance(float variance) => RNG.NextSingle(-variance, variance);
|
||||
|
@ -93,14 +93,15 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
protected override BackgroundScreen CreateBackground() => new DailyChallengeIntroBackgroundScreen(colourProvider);
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(BeatmapDifficultyCache difficultyCache, BeatmapModelDownloader beatmapDownloader, OsuConfigManager config, AudioManager audio)
|
||||
private void load(RulesetStore rulesets, BeatmapDifficultyCache difficultyCache, BeatmapModelDownloader beatmapDownloader, OsuConfigManager config, AudioManager audio)
|
||||
{
|
||||
const float horizontal_info_size = 500f;
|
||||
|
||||
Ruleset ruleset = Ruleset.Value.CreateInstance();
|
||||
|
||||
StarRatingDisplay starRatingDisplay;
|
||||
|
||||
IBeatmapInfo beatmap = item.Beatmap;
|
||||
Ruleset ruleset = rulesets.GetRuleset(item.Beatmap.Ruleset.ShortName)!.CreateInstance();
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
beatmapAvailabilityTracker,
|
||||
@ -242,13 +243,13 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
Origin = Anchor.TopCentre,
|
||||
Shear = new Vector2(-OsuGame.SHEAR, 0f),
|
||||
MaxWidth = horizontal_info_size,
|
||||
Text = item.Beatmap.BeatmapSet!.Metadata.GetDisplayTitleRomanisable(false),
|
||||
Text = beatmap.BeatmapSet!.Metadata.GetDisplayTitleRomanisable(false),
|
||||
Padding = new MarginPadding { Horizontal = 5f },
|
||||
Font = OsuFont.GetFont(size: 26),
|
||||
},
|
||||
new TruncatingSpriteText
|
||||
{
|
||||
Text = $"Difficulty: {item.Beatmap.DifficultyName}",
|
||||
Text = $"Difficulty: {beatmap.DifficultyName}",
|
||||
Font = OsuFont.GetFont(size: 20, italics: true),
|
||||
MaxWidth = horizontal_info_size,
|
||||
Shear = new Vector2(-OsuGame.SHEAR, 0f),
|
||||
@ -257,7 +258,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
},
|
||||
new TruncatingSpriteText
|
||||
{
|
||||
Text = $"by {item.Beatmap.Metadata.Author.Username}",
|
||||
Text = $"by {beatmap.Metadata.Author.Username}",
|
||||
Font = OsuFont.GetFont(size: 16, italics: true),
|
||||
MaxWidth = horizontal_info_size,
|
||||
Shear = new Vector2(-OsuGame.SHEAR, 0f),
|
||||
@ -309,14 +310,14 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
}
|
||||
};
|
||||
|
||||
starDifficulty = difficultyCache.GetBindableDifficulty(item.Beatmap);
|
||||
starDifficulty = difficultyCache.GetBindableDifficulty(beatmap);
|
||||
starDifficulty.BindValueChanged(star =>
|
||||
{
|
||||
if (star.NewValue != null)
|
||||
starRatingDisplay.Current.Value = star.NewValue.Value;
|
||||
}, true);
|
||||
|
||||
LoadComponentAsync(new OnlineBeatmapSetCover(item.Beatmap.BeatmapSet as IBeatmapSetOnlineInfo)
|
||||
LoadComponentAsync(new OnlineBeatmapSetCover(beatmap.BeatmapSet as IBeatmapSetOnlineInfo)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
@ -334,8 +335,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
|
||||
if (config.Get<bool>(OsuSetting.AutomaticallyDownloadMissingBeatmaps))
|
||||
{
|
||||
if (!beatmapManager.IsAvailableLocally(new BeatmapSetInfo { OnlineID = item.Beatmap.BeatmapSet!.OnlineID }))
|
||||
beatmapDownloader.Download(item.Beatmap.BeatmapSet!, config.Get<bool>(OsuSetting.PreferNoVideo));
|
||||
if (!beatmapManager.IsAvailableLocally(new BeatmapSetInfo { OnlineID = beatmap.BeatmapSet!.OnlineID }))
|
||||
beatmapDownloader.Download(beatmap.BeatmapSet!, config.Get<bool>(OsuSetting.PreferNoVideo));
|
||||
}
|
||||
|
||||
dateWindupSample = audio.Samples.Get(@"DailyChallenge/date-windup");
|
||||
|
@ -11,12 +11,12 @@ namespace osu.Game.Screens.Play.Break
|
||||
{
|
||||
public partial class LetterboxOverlay : CompositeDrawable
|
||||
{
|
||||
private const int height = 350;
|
||||
|
||||
private static readonly Color4 transparent_black = new Color4(0, 0, 0, 0);
|
||||
|
||||
public LetterboxOverlay()
|
||||
{
|
||||
const int height = 150;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
|
@ -1,16 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play.Break;
|
||||
@ -29,7 +30,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private readonly Container fadeContainer;
|
||||
|
||||
private IReadOnlyList<BreakPeriod> breaks;
|
||||
private IReadOnlyList<BreakPeriod> breaks = Array.Empty<BreakPeriod>();
|
||||
|
||||
public IReadOnlyList<BreakPeriod> Breaks
|
||||
{
|
||||
@ -69,6 +70,30 @@ namespace osu.Game.Screens.Play
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
new CircularContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 80,
|
||||
Height = 4,
|
||||
Masking = true,
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Radius = 260,
|
||||
Colour = OsuColour.Gray(0.2f).Opacity(0.8f),
|
||||
Roundness = 12
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Alpha = 0,
|
||||
AlwaysPresent = true,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
}
|
||||
},
|
||||
remainingTimeAdjustmentBox = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
@ -111,12 +136,9 @@ namespace osu.Game.Screens.Play
|
||||
base.LoadComplete();
|
||||
initializeBreaks();
|
||||
|
||||
if (scoreProcessor != null)
|
||||
{
|
||||
info.AccuracyDisplay.Current.BindTo(scoreProcessor.Accuracy);
|
||||
((IBindable<ScoreRank>)info.GradeDisplay.Current).BindTo(scoreProcessor.Rank);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
@ -130,8 +152,6 @@ namespace osu.Game.Screens.Play
|
||||
FinishTransforms(true);
|
||||
Scheduler.CancelDelayedTasks();
|
||||
|
||||
if (breaks == null) return; // we need breaks.
|
||||
|
||||
foreach (var b in breaks)
|
||||
{
|
||||
if (!b.HasEffect)
|
||||
|
@ -95,10 +95,10 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private readonly BindableBool holdingForHUD = new BindableBool();
|
||||
|
||||
private readonly SkinComponentsContainer mainComponents;
|
||||
private readonly SkinnableContainer mainComponents;
|
||||
|
||||
[CanBeNull]
|
||||
private readonly SkinComponentsContainer rulesetComponents;
|
||||
private readonly SkinnableContainer rulesetComponents;
|
||||
|
||||
/// <summary>
|
||||
/// A flow which sits at the left side of the screen to house leaderboard (and related) components.
|
||||
@ -109,7 +109,7 @@ namespace osu.Game.Screens.Play
|
||||
private readonly List<Drawable> hideTargets;
|
||||
|
||||
/// <summary>
|
||||
/// The container for skin components attached to <see cref="SkinComponentsContainerLookup.TargetArea.Playfield"/>
|
||||
/// The container for skin components attached to <see cref="GlobalSkinnableContainers.Playfield"/>
|
||||
/// </summary>
|
||||
internal readonly Drawable PlayfieldSkinLayer;
|
||||
|
||||
@ -132,7 +132,7 @@ namespace osu.Game.Screens.Play
|
||||
? (rulesetComponents = new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, })
|
||||
: Empty(),
|
||||
PlayfieldSkinLayer = drawableRuleset != null
|
||||
? new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.Playfield, drawableRuleset.Ruleset.RulesetInfo)) { AlwaysPresent = true, }
|
||||
? new SkinnableContainer(new GlobalSkinnableContainerLookup(GlobalSkinnableContainers.Playfield, drawableRuleset.Ruleset.RulesetInfo)) { AlwaysPresent = true, }
|
||||
: Empty(),
|
||||
topRightElements = new FillFlowContainer
|
||||
{
|
||||
@ -280,7 +280,7 @@ namespace osu.Game.Screens.Play
|
||||
else
|
||||
bottomRightElements.Y = 0;
|
||||
|
||||
void processDrawables(SkinComponentsContainer components)
|
||||
void processDrawables(SkinnableContainer components)
|
||||
{
|
||||
// Avoid using foreach due to missing GetEnumerator implementation.
|
||||
// See https://github.com/ppy/osu-framework/blob/e10051e6643731e393b09de40a3a3d209a545031/osu.Framework/Bindables/IBindableList.cs#L41-L44.
|
||||
@ -440,7 +440,7 @@ namespace osu.Game.Screens.Play
|
||||
}
|
||||
}
|
||||
|
||||
private partial class HUDComponentsContainer : SkinComponentsContainer
|
||||
private partial class HUDComponentsContainer : SkinnableContainer
|
||||
{
|
||||
private Bindable<ScoringMode> scoringMode;
|
||||
|
||||
@ -448,7 +448,7 @@ namespace osu.Game.Screens.Play
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
||||
public HUDComponentsContainer([CanBeNull] RulesetInfo ruleset = null)
|
||||
: base(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.MainHUDComponents, ruleset))
|
||||
: base(new GlobalSkinnableContainerLookup(GlobalSkinnableContainers.MainHUDComponents, ruleset))
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
94
osu.Game/Screens/Play/KiaiGameplayFountains.cs
Normal file
94
osu.Game/Screens/Play/KiaiGameplayFountains.cs
Normal file
@ -0,0 +1,94 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Screens.Menu;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
public partial class KiaiGameplayFountains : BeatSyncedContainer
|
||||
{
|
||||
private StarFountain leftFountain = null!;
|
||||
private StarFountain rightFountain = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
Children = new[]
|
||||
{
|
||||
leftFountain = new GameplayStarFountain
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
X = 75,
|
||||
},
|
||||
rightFountain = new GameplayStarFountain
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
X = -75,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private bool isTriggered;
|
||||
|
||||
private double? lastTrigger;
|
||||
|
||||
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
|
||||
{
|
||||
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
|
||||
|
||||
if (effectPoint.KiaiMode && !isTriggered)
|
||||
{
|
||||
bool isNearEffectPoint = Math.Abs(BeatSyncSource.Clock.CurrentTime - effectPoint.Time) < 500;
|
||||
if (isNearEffectPoint)
|
||||
Shoot();
|
||||
}
|
||||
|
||||
isTriggered = effectPoint.KiaiMode;
|
||||
}
|
||||
|
||||
public void Shoot()
|
||||
{
|
||||
if (lastTrigger != null && Clock.CurrentTime - lastTrigger < 500)
|
||||
return;
|
||||
|
||||
leftFountain.Shoot(1);
|
||||
rightFountain.Shoot(-1);
|
||||
lastTrigger = Clock.CurrentTime;
|
||||
}
|
||||
|
||||
public partial class GameplayStarFountain : StarFountain
|
||||
{
|
||||
protected override StarFountainSpewer CreateSpewer() => new GameplayStarFountainSpewer();
|
||||
|
||||
private partial class GameplayStarFountainSpewer : StarFountainSpewer
|
||||
{
|
||||
protected override double ShootDuration => 400;
|
||||
|
||||
public GameplayStarFountainSpewer()
|
||||
: base(perSecond: 180)
|
||||
{
|
||||
}
|
||||
|
||||
protected override float GetCurrentAngle()
|
||||
{
|
||||
const float x_velocity_from_direction = 450;
|
||||
const float x_velocity_to_direction = 600;
|
||||
|
||||
return LastShootDirection * RNG.NextSingle(x_velocity_from_direction, x_velocity_to_direction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -405,8 +405,20 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart);
|
||||
|
||||
private Drawable createUnderlayComponents() =>
|
||||
DimmableStoryboard = new DimmableStoryboard(GameplayState.Storyboard, GameplayState.Mods) { RelativeSizeAxes = Axes.Both };
|
||||
private Drawable createUnderlayComponents()
|
||||
{
|
||||
var container = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
DimmableStoryboard = new DimmableStoryboard(GameplayState.Storyboard, GameplayState.Mods) { RelativeSizeAxes = Axes.Both },
|
||||
new KiaiGameplayFountains(),
|
||||
},
|
||||
};
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
private Drawable createGameplayComponents(IWorkingBeatmap working) => new ScalingContainer(ScalingMode.Gameplay)
|
||||
{
|
||||
@ -434,15 +446,6 @@ namespace osu.Game.Screens.Play
|
||||
Children = new[]
|
||||
{
|
||||
DimmableStoryboard.OverlayLayerContainer.CreateProxy(),
|
||||
BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor)
|
||||
{
|
||||
Clock = DrawableRuleset.FrameStableClock,
|
||||
ProcessCustomClock = false,
|
||||
Breaks = working.Beatmap.Breaks
|
||||
},
|
||||
// display the cursor above some HUD elements.
|
||||
DrawableRuleset.Cursor?.CreateProxy() ?? new Container(),
|
||||
DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(),
|
||||
HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods, Configuration.AlwaysShowLeaderboard)
|
||||
{
|
||||
HoldToQuit =
|
||||
@ -461,6 +464,14 @@ namespace osu.Game.Screens.Play
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
},
|
||||
BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor)
|
||||
{
|
||||
Clock = DrawableRuleset.FrameStableClock,
|
||||
ProcessCustomClock = false,
|
||||
Breaks = working.Beatmap.Breaks
|
||||
},
|
||||
// display the cursor above some HUD elements.
|
||||
DrawableRuleset.Cursor?.CreateProxy() ?? new Container(),
|
||||
skipIntroOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime)
|
||||
{
|
||||
RequestSkip = performUserRequestedSkip
|
||||
@ -470,6 +481,7 @@ namespace osu.Game.Screens.Play
|
||||
RequestSkip = () => progressToResults(false),
|
||||
Alpha = 0
|
||||
},
|
||||
DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(),
|
||||
PauseOverlay = new PauseOverlay
|
||||
{
|
||||
OnResume = Resume,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user