mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 09:27:29 +08:00
Merge branch 'master' into free-sliders
This commit is contained in:
commit
1b26e1c126
@ -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();
|
||||
}
|
||||
|
@ -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 =>
|
||||
{
|
||||
@ -56,10 +52,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
{
|
||||
// set the anchor to top right so that it won't squash to the return button to the top
|
||||
keyCounter.Anchor = Anchor.CentreRight;
|
||||
keyCounter.Origin = Anchor.CentreRight;
|
||||
keyCounter.X = 0;
|
||||
// 340px is the default height inherit from stable
|
||||
keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y;
|
||||
keyCounter.Origin = Anchor.TopRight;
|
||||
keyCounter.Position = new Vector2(0, -40) * 1.6f;
|
||||
}
|
||||
})
|
||||
{
|
||||
|
@ -110,9 +110,9 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
if (Catcher.Dashing || Catcher.HyperDashing)
|
||||
{
|
||||
double generationInterval = Catcher.HyperDashing ? 25 : 50;
|
||||
const double trail_generation_interval = 16;
|
||||
|
||||
if (Time.Current - catcherTrails.LastDashTrailTime >= generationInterval)
|
||||
if (Time.Current - catcherTrails.LastDashTrailTime >= trail_generation_interval)
|
||||
displayCatcherTrail(Catcher.HyperDashing ? CatcherTrailAnimation.HyperDashing : CatcherTrailAnimation.Dashing);
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
public abstract partial class ManiaSkinnableTestScene : SkinnableTestScene
|
||||
{
|
||||
[Cached(Type = typeof(IScrollingInfo))]
|
||||
private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo();
|
||||
protected readonly TestScrollingInfo ScrollingInfo = new TestScrollingInfo();
|
||||
|
||||
[Cached]
|
||||
private readonly StageDefinition stage = new StageDefinition(4);
|
||||
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
|
||||
protected ManiaSkinnableTestScene()
|
||||
{
|
||||
scrollingInfo.Direction.Value = ScrollingDirection.Down;
|
||||
ScrollingInfo.Direction.Value = ScrollingDirection.Down;
|
||||
|
||||
Add(new Box
|
||||
{
|
||||
@ -43,16 +43,16 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
[Test]
|
||||
public void TestScrollingDown()
|
||||
{
|
||||
AddStep("change direction to down", () => scrollingInfo.Direction.Value = ScrollingDirection.Down);
|
||||
AddStep("change direction to down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScrollingUp()
|
||||
{
|
||||
AddStep("change direction to up", () => scrollingInfo.Direction.Value = ScrollingDirection.Up);
|
||||
AddStep("change direction to up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
|
||||
}
|
||||
|
||||
private class TestScrollingInfo : IScrollingInfo
|
||||
protected class TestScrollingInfo : IScrollingInfo
|
||||
{
|
||||
public readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
|
@ -1,13 +1,17 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Argon;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Legacy;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
@ -17,22 +21,75 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
[Cached]
|
||||
private ScoreProcessor scoreProcessor = new ScoreProcessor(new ManiaRuleset());
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
[Test]
|
||||
public void TestDisplay()
|
||||
{
|
||||
AddStep("setup", () => SetContents(s =>
|
||||
{
|
||||
if (s is ArgonSkin)
|
||||
return new ArgonManiaComboCounter();
|
||||
|
||||
if (s is LegacySkin)
|
||||
return new LegacyManiaComboCounter();
|
||||
|
||||
return new LegacyManiaComboCounter();
|
||||
}));
|
||||
|
||||
setup(Anchor.Centre);
|
||||
AddRepeatStep("perform hit", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Great }), 20);
|
||||
AddStep("perform miss", () => scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new Judgement()) { Type = HitResult.Miss }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAnchorOrigin()
|
||||
{
|
||||
AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
|
||||
setup(Anchor.TopCentre, 20);
|
||||
AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
|
||||
check(Anchor.BottomCentre, -20);
|
||||
|
||||
AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
|
||||
setup(Anchor.BottomCentre, -20);
|
||||
AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
|
||||
check(Anchor.TopCentre, 20);
|
||||
|
||||
AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
|
||||
setup(Anchor.Centre, 20);
|
||||
AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
|
||||
check(Anchor.Centre, 20);
|
||||
|
||||
AddStep("set direction up", () => ScrollingInfo.Direction.Value = ScrollingDirection.Up);
|
||||
setup(Anchor.Centre, -20);
|
||||
AddStep("set direction down", () => ScrollingInfo.Direction.Value = ScrollingDirection.Down);
|
||||
check(Anchor.Centre, -20);
|
||||
}
|
||||
|
||||
private void setup(Anchor anchor, float y = 0)
|
||||
{
|
||||
AddStep($"setup {anchor} {y}", () => SetContents(s =>
|
||||
{
|
||||
var container = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
|
||||
if (s is ArgonSkin)
|
||||
container.Add(new ArgonManiaComboCounter());
|
||||
else if (s is LegacySkin)
|
||||
container.Add(new LegacyManiaComboCounter());
|
||||
else
|
||||
container.Add(new LegacyManiaComboCounter());
|
||||
|
||||
container.Child.Anchor = anchor;
|
||||
container.Child.Origin = Anchor.Centre;
|
||||
container.Child.Y = y;
|
||||
|
||||
return container;
|
||||
}));
|
||||
}
|
||||
|
||||
private void check(Anchor anchor, float y)
|
||||
{
|
||||
AddAssert($"check {anchor} {y}", () =>
|
||||
{
|
||||
foreach (var combo in this.ChildrenOfType<ISerialisableDrawable>())
|
||||
{
|
||||
var drawableCombo = (Drawable)combo;
|
||||
if (drawableCombo.Anchor != anchor || drawableCombo.Y != y)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"/>.
|
||||
|
@ -38,11 +38,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
private void updateAnchor()
|
||||
{
|
||||
// if the anchor isn't a vertical center, set top or bottom anchor based on scroll direction
|
||||
if (!Anchor.HasFlag(Anchor.y1))
|
||||
{
|
||||
Anchor &= ~(Anchor.y0 | Anchor.y2);
|
||||
Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0;
|
||||
}
|
||||
if (Anchor.HasFlag(Anchor.y1))
|
||||
return;
|
||||
|
||||
Anchor &= ~(Anchor.y0 | Anchor.y2);
|
||||
Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0;
|
||||
|
||||
// change the sign of the Y coordinate in line with the scrolling direction.
|
||||
// i.e. if the user changes direction from down to up, the anchor is changed from top to bottom, and the Y is flipped from positive to negative here.
|
||||
|
@ -28,18 +28,14 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
{
|
||||
switch (lookup)
|
||||
{
|
||||
case SkinComponentsContainerLookup containerLookup:
|
||||
case GlobalSkinnableContainerLookup containerLookup:
|
||||
// Only handle per ruleset defaults here.
|
||||
if (containerLookup.Ruleset == null)
|
||||
return base.GetDrawableComponent(lookup);
|
||||
|
||||
// Skin has configuration.
|
||||
if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d)
|
||||
return d;
|
||||
|
||||
switch (containerLookup.Target)
|
||||
switch (containerLookup.Lookup)
|
||||
{
|
||||
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
|
||||
case GlobalSkinnableContainers.MainHUDComponents:
|
||||
return new DefaultSkinComponentsContainer(container =>
|
||||
{
|
||||
var combo = container.ChildrenOfType<ArgonManiaComboCounter>().FirstOrDefault();
|
||||
@ -59,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
|
||||
return null;
|
||||
|
||||
case GameplaySkinComponentLookup<HitResult> resultComponent:
|
||||
case SkinComponentLookup<HitResult> resultComponent:
|
||||
// This should eventually be moved to a skin setting, when supported.
|
||||
if (Skin is ArgonProSkin && resultComponent.Component >= HitResult.Great)
|
||||
return Drawable.Empty();
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -44,16 +45,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
private void updateAnchor()
|
||||
{
|
||||
// if the anchor isn't a vertical center, set top or bottom anchor based on scroll direction
|
||||
if (!Anchor.HasFlag(Anchor.y1))
|
||||
{
|
||||
Anchor &= ~(Anchor.y0 | Anchor.y2);
|
||||
Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0;
|
||||
}
|
||||
if (Anchor.HasFlag(Anchor.y1))
|
||||
return;
|
||||
|
||||
// since we flip the vertical anchor when changing scroll direction,
|
||||
// we can use the sign of the Y value as an indicator to make the combo counter displayed correctly.
|
||||
if ((Y < 0 && direction.Value == ScrollingDirection.Down) || (Y > 0 && direction.Value == ScrollingDirection.Up))
|
||||
Y = -Y;
|
||||
Anchor &= ~(Anchor.y0 | Anchor.y2);
|
||||
Anchor |= direction.Value == ScrollingDirection.Up ? Anchor.y2 : Anchor.y0;
|
||||
|
||||
// change the sign of the Y coordinate in line with the scrolling direction.
|
||||
// i.e. if the user changes direction from down to up, the anchor is changed from top to bottom, and the Y is flipped from positive to negative here.
|
||||
Y = Math.Abs(Y) * (direction.Value == ScrollingDirection.Up ? -1 : 1);
|
||||
}
|
||||
|
||||
protected override void OnCountIncrement()
|
||||
|
@ -80,22 +80,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
{
|
||||
switch (lookup)
|
||||
{
|
||||
case SkinComponentsContainerLookup containerLookup:
|
||||
case GlobalSkinnableContainerLookup containerLookup:
|
||||
// Modifications for global components.
|
||||
if (containerLookup.Ruleset == null)
|
||||
return base.GetDrawableComponent(lookup);
|
||||
|
||||
// Skin has configuration.
|
||||
if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d)
|
||||
return d;
|
||||
|
||||
// we don't have enough assets to display these components (this is especially the case on a "beatmap" skin).
|
||||
if (!IsProvidingLegacyResources)
|
||||
return null;
|
||||
|
||||
switch (containerLookup.Target)
|
||||
switch (containerLookup.Lookup)
|
||||
{
|
||||
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
|
||||
case GlobalSkinnableContainers.MainHUDComponents:
|
||||
return new DefaultSkinComponentsContainer(container =>
|
||||
{
|
||||
var combo = container.ChildrenOfType<LegacyManiaComboCounter>().FirstOrDefault();
|
||||
@ -114,7 +110,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
|
||||
return null;
|
||||
|
||||
case GameplaySkinComponentLookup<HitResult> resultComponent:
|
||||
case SkinComponentLookup<HitResult> resultComponent:
|
||||
return getResult(resultComponent.Component);
|
||||
|
||||
case ManiaSkinComponentLookup maniaComponent:
|
||||
|
@ -24,24 +24,24 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
[Test]
|
||||
public void TestGridToggles()
|
||||
{
|
||||
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
|
||||
AddStep("enable distance snap grid", () => InputManager.Key(Key.Y));
|
||||
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||
|
||||
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||
gridActive<RectangularPositionSnapGrid>(false);
|
||||
|
||||
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
|
||||
AddStep("enable rectangular grid", () => InputManager.Key(Key.T));
|
||||
|
||||
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||
AddUntilStep("distance snap grid still visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||
gridActive<RectangularPositionSnapGrid>(true);
|
||||
|
||||
AddStep("disable distance snap grid", () => InputManager.Key(Key.T));
|
||||
AddStep("disable distance snap grid", () => InputManager.Key(Key.Y));
|
||||
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||
gridActive<RectangularPositionSnapGrid>(true);
|
||||
|
||||
AddStep("disable rectangular grid", () => InputManager.Key(Key.Y));
|
||||
AddStep("disable rectangular grid", () => InputManager.Key(Key.T));
|
||||
AddUntilStep("distance snap grid still hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||
gridActive<RectangularPositionSnapGrid>(false);
|
||||
}
|
||||
@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
|
||||
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||
|
||||
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
|
||||
AddStep("enable distance snap grid", () => InputManager.Key(Key.Y));
|
||||
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||
AddStep("hold alt", () => InputManager.PressKey(Key.AltLeft));
|
||||
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||
@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
double distanceSnap = double.PositiveInfinity;
|
||||
|
||||
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
|
||||
AddStep("enable distance snap grid", () => InputManager.Key(Key.Y));
|
||||
|
||||
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||
@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
[Test]
|
||||
public void TestGridSizeToggling()
|
||||
{
|
||||
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
|
||||
AddStep("enable rectangular grid", () => InputManager.Key(Key.T));
|
||||
AddUntilStep("rectangular grid visible", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Any());
|
||||
gridSizeIs(4);
|
||||
|
||||
|
@ -88,6 +88,21 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
AddAssert("trail is disjoint", () => this.ChildrenOfType<LegacyCursorTrail>().Single().DisjointTrail, () => Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClickExpand()
|
||||
{
|
||||
createTest(() => new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Scale = new Vector2(10),
|
||||
Child = new CursorTrail(),
|
||||
});
|
||||
|
||||
AddStep("expand", () => this.ChildrenOfType<CursorTrail>().Single().NewPartScale = new Vector2(3));
|
||||
AddWaitStep("let the cursor trail draw a bit", 5);
|
||||
AddStep("contract", () => this.ChildrenOfType<CursorTrail>().Single().NewPartScale = Vector2.One);
|
||||
}
|
||||
|
||||
private void createTest(Func<Drawable> createContent) => AddStep("create trail", () =>
|
||||
{
|
||||
Clear();
|
||||
|
54
osu.Game.Rulesets.Osu/Edit/GenerateToolboxGroup.cs
Normal file
54
osu.Game.Rulesets.Osu/Edit/GenerateToolboxGroup.cs
Normal file
@ -0,0 +1,54 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Screens.Edit.Components;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public partial class GenerateToolboxGroup : EditorToolboxGroup
|
||||
{
|
||||
private readonly EditorToolButton polygonButton;
|
||||
|
||||
public GenerateToolboxGroup()
|
||||
: base("Generate")
|
||||
{
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(5),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
polygonButton = new EditorToolButton("Polygon",
|
||||
() => new SpriteIcon { Icon = FontAwesome.Solid.Spinner },
|
||||
() => new PolygonGenerationPopover()),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (e.Repeat) return false;
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.D:
|
||||
if (!e.ControlPressed || !e.ShiftPressed)
|
||||
return false;
|
||||
|
||||
polygonButton.TriggerClick();
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -54,18 +54,15 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
|
||||
=> base.CreateTernaryButtons()
|
||||
.Concat(DistanceSnapProvider.CreateTernaryButtons())
|
||||
.Concat(new[]
|
||||
{
|
||||
new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap })
|
||||
});
|
||||
.Append(new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = OsuIcon.EditorGridSnap }))
|
||||
.Concat(DistanceSnapProvider.CreateTernaryButtons());
|
||||
|
||||
private BindableList<HitObject> selectedHitObjects;
|
||||
|
||||
private Bindable<HitObject> placementObject;
|
||||
|
||||
[Cached(typeof(IDistanceSnapProvider))]
|
||||
protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider();
|
||||
public readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider();
|
||||
|
||||
[Cached]
|
||||
protected readonly OsuGridToolboxGroup OsuGridToolboxGroup = new OsuGridToolboxGroup();
|
||||
@ -110,6 +107,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler,
|
||||
ScaleHandler = (OsuSelectionScaleHandler)BlueprintContainer.SelectionHandler.ScaleHandler,
|
||||
},
|
||||
new GenerateToolboxGroup(),
|
||||
FreehandSliderToolboxGroup
|
||||
}
|
||||
);
|
||||
|
193
osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs
Normal file
193
osu.Game.Rulesets.Osu/Edit/PolygonGenerationPopover.cs
Normal file
@ -0,0 +1,193 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public partial class PolygonGenerationPopover : OsuPopover
|
||||
{
|
||||
private SliderWithTextBoxInput<double> distanceSnapInput = null!;
|
||||
private SliderWithTextBoxInput<int> offsetAngleInput = null!;
|
||||
private SliderWithTextBoxInput<int> repeatCountInput = null!;
|
||||
private SliderWithTextBoxInput<int> pointInput = null!;
|
||||
private RoundedButton commitButton = null!;
|
||||
|
||||
private readonly List<HitCircle> insertedCircles = new List<HitCircle>();
|
||||
private bool began;
|
||||
private bool committed;
|
||||
|
||||
[Resolved]
|
||||
private IBeatSnapProvider beatSnapProvider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private EditorClock editorClock { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IEditorChangeHandler? changeHandler { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private HitObjectComposer composer { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
Width = 220,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(20),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
distanceSnapInput = new SliderWithTextBoxInput<double>("Distance snap:")
|
||||
{
|
||||
Current = new BindableNumber<double>(1)
|
||||
{
|
||||
MinValue = 0.1,
|
||||
MaxValue = 6,
|
||||
Precision = 0.1,
|
||||
Value = ((OsuHitObjectComposer)composer).DistanceSnapProvider.DistanceSpacingMultiplier.Value,
|
||||
},
|
||||
Instantaneous = true
|
||||
},
|
||||
offsetAngleInput = new SliderWithTextBoxInput<int>("Offset angle:")
|
||||
{
|
||||
Current = new BindableNumber<int>
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 180,
|
||||
Precision = 1
|
||||
},
|
||||
Instantaneous = true
|
||||
},
|
||||
repeatCountInput = new SliderWithTextBoxInput<int>("Repeats:")
|
||||
{
|
||||
Current = new BindableNumber<int>(1)
|
||||
{
|
||||
MinValue = 1,
|
||||
MaxValue = 10,
|
||||
Precision = 1
|
||||
},
|
||||
Instantaneous = true
|
||||
},
|
||||
pointInput = new SliderWithTextBoxInput<int>("Vertices:")
|
||||
{
|
||||
Current = new BindableNumber<int>(3)
|
||||
{
|
||||
MinValue = 3,
|
||||
MaxValue = 10,
|
||||
Precision = 1,
|
||||
},
|
||||
Instantaneous = true
|
||||
},
|
||||
commitButton = new RoundedButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = "Create",
|
||||
Action = commit
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
began = true;
|
||||
|
||||
distanceSnapInput.Current.BindValueChanged(_ => tryCreatePolygon());
|
||||
offsetAngleInput.Current.BindValueChanged(_ => tryCreatePolygon());
|
||||
repeatCountInput.Current.BindValueChanged(_ => tryCreatePolygon());
|
||||
pointInput.Current.BindValueChanged(_ => tryCreatePolygon());
|
||||
tryCreatePolygon();
|
||||
}
|
||||
|
||||
private void tryCreatePolygon()
|
||||
{
|
||||
double startTime = beatSnapProvider.SnapTime(editorClock.CurrentTime);
|
||||
TimingControlPoint timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(startTime);
|
||||
double timeSpacing = timingPoint.BeatLength / editorBeatmap.BeatDivisor;
|
||||
IHasSliderVelocity lastWithSliderVelocity = editorBeatmap.HitObjects.Where(ho => ho.GetEndTime() <= startTime).OfType<IHasSliderVelocity>().LastOrDefault() ?? new Slider();
|
||||
double velocity = OsuHitObject.BASE_SCORING_DISTANCE * editorBeatmap.Difficulty.SliderMultiplier
|
||||
/ LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(lastWithSliderVelocity, timingPoint, OsuRuleset.SHORT_NAME);
|
||||
double length = distanceSnapInput.Current.Value * velocity * timeSpacing;
|
||||
float polygonRadius = (float)(length / (2 * Math.Sin(double.Pi / pointInput.Current.Value)));
|
||||
|
||||
editorBeatmap.RemoveRange(insertedCircles);
|
||||
insertedCircles.Clear();
|
||||
|
||||
var selectionHandler = (EditorSelectionHandler)composer.BlueprintContainer.SelectionHandler;
|
||||
bool first = true;
|
||||
|
||||
for (int i = 1; i <= pointInput.Current.Value * repeatCountInput.Current.Value; ++i)
|
||||
{
|
||||
float angle = float.DegreesToRadians(offsetAngleInput.Current.Value) + i * (2 * float.Pi / pointInput.Current.Value);
|
||||
var position = OsuPlayfield.BASE_SIZE / 2 + new Vector2(polygonRadius * float.Cos(angle), polygonRadius * float.Sin(angle));
|
||||
|
||||
var circle = new HitCircle
|
||||
{
|
||||
Position = position,
|
||||
StartTime = startTime,
|
||||
NewCombo = first && selectionHandler.SelectionNewComboState.Value == TernaryState.True,
|
||||
};
|
||||
// TODO: probably ensure samples also follow current ternary status (not trivial)
|
||||
circle.Samples.Add(circle.CreateHitSampleInfo());
|
||||
|
||||
if (position.X < 0 || position.Y < 0 || position.X > OsuPlayfield.BASE_SIZE.X || position.Y > OsuPlayfield.BASE_SIZE.Y)
|
||||
{
|
||||
commitButton.Enabled.Value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
insertedCircles.Add(circle);
|
||||
startTime = beatSnapProvider.SnapTime(startTime + timeSpacing);
|
||||
|
||||
first = false;
|
||||
}
|
||||
|
||||
editorBeatmap.AddRange(insertedCircles);
|
||||
commitButton.Enabled.Value = true;
|
||||
}
|
||||
|
||||
private void commit()
|
||||
{
|
||||
changeHandler?.EndChange();
|
||||
committed = true;
|
||||
Hide();
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
base.PopOut();
|
||||
|
||||
if (began && !committed)
|
||||
{
|
||||
editorBeatmap.RemoveRange(insertedCircles);
|
||||
changeHandler?.EndChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu
|
||||
{
|
||||
public class OsuSkinComponentLookup : GameplaySkinComponentLookup<OsuSkinComponents>
|
||||
public class OsuSkinComponentLookup : SkinComponentLookup<OsuSkinComponents>
|
||||
{
|
||||
public OsuSkinComponentLookup(OsuSkinComponents component)
|
||||
: base(component)
|
||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
{
|
||||
switch (lookup)
|
||||
{
|
||||
case GameplaySkinComponentLookup<HitResult> resultComponent:
|
||||
case SkinComponentLookup<HitResult> resultComponent:
|
||||
HitResult result = resultComponent.Component;
|
||||
|
||||
// This should eventually be moved to a skin setting, when supported.
|
||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
{
|
||||
switch (lookup)
|
||||
{
|
||||
case GameplaySkinComponentLookup<HitResult> resultComponent:
|
||||
case SkinComponentLookup<HitResult> resultComponent:
|
||||
HitResult result = resultComponent.Component;
|
||||
|
||||
switch (result)
|
||||
|
@ -44,23 +44,19 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
switch (lookup)
|
||||
{
|
||||
case SkinComponentsContainerLookup containerLookup:
|
||||
case GlobalSkinnableContainerLookup containerLookup:
|
||||
// Only handle per ruleset defaults here.
|
||||
if (containerLookup.Ruleset == null)
|
||||
return base.GetDrawableComponent(lookup);
|
||||
|
||||
// Skin has configuration.
|
||||
if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d)
|
||||
return d;
|
||||
|
||||
// we don't have enough assets to display these components (this is especially the case on a "beatmap" skin).
|
||||
if (!IsProvidingLegacyResources)
|
||||
return null;
|
||||
|
||||
// Our own ruleset components default.
|
||||
switch (containerLookup.Target)
|
||||
switch (containerLookup.Lookup)
|
||||
{
|
||||
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
|
||||
case GlobalSkinnableContainers.MainHUDComponents:
|
||||
return new DefaultSkinComponentsContainer(container =>
|
||||
{
|
||||
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
|
||||
@ -69,10 +65,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
// set the anchor to top right so that it won't squash to the return button to the top
|
||||
keyCounter.Anchor = Anchor.CentreRight;
|
||||
keyCounter.Origin = Anchor.CentreRight;
|
||||
keyCounter.X = 0;
|
||||
// 340px is the default height inherit from stable
|
||||
keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y;
|
||||
keyCounter.Origin = Anchor.TopRight;
|
||||
keyCounter.Position = new Vector2(0, -40) * 1.6f;
|
||||
}
|
||||
|
||||
var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault();
|
||||
|
@ -38,6 +38,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
private double timeOffset;
|
||||
private float time;
|
||||
|
||||
/// <summary>
|
||||
/// The scale used on creation of a new trail part.
|
||||
/// </summary>
|
||||
public Vector2 NewPartScale = Vector2.One;
|
||||
|
||||
private Anchor trailOrigin = Anchor.Centre;
|
||||
|
||||
protected Anchor TrailOrigin
|
||||
@ -188,6 +193,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
{
|
||||
parts[currentIndex].Position = localSpacePosition;
|
||||
parts[currentIndex].Time = time + 1;
|
||||
parts[currentIndex].Scale = NewPartScale;
|
||||
++parts[currentIndex].InvalidationID;
|
||||
|
||||
currentIndex = (currentIndex + 1) % max_sprites;
|
||||
@ -199,6 +205,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
{
|
||||
public Vector2 Position;
|
||||
public float Time;
|
||||
public Vector2 Scale;
|
||||
public long InvalidationID;
|
||||
}
|
||||
|
||||
@ -280,7 +287,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y)),
|
||||
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y),
|
||||
TexturePosition = textureRect.BottomLeft,
|
||||
TextureRect = new Vector4(0, 0, 1, 1),
|
||||
Colour = DrawColourInfo.Colour.BottomLeft.Linear,
|
||||
@ -289,7 +296,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X), part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y)),
|
||||
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y),
|
||||
TexturePosition = textureRect.BottomRight,
|
||||
TextureRect = new Vector4(0, 0, 1, 1),
|
||||
Colour = DrawColourInfo.Colour.BottomRight.Linear,
|
||||
@ -298,7 +305,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X), part.Position.Y - texture.DisplayHeight * originPosition.Y),
|
||||
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y),
|
||||
TexturePosition = textureRect.TopRight,
|
||||
TextureRect = new Vector4(0, 0, 1, 1),
|
||||
Colour = DrawColourInfo.Colour.TopRight.Linear,
|
||||
@ -307,7 +314,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X, part.Position.Y - texture.DisplayHeight * originPosition.Y),
|
||||
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y),
|
||||
TexturePosition = textureRect.TopLeft,
|
||||
TextureRect = new Vector4(0, 0, 1, 1),
|
||||
Colour = DrawColourInfo.Colour.TopLeft.Linear,
|
||||
|
@ -31,6 +31,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
private SkinnableCursor skinnableCursor => (SkinnableCursor)cursorSprite.Drawable;
|
||||
|
||||
/// <summary>
|
||||
/// The current expanded scale of the cursor.
|
||||
/// </summary>
|
||||
public Vector2 CurrentExpandedScale => skinnableCursor.ExpandTarget?.Scale ?? Vector2.One;
|
||||
|
||||
public IBindable<float> CursorScale => cursorScale;
|
||||
|
||||
private readonly Bindable<float> cursorScale = new BindableFloat(1);
|
||||
|
@ -23,14 +23,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
public new OsuCursor ActiveCursor => (OsuCursor)base.ActiveCursor;
|
||||
|
||||
protected override Drawable CreateCursor() => new OsuCursor();
|
||||
|
||||
protected override Container<Drawable> Content => fadeContainer;
|
||||
|
||||
private readonly Container<Drawable> fadeContainer;
|
||||
|
||||
private readonly Bindable<bool> showTrail = new Bindable<bool>(true);
|
||||
|
||||
private readonly Drawable cursorTrail;
|
||||
private readonly SkinnableDrawable cursorTrail;
|
||||
|
||||
private readonly CursorRippleVisualiser rippleVisualiser;
|
||||
|
||||
@ -39,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
InternalChild = fadeContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new[]
|
||||
Children = new CompositeDrawable[]
|
||||
{
|
||||
cursorTrail = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling),
|
||||
rippleVisualiser = new CursorRippleVisualiser(),
|
||||
@ -79,6 +78,14 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
ActiveCursor.Contract();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (cursorTrail.Drawable is CursorTrail trail)
|
||||
trail.NewPartScale = ActiveCursor.CurrentExpandedScale;
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
|
||||
{
|
||||
switch (e.Action)
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,10 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
beatmap.ControlPointInfo.Add(100000, new TimingControlPoint { BeatLength = 100 });
|
||||
beatmap.ControlPointInfo.Add(50000, new DifficultyControlPoint { SliderVelocity = 2 });
|
||||
beatmap.ControlPointInfo.Add(80000, new EffectControlPoint { KiaiMode = true });
|
||||
beatmap.ControlPointInfo.Add(110000, new EffectControlPoint { KiaiMode = false });
|
||||
beatmap.BeatmapInfo.Bookmarks = new[] { 75000, 125000 };
|
||||
beatmap.Breaks.Add(new ManualBreakPeriod(90000, 120000));
|
||||
|
||||
editorBeatmap = new EditorBeatmap(beatmap);
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new TrackVirtual(60000), false, false);
|
||||
|
||||
// best way to check without exposing.
|
||||
private Drawable hideTarget => hudOverlay.ChildrenOfType<SkinComponentsContainer>().First();
|
||||
private Drawable hideTarget => hudOverlay.ChildrenOfType<SkinnableContainer>().First();
|
||||
private Drawable keyCounterContent => hudOverlay.ChildrenOfType<KeyCounterDisplay>().First().ChildrenOfType<Drawable>().Skip(1).First();
|
||||
|
||||
public TestSceneHUDOverlay()
|
||||
@ -174,6 +174,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
holdForMenu.Action += () => activated = true;
|
||||
});
|
||||
|
||||
AddStep("set hold button always visible", () => localConfig.SetValue(OsuSetting.AlwaysShowHoldForMenuButton, true));
|
||||
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
|
||||
AddUntilStep("hidetarget is hidden", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0));
|
||||
|
||||
@ -214,6 +215,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
progress.ChildrenOfType<ArgonSongProgressBar>().Single().OnSeek += _ => seeked = true;
|
||||
});
|
||||
|
||||
AddStep("set hold button always visible", () => localConfig.SetValue(OsuSetting.AlwaysShowHoldForMenuButton, true));
|
||||
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
|
||||
AddUntilStep("hidetarget is hidden", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0));
|
||||
|
||||
@ -240,8 +242,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
createNew();
|
||||
|
||||
AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().Single().Alpha == 0);
|
||||
AddUntilStep("wait for hud load", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().All(c => c.ComponentsLoaded));
|
||||
AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType<SkinnableContainer>().Single().Alpha == 0);
|
||||
AddUntilStep("wait for hud load", () => hudOverlay.ChildrenOfType<SkinnableContainer>().All(c => c.ComponentsLoaded));
|
||||
|
||||
AddStep("bind on update", () =>
|
||||
{
|
||||
@ -258,10 +260,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
createNew();
|
||||
|
||||
AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().Single().Alpha == 0);
|
||||
AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType<SkinnableContainer>().Single().Alpha == 0);
|
||||
|
||||
AddStep("reload components", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().Single().Reload());
|
||||
AddUntilStep("skinnable components loaded", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().Single().ComponentsLoaded);
|
||||
AddStep("reload components", () => hudOverlay.ChildrenOfType<SkinnableContainer>().Single().Reload());
|
||||
AddUntilStep("skinnable components loaded", () => hudOverlay.ChildrenOfType<SkinnableContainer>().Single().ComponentsLoaded);
|
||||
}
|
||||
|
||||
private void createNew(Action<HUDOverlay>? action = null)
|
||||
|
@ -1,13 +1,13 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
@ -21,11 +21,19 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
protected override double TimePerAction => 100; // required for the early exit test, since hold-to-confirm delay is 200ms
|
||||
|
||||
private HoldForMenuButton holdForMenuButton;
|
||||
private HoldForMenuButton holdForMenuButton = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("set button always on", () =>
|
||||
{
|
||||
config.SetValue(OsuSetting.AlwaysShowHoldForMenuButton, true);
|
||||
});
|
||||
|
||||
AddStep("create button", () =>
|
||||
{
|
||||
exitAction = false;
|
||||
|
@ -27,6 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[TestCase(2000, 0)]
|
||||
[TestCase(3000, first_hit_object - 3000)]
|
||||
[TestCase(10000, first_hit_object - 10000)]
|
||||
[FlakyTest]
|
||||
public void TestLeadInProducesCorrectStartTime(double leadIn, double expectedStartTime)
|
||||
{
|
||||
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo)
|
||||
@ -41,6 +42,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[TestCase(0, 0)]
|
||||
[TestCase(-1000, -1000)]
|
||||
[TestCase(-10000, -10000)]
|
||||
[FlakyTest]
|
||||
public void TestStoryboardProducesCorrectStartTimeSimpleAlpha(double firstStoryboardEvent, double expectedStartTime)
|
||||
{
|
||||
var storyboard = new Storyboard();
|
||||
@ -64,6 +66,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[TestCase(0, 0, true)]
|
||||
[TestCase(-1000, -1000, true)]
|
||||
[TestCase(-10000, -10000, true)]
|
||||
[FlakyTest]
|
||||
public void TestStoryboardProducesCorrectStartTimeFadeInAfterOtherEvents(double firstStoryboardEvent, double expectedStartTime, bool addEventToLoop)
|
||||
{
|
||||
const double loop_start_time = -20000;
|
||||
|
@ -320,6 +320,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestExitViaHoldToExit()
|
||||
{
|
||||
AddStep("set hold button always visible", () => LocalConfig.SetValue(OsuSetting.AlwaysShowHoldForMenuButton, true));
|
||||
|
||||
AddStep("exit", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(Player.HUDOverlay.HoldToQuit.First(c => c is HoldToConfirmContainer));
|
||||
|
@ -286,7 +286,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
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));
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Screens.Edit;
|
||||
|
||||
namespace osu.Game.Beatmaps.Formats
|
||||
{
|
||||
@ -336,7 +337,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
break;
|
||||
|
||||
case @"BeatDivisor":
|
||||
beatmap.BeatmapInfo.BeatDivisor = Parsing.ParseInt(pair.Value);
|
||||
beatmap.BeatmapInfo.BeatDivisor = Math.Clamp(Parsing.ParseInt(pair.Value), BindableBeatDivisor.MINIMUM_DIVISOR, BindableBeatDivisor.MAXIMUM_DIVISOR);
|
||||
break;
|
||||
|
||||
case @"GridSize":
|
||||
|
@ -80,6 +80,8 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public bool TryLookup(BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata)
|
||||
{
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
|
||||
if (!Available)
|
||||
{
|
||||
onlineMetadata = null;
|
||||
@ -94,43 +96,21 @@ namespace osu.Game.Beatmaps
|
||||
return false;
|
||||
}
|
||||
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
|
||||
try
|
||||
{
|
||||
using (var db = new SqliteConnection(string.Concat(@"Data Source=", storage.GetFullPath(@"online.db", true))))
|
||||
{
|
||||
db.Open();
|
||||
|
||||
using (var cmd = db.CreateCommand())
|
||||
switch (getCacheVersion(db))
|
||||
{
|
||||
cmd.CommandText =
|
||||
@"SELECT beatmapset_id, beatmap_id, approved, user_id, checksum, last_update FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineID OR filename = @Path";
|
||||
case 1:
|
||||
// will eventually become irrelevant due to the monthly recycling of local caches
|
||||
// can be removed 20250221
|
||||
return queryCacheVersion1(db, beatmapInfo, out onlineMetadata);
|
||||
|
||||
cmd.Parameters.Add(new SqliteParameter(@"@MD5Hash", beatmapInfo.MD5Hash));
|
||||
cmd.Parameters.Add(new SqliteParameter(@"@OnlineID", beatmapInfo.OnlineID));
|
||||
cmd.Parameters.Add(new SqliteParameter(@"@Path", beatmapInfo.Path));
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
if (reader.Read())
|
||||
{
|
||||
logForModel(beatmapInfo.BeatmapSet, $@"Cached local retrieval for {beatmapInfo}.");
|
||||
|
||||
onlineMetadata = new OnlineBeatmapMetadata
|
||||
{
|
||||
BeatmapSetID = reader.GetInt32(0),
|
||||
BeatmapID = reader.GetInt32(1),
|
||||
BeatmapStatus = (BeatmapOnlineStatus)reader.GetByte(2),
|
||||
BeatmapSetStatus = (BeatmapOnlineStatus)reader.GetByte(2),
|
||||
AuthorID = reader.GetInt32(3),
|
||||
MD5Hash = reader.GetString(4),
|
||||
LastUpdated = reader.GetDateTimeOffset(5),
|
||||
// TODO: DateSubmitted and DateRanked are not provided by local cache.
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
case 2:
|
||||
return queryCacheVersion2(db, beatmapInfo, out onlineMetadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -211,6 +191,115 @@ namespace osu.Game.Beatmaps
|
||||
});
|
||||
}
|
||||
|
||||
private int getCacheVersion(SqliteConnection connection)
|
||||
{
|
||||
using (var cmd = connection.CreateCommand())
|
||||
{
|
||||
cmd.CommandText = @"SELECT COUNT(1) FROM `sqlite_master` WHERE `type` = 'table' AND `name` = 'schema_version'";
|
||||
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
if (!reader.Read())
|
||||
throw new InvalidOperationException("Error when attempting to check for existence of `schema_version` table.");
|
||||
|
||||
// No versioning table means that this is the very first version of the schema.
|
||||
if (reader.GetInt32(0) == 0)
|
||||
return 1;
|
||||
}
|
||||
|
||||
using (var cmd = connection.CreateCommand())
|
||||
{
|
||||
cmd.CommandText = @"SELECT `number` FROM `schema_version`";
|
||||
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
if (!reader.Read())
|
||||
throw new InvalidOperationException("Error when attempting to query schema version.");
|
||||
|
||||
return reader.GetInt32(0);
|
||||
}
|
||||
}
|
||||
|
||||
private bool queryCacheVersion1(SqliteConnection db, BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata)
|
||||
{
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
|
||||
using var cmd = db.CreateCommand();
|
||||
|
||||
cmd.CommandText =
|
||||
@"SELECT beatmapset_id, beatmap_id, approved, user_id, checksum, last_update FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineID OR filename = @Path";
|
||||
|
||||
cmd.Parameters.Add(new SqliteParameter(@"@MD5Hash", beatmapInfo.MD5Hash));
|
||||
cmd.Parameters.Add(new SqliteParameter(@"@OnlineID", beatmapInfo.OnlineID));
|
||||
cmd.Parameters.Add(new SqliteParameter(@"@Path", beatmapInfo.Path));
|
||||
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
if (reader.Read())
|
||||
{
|
||||
logForModel(beatmapInfo.BeatmapSet, $@"Cached local retrieval for {beatmapInfo} (cache version 1).");
|
||||
|
||||
onlineMetadata = new OnlineBeatmapMetadata
|
||||
{
|
||||
BeatmapSetID = reader.GetInt32(0),
|
||||
BeatmapID = reader.GetInt32(1),
|
||||
BeatmapStatus = (BeatmapOnlineStatus)reader.GetByte(2),
|
||||
BeatmapSetStatus = (BeatmapOnlineStatus)reader.GetByte(2),
|
||||
AuthorID = reader.GetInt32(3),
|
||||
MD5Hash = reader.GetString(4),
|
||||
LastUpdated = reader.GetDateTimeOffset(5),
|
||||
// TODO: DateSubmitted and DateRanked are not provided by local cache in this version.
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
onlineMetadata = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool queryCacheVersion2(SqliteConnection db, BeatmapInfo beatmapInfo, out OnlineBeatmapMetadata? onlineMetadata)
|
||||
{
|
||||
Debug.Assert(beatmapInfo.BeatmapSet != null);
|
||||
|
||||
using var cmd = db.CreateCommand();
|
||||
|
||||
cmd.CommandText =
|
||||
"""
|
||||
SELECT `b`.`beatmapset_id`, `b`.`beatmap_id`, `b`.`approved`, `b`.`user_id`, `b`.`checksum`, `b`.`last_update`, `s`.`submit_date`, `s`.`approved_date`
|
||||
FROM `osu_beatmaps` AS `b`
|
||||
JOIN `osu_beatmapsets` AS `s` ON `s`.`beatmapset_id` = `b`.`beatmapset_id`
|
||||
WHERE `b`.`checksum` = @MD5Hash OR `b`.`beatmap_id` = @OnlineID OR `b`.`filename` = @Path
|
||||
""";
|
||||
|
||||
cmd.Parameters.Add(new SqliteParameter(@"@MD5Hash", beatmapInfo.MD5Hash));
|
||||
cmd.Parameters.Add(new SqliteParameter(@"@OnlineID", beatmapInfo.OnlineID));
|
||||
cmd.Parameters.Add(new SqliteParameter(@"@Path", beatmapInfo.Path));
|
||||
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
if (reader.Read())
|
||||
{
|
||||
logForModel(beatmapInfo.BeatmapSet, $@"Cached local retrieval for {beatmapInfo} (cache version 2).");
|
||||
|
||||
onlineMetadata = new OnlineBeatmapMetadata
|
||||
{
|
||||
BeatmapSetID = reader.GetInt32(0),
|
||||
BeatmapID = reader.GetInt32(1),
|
||||
BeatmapStatus = (BeatmapOnlineStatus)reader.GetByte(2),
|
||||
BeatmapSetStatus = (BeatmapOnlineStatus)reader.GetByte(2),
|
||||
AuthorID = reader.GetInt32(3),
|
||||
MD5Hash = reader.GetString(4),
|
||||
LastUpdated = reader.GetDateTimeOffset(5),
|
||||
DateSubmitted = reader.GetDateTimeOffset(6),
|
||||
DateRanked = reader.GetDateTimeOffset(7),
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
onlineMetadata = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void log(string message)
|
||||
=> Logger.Log($@"[{nameof(LocalCachedBeatmapMetadataSource)}] {message}", LoggingTarget.Database);
|
||||
|
||||
|
@ -205,6 +205,8 @@ namespace osu.Game.Configuration
|
||||
|
||||
SetDefault(OsuSetting.EditorTimelineShowTimingChanges, true);
|
||||
SetDefault(OsuSetting.EditorTimelineShowTicks, true);
|
||||
|
||||
SetDefault(OsuSetting.AlwaysShowHoldForMenuButton, false);
|
||||
}
|
||||
|
||||
protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup)
|
||||
@ -429,5 +431,6 @@ namespace osu.Game.Configuration
|
||||
HideCountryFlags,
|
||||
EditorTimelineShowTimingChanges,
|
||||
EditorTimelineShowTicks,
|
||||
AlwaysShowHoldForMenuButton
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -84,6 +84,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString AlwaysShowGameplayLeaderboard => new TranslatableString(getKey(@"gameplay_leaderboard"), @"Always show gameplay leaderboard");
|
||||
|
||||
/// <summary>
|
||||
/// "Always show hold for menu button"
|
||||
/// </summary>
|
||||
public static LocalisableString AlwaysShowHoldForMenuButton => new TranslatableString(getKey(@"always_show_hold_for_menu_button"), @"Always show hold for menu button");
|
||||
|
||||
/// <summary>
|
||||
/// "Always play first combo break sound"
|
||||
/// </summary>
|
||||
|
@ -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]
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,6 +60,8 @@ namespace osu.Game.Overlays
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
private BindableNumber<double> sampleVolume = null!;
|
||||
|
||||
private readonly BindableDouble audioDuckVolume = new BindableDouble(1);
|
||||
|
||||
private AudioFilter audioDuckFilter = null!;
|
||||
@ -69,6 +71,7 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
AddInternal(audioDuckFilter = new AudioFilter(audio.TrackMixer));
|
||||
audio.Tracks.AddAdjustment(AdjustableProperty.Volume, audioDuckVolume);
|
||||
sampleVolume = audio.VolumeSample.GetBoundCopy();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -269,6 +272,10 @@ namespace osu.Game.Overlays
|
||||
/// <returns>A <see cref="IDisposable"/> which will restore the duck operation when disposed.</returns>
|
||||
public IDisposable Duck(DuckParameters? parameters = null)
|
||||
{
|
||||
// Don't duck if samples have no volume, it sounds weird.
|
||||
if (sampleVolume.Value == 0)
|
||||
return new InvokeOnDisposal(() => { });
|
||||
|
||||
parameters ??= new DuckParameters();
|
||||
|
||||
duckOperations.Add(parameters);
|
||||
@ -302,6 +309,10 @@ namespace osu.Game.Overlays
|
||||
/// <param name="parameters">Parameters defining the ducking operation.</param>
|
||||
public void DuckMomentarily(double delayUntilRestore, DuckParameters? parameters = null)
|
||||
{
|
||||
// Don't duck if samples have no volume, it sounds weird.
|
||||
if (sampleVolume.Value == 0)
|
||||
return;
|
||||
|
||||
parameters ??= new DuckParameters();
|
||||
|
||||
IDisposable duckOperation = Duck(parameters);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
|
||||
Current = config.GetBindable<bool>(OsuSetting.GameplayLeaderboard),
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = GameplaySettingsStrings.AlwaysShowHoldForMenuButton,
|
||||
Current = config.GetBindable<bool>(OsuSetting.AlwaysShowHoldForMenuButton),
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
ClassicDefault = false,
|
||||
LabelText = GameplaySettingsStrings.ShowHealthDisplayWhenCantFail,
|
||||
|
@ -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,
|
||||
@ -421,6 +421,9 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
if (targetContainer != null)
|
||||
changeHandler = new SkinEditorChangeHandler(targetContainer);
|
||||
hasBegunMutating = true;
|
||||
|
||||
// Reload sidebar components.
|
||||
selectedTarget.TriggerChange();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -469,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)
|
||||
{
|
||||
@ -552,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;
|
||||
@ -597,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();
|
||||
@ -621,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 += () =>
|
||||
|
@ -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);
|
||||
|
@ -16,6 +16,9 @@ namespace osu.Game.Screens.Edit
|
||||
{
|
||||
public static readonly int[] PREDEFINED_DIVISORS = { 1, 2, 3, 4, 6, 8, 12, 16 };
|
||||
|
||||
public const int MINIMUM_DIVISOR = 1;
|
||||
public const int MAXIMUM_DIVISOR = 64;
|
||||
|
||||
public Bindable<BeatDivisorPresetCollection> ValidDivisors { get; } = new Bindable<BeatDivisorPresetCollection>(BeatDivisorPresetCollection.COMMON);
|
||||
|
||||
public BindableBeatDivisor(int value = 1)
|
||||
@ -30,8 +33,12 @@ namespace osu.Game.Screens.Edit
|
||||
/// </summary>
|
||||
/// <param name="divisor">The intended divisor.</param>
|
||||
/// <param name="preferKnownPresets">Forces changing the valid divisors to a known preset.</param>
|
||||
public void SetArbitraryDivisor(int divisor, bool preferKnownPresets = false)
|
||||
/// <returns>Whether the divisor was successfully set.</returns>
|
||||
public bool SetArbitraryDivisor(int divisor, bool preferKnownPresets = false)
|
||||
{
|
||||
if (divisor < MINIMUM_DIVISOR || divisor > MAXIMUM_DIVISOR)
|
||||
return false;
|
||||
|
||||
// If the current valid divisor range doesn't contain the proposed value, attempt to find one which does.
|
||||
if (preferKnownPresets || !ValidDivisors.Value.Presets.Contains(divisor))
|
||||
{
|
||||
@ -44,6 +51,7 @@ namespace osu.Game.Screens.Edit
|
||||
}
|
||||
|
||||
Value = divisor;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateBindableProperties()
|
||||
@ -137,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,8 +69,9 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
|
||||
RelativePositionAxes = Axes.X;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = new Circle { RelativeSizeAxes = Axes.Both };
|
||||
Colour = colours.Gray6;
|
||||
InternalChild = new Box { RelativeSizeAxes = Axes.Both };
|
||||
Colour = colours.Gray5;
|
||||
Alpha = 0.4f;
|
||||
}
|
||||
|
||||
public LocalisableString TooltipText => $"{breakPeriod.StartTime.ToEditorFormattedString()} - {breakPeriod.EndTime.ToEditorFormattedString()} break time";
|
||||
|
@ -59,6 +59,12 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 0.4f,
|
||||
},
|
||||
new BreakPart
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new ControlPointPart
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
@ -73,13 +79,6 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 0.4f
|
||||
},
|
||||
new BreakPart
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Height = 0.15f
|
||||
},
|
||||
new MarkerPart { RelativeSizeAxes = Axes.Both },
|
||||
};
|
||||
}
|
||||
|
@ -330,14 +330,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
private void setPresetsFromTextBoxEntry()
|
||||
{
|
||||
if (!int.TryParse(divisorTextBox.Text, out int divisor) || divisor < 1 || divisor > 64)
|
||||
if (!int.TryParse(divisorTextBox.Text, out int divisor) || !BeatDivisor.SetArbitraryDivisor(divisor))
|
||||
{
|
||||
// the text either didn't parse as a divisor, or the divisor was not set due to being out of range.
|
||||
// force a state update to reset the text box's value to the last sane value.
|
||||
updateState();
|
||||
return;
|
||||
}
|
||||
|
||||
BeatDivisor.SetArbitraryDivisor(divisor);
|
||||
|
||||
this.HidePopover();
|
||||
}
|
||||
|
||||
@ -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 };
|
||||
|
@ -2,15 +2,17 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Layout;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
public abstract partial class PositionSnapGrid : CompositeDrawable
|
||||
public abstract partial class PositionSnapGrid : BufferedContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// The position of the origin of this <see cref="PositionSnapGrid"/> in local coordinates.
|
||||
@ -20,7 +22,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
protected readonly LayoutValue GridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit);
|
||||
|
||||
protected PositionSnapGrid()
|
||||
: base(cachedFrameBuffer: true)
|
||||
{
|
||||
BackgroundColour = Color4.White.Opacity(0);
|
||||
|
||||
StartPosition.BindValueChanged(_ => GridCache.Invalidate());
|
||||
|
||||
AddLayout(GridCache);
|
||||
@ -30,7 +35,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (GridCache.IsValid) return;
|
||||
if (GridCache.IsValid)
|
||||
return;
|
||||
|
||||
ClearInternal();
|
||||
|
||||
@ -38,6 +44,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
CreateContent();
|
||||
|
||||
GridCache.Validate();
|
||||
ForceRedraw();
|
||||
}
|
||||
|
||||
protected abstract void CreateContent();
|
||||
@ -53,7 +60,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
Colour = Colour4.White,
|
||||
Alpha = 0.3f,
|
||||
Origin = Anchor.CentreLeft,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = lineWidth,
|
||||
Y = 0,
|
||||
@ -62,28 +68,26 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
Colour = Colour4.White,
|
||||
Alpha = 0.3f,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = lineWidth,
|
||||
Y = drawSize.Y,
|
||||
Height = lineWidth
|
||||
},
|
||||
new Box
|
||||
{
|
||||
Colour = Colour4.White,
|
||||
Alpha = 0.3f,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = lineWidth,
|
||||
X = 0,
|
||||
Width = lineWidth
|
||||
},
|
||||
new Box
|
||||
{
|
||||
Colour = Colour4.White,
|
||||
Alpha = 0.3f,
|
||||
Origin = Anchor.TopCentre,
|
||||
Origin = Anchor.TopRight,
|
||||
Anchor = Anchor.TopRight,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = lineWidth,
|
||||
X = drawSize.X,
|
||||
Width = lineWidth
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -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()
|
||||
private void goToCurrentGroup()
|
||||
{
|
||||
// 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;
|
||||
double accurateTime = clock.CurrentTimeAccurate;
|
||||
|
||||
// If the selected group only has one control point, update the tracking type.
|
||||
case 1:
|
||||
trackedType = selectedGroup.Value?.ControlPoints[0].GetType();
|
||||
break;
|
||||
var activeTimingPoint = Beatmap.ControlPointInfo.TimingPointAt(accurateTime);
|
||||
var activeEffectPoint = Beatmap.ControlPointInfo.EffectPointAt(accurateTime);
|
||||
|
||||
// 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)
|
||||
{
|
||||
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.
|
||||
|
||||
// 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,40 +100,63 @@ 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()
|
||||
protected new UserTrackingScrollContainer Scroll => (UserTrackingScrollContainer)base.Scroll;
|
||||
|
||||
public void ScrollTo(ControlPointGroup group, bool force)
|
||||
{
|
||||
base.LoadComplete();
|
||||
if (Scroll.UserScrolling && !force)
|
||||
return;
|
||||
|
||||
selectedGroup.BindValueChanged(val =>
|
||||
{
|
||||
// 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));
|
||||
// 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(group));
|
||||
|
||||
if (row == null)
|
||||
return;
|
||||
if (row == null)
|
||||
return;
|
||||
|
||||
float minPos = row.Y;
|
||||
float maxPos = minPos + row_height;
|
||||
float minPos = row.Y;
|
||||
float maxPos = minPos + row_height;
|
||||
|
||||
if (minPos < Scroll.Current)
|
||||
Scroll.ScrollTo(minPos);
|
||||
else if (maxPos > Scroll.Current + Scroll.DisplayableContent)
|
||||
Scroll.ScrollTo(maxPos - Scroll.DisplayableContent);
|
||||
});
|
||||
if (minPos < Scroll.Current)
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -323,14 +401,8 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
case TimingControlPoint timing:
|
||||
return new TimingRowAttribute(timing);
|
||||
|
||||
case DifficultyControlPoint difficulty:
|
||||
return new DifficultyRowAttribute(difficulty);
|
||||
|
||||
case EffectControlPoint effect:
|
||||
return new EffectRowAttribute(effect);
|
||||
|
||||
case SampleControlPoint sample:
|
||||
return new SampleRowAttribute(sample);
|
||||
}
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(controlPoint), $"Control point type {controlPoint.GetType()} is not supported");
|
||||
|
@ -1,44 +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.Graphics;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Timing.RowAttributes
|
||||
{
|
||||
public partial class DifficultyRowAttribute : RowAttribute
|
||||
{
|
||||
private readonly BindableNumber<double> speedMultiplier;
|
||||
|
||||
private OsuSpriteText text = null!;
|
||||
|
||||
public DifficultyRowAttribute(DifficultyControlPoint difficulty)
|
||||
: base(difficulty, "difficulty")
|
||||
{
|
||||
speedMultiplier = difficulty.SliderVelocityBindable.GetBoundCopy();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Content.AddRange(new Drawable[]
|
||||
{
|
||||
new AttributeProgressBar(Point)
|
||||
{
|
||||
Current = speedMultiplier,
|
||||
},
|
||||
text = new AttributeText(Point)
|
||||
{
|
||||
Width = 45,
|
||||
},
|
||||
});
|
||||
|
||||
speedMultiplier.BindValueChanged(_ => updateText(), true);
|
||||
}
|
||||
|
||||
private void updateText() => text.Text = $"{speedMultiplier.Value:n2}x";
|
||||
}
|
||||
}
|
@ -1,57 +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.Graphics;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Timing.RowAttributes
|
||||
{
|
||||
public partial class SampleRowAttribute : RowAttribute
|
||||
{
|
||||
private AttributeText sampleText = null!;
|
||||
private OsuSpriteText volumeText = null!;
|
||||
|
||||
private readonly Bindable<string> sampleBank;
|
||||
private readonly BindableNumber<int> volume;
|
||||
|
||||
public SampleRowAttribute(SampleControlPoint sample)
|
||||
: base(sample, "sample")
|
||||
{
|
||||
sampleBank = sample.SampleBankBindable.GetBoundCopy();
|
||||
volume = sample.SampleVolumeBindable.GetBoundCopy();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AttributeProgressBar progress;
|
||||
|
||||
Content.AddRange(new Drawable[]
|
||||
{
|
||||
sampleText = new AttributeText(Point),
|
||||
progress = new AttributeProgressBar(Point),
|
||||
volumeText = new AttributeText(Point)
|
||||
{
|
||||
Width = 40,
|
||||
},
|
||||
});
|
||||
|
||||
volume.BindValueChanged(vol =>
|
||||
{
|
||||
progress.Current.Value = vol.NewValue / 100f;
|
||||
updateText();
|
||||
}, true);
|
||||
|
||||
sampleBank.BindValueChanged(_ => updateText(), true);
|
||||
}
|
||||
|
||||
private void updateText()
|
||||
{
|
||||
volumeText.Text = $"{volume.Value}%";
|
||||
sampleText.Text = $"{sampleBank.Value}";
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -30,6 +30,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||
|
||||
public override bool PropagatePositionalInputSubTree => alwaysShow.Value || touchActive.Value;
|
||||
|
||||
public readonly Bindable<bool> IsPaused = new Bindable<bool>();
|
||||
|
||||
public readonly Bindable<bool> ReplayLoaded = new Bindable<bool>();
|
||||
@ -40,6 +42,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
private OsuSpriteText text;
|
||||
|
||||
private Bindable<bool> alwaysShow;
|
||||
|
||||
public HoldForMenuButton()
|
||||
{
|
||||
Direction = FillDirection.Horizontal;
|
||||
@ -50,7 +54,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(Player player)
|
||||
private void load(Player player, OsuConfigManager config)
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@ -71,6 +75,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
};
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
alwaysShow = config.GetBindable<bool>(OsuSetting.AlwaysShowHoldForMenuButton);
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
@ -117,9 +123,14 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// While the button is hovered or still animating, keep fully visible.
|
||||
if (text.Alpha > 0 || button.Progress.Value > 0 || button.IsHovered)
|
||||
Alpha = 1;
|
||||
else
|
||||
// When touch input is detected, keep visible at a constant opacity.
|
||||
else if (touchActive.Value)
|
||||
Alpha = 0.5f;
|
||||
// Otherwise, if the user chooses, show it when the mouse is nearby.
|
||||
else if (alwaysShow.Value)
|
||||
{
|
||||
float minAlpha = touchActive.Value ? .08f : 0;
|
||||
|
||||
@ -127,6 +138,8 @@ namespace osu.Game.Screens.Play.HUD
|
||||
Math.Clamp(Clock.ElapsedFrameTime, 0, 200),
|
||||
Alpha, Math.Clamp(1 - positionalAdjust, minAlpha, 1), 0, 200, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
Alpha = 0;
|
||||
}
|
||||
|
||||
private partial class HoldButton : HoldToConfirmContainer, IKeyBindingHandler<GlobalAction>
|
||||
|
@ -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)
|
||||
{
|
||||
@ -442,7 +454,6 @@ namespace osu.Game.Screens.Play
|
||||
},
|
||||
// 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 =
|
||||
@ -470,6 +481,7 @@ namespace osu.Game.Screens.Play
|
||||
RequestSkip = () => progressToResults(false),
|
||||
Alpha = 0
|
||||
},
|
||||
DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(),
|
||||
PauseOverlay = new PauseOverlay
|
||||
{
|
||||
OnResume = Resume,
|
||||
|
81
osu.Game/Screens/Ranking/CollectionButton.cs
Normal file
81
osu.Game/Screens/Ranking/CollectionButton.cs
Normal file
@ -0,0 +1,81 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osuTK;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Screens.Ranking
|
||||
{
|
||||
public partial class CollectionButton : GrayButton, IHasPopover
|
||||
{
|
||||
private readonly BeatmapInfo beatmapInfo;
|
||||
private readonly Bindable<bool> isInAnyCollection;
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realmAccess { get; set; } = null!;
|
||||
|
||||
private IDisposable? collectionSubscription;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
public CollectionButton(BeatmapInfo beatmapInfo)
|
||||
: base(FontAwesome.Solid.Book)
|
||||
{
|
||||
this.beatmapInfo = beatmapInfo;
|
||||
isInAnyCollection = new Bindable<bool>(false);
|
||||
|
||||
Size = new Vector2(75, 30);
|
||||
|
||||
TooltipText = "collections";
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Action = this.ShowPopover;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
collectionSubscription = realmAccess.RegisterForNotifications(r => r.All<BeatmapCollection>(), collectionsChanged);
|
||||
|
||||
isInAnyCollection.BindValueChanged(_ => updateState(), true);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
collectionSubscription?.Dispose();
|
||||
}
|
||||
|
||||
private void collectionsChanged(IRealmCollection<BeatmapCollection> sender, ChangeSet? changes)
|
||||
{
|
||||
isInAnyCollection.Value = sender.AsEnumerable().Any(c => c.BeatmapMD5Hashes.Contains(beatmapInfo.MD5Hash));
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
Background.FadeColour(isInAnyCollection.Value ? colours.Green : colours.Gray4, 500, Easing.InOutExpo);
|
||||
}
|
||||
|
||||
public Popover GetPopover() => new CollectionPopover(beatmapInfo);
|
||||
}
|
||||
}
|
67
osu.Game/Screens/Ranking/CollectionPopover.cs
Normal file
67
osu.Game/Screens/Ranking/CollectionPopover.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 osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
|
||||
namespace osu.Game.Screens.Ranking
|
||||
{
|
||||
public partial class CollectionPopover : OsuPopover
|
||||
{
|
||||
private readonly BeatmapInfo beatmapInfo;
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private ManageCollectionsDialog? manageCollectionsDialog { get; set; }
|
||||
|
||||
public CollectionPopover(BeatmapInfo beatmapInfo)
|
||||
: base(false)
|
||||
{
|
||||
this.beatmapInfo = beatmapInfo;
|
||||
|
||||
Body.CornerRadius = 4;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Children = new[]
|
||||
{
|
||||
new OsuMenu(Direction.Vertical, true)
|
||||
{
|
||||
Items = items,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnFocusLost(FocusLostEvent e)
|
||||
{
|
||||
base.OnFocusLost(e);
|
||||
Hide();
|
||||
}
|
||||
|
||||
private OsuMenuItem[] items
|
||||
{
|
||||
get
|
||||
{
|
||||
var collectionItems = realm.Realm.All<BeatmapCollection>()
|
||||
.OrderBy(c => c.Name)
|
||||
.AsEnumerable()
|
||||
.Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmapInfo)).Cast<OsuMenuItem>().ToList();
|
||||
|
||||
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, () => manageCollectionsDialog?.Show()));
|
||||
|
||||
return collectionItems.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
155
osu.Game/Screens/Ranking/FavouriteButton.cs
Normal file
155
osu.Game/Screens/Ranking/FavouriteButton.cs
Normal file
@ -0,0 +1,155 @@
|
||||
// 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.Graphics.Sprites;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Drawables.Cards;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Ranking
|
||||
{
|
||||
public partial class FavouriteButton : GrayButton
|
||||
{
|
||||
public readonly BeatmapSetInfo BeatmapSetInfo;
|
||||
private APIBeatmapSet? beatmapSet;
|
||||
private readonly Bindable<BeatmapSetFavouriteState> current;
|
||||
|
||||
private PostBeatmapFavouriteRequest? favouriteRequest;
|
||||
private LoadingLayer loading = null!;
|
||||
|
||||
private readonly IBindable<APIUser> localUser = new Bindable<APIUser>();
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
public FavouriteButton(BeatmapSetInfo beatmapSetInfo)
|
||||
: base(FontAwesome.Regular.Heart)
|
||||
{
|
||||
BeatmapSetInfo = beatmapSetInfo;
|
||||
current = new BindableWithCurrent<BeatmapSetFavouriteState>(new BeatmapSetFavouriteState(false, 0));
|
||||
|
||||
Size = new Vector2(75, 30);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Add(loading = new LoadingLayer(true, false));
|
||||
|
||||
Action = toggleFavouriteStatus;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
current.BindValueChanged(_ => updateState(), true);
|
||||
|
||||
localUser.BindTo(api.LocalUser);
|
||||
localUser.BindValueChanged(_ => updateUser(), true);
|
||||
}
|
||||
|
||||
private void getBeatmapSet()
|
||||
{
|
||||
GetBeatmapSetRequest beatmapSetRequest = new GetBeatmapSetRequest(BeatmapSetInfo.OnlineID);
|
||||
|
||||
loading.Show();
|
||||
beatmapSetRequest.Success += beatmapSet =>
|
||||
{
|
||||
this.beatmapSet = beatmapSet;
|
||||
current.Value = new BeatmapSetFavouriteState(this.beatmapSet.HasFavourited, this.beatmapSet.FavouriteCount);
|
||||
|
||||
loading.Hide();
|
||||
Enabled.Value = true;
|
||||
};
|
||||
beatmapSetRequest.Failure += e =>
|
||||
{
|
||||
Logger.Error(e, $"Failed to fetch beatmap info: {e.Message}");
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
loading.Hide();
|
||||
Enabled.Value = false;
|
||||
});
|
||||
};
|
||||
api.Queue(beatmapSetRequest);
|
||||
}
|
||||
|
||||
private void toggleFavouriteStatus()
|
||||
{
|
||||
if (beatmapSet == null)
|
||||
return;
|
||||
|
||||
Enabled.Value = false;
|
||||
loading.Show();
|
||||
|
||||
var actionType = current.Value.Favourited ? BeatmapFavouriteAction.UnFavourite : BeatmapFavouriteAction.Favourite;
|
||||
|
||||
favouriteRequest?.Cancel();
|
||||
favouriteRequest = new PostBeatmapFavouriteRequest(beatmapSet.OnlineID, actionType);
|
||||
|
||||
favouriteRequest.Success += () =>
|
||||
{
|
||||
bool favourited = actionType == BeatmapFavouriteAction.Favourite;
|
||||
|
||||
current.Value = new BeatmapSetFavouriteState(favourited, current.Value.FavouriteCount + (favourited ? 1 : -1));
|
||||
|
||||
Enabled.Value = true;
|
||||
loading.Hide();
|
||||
};
|
||||
favouriteRequest.Failure += e =>
|
||||
{
|
||||
Logger.Error(e, $"Failed to {actionType.ToString().ToLowerInvariant()} beatmap: {e.Message}");
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
Enabled.Value = true;
|
||||
loading.Hide();
|
||||
});
|
||||
};
|
||||
|
||||
api.Queue(favouriteRequest);
|
||||
}
|
||||
|
||||
private void updateUser()
|
||||
{
|
||||
if (!(localUser.Value is GuestUser) && BeatmapSetInfo.OnlineID > 0)
|
||||
getBeatmapSet();
|
||||
else
|
||||
{
|
||||
Enabled.Value = false;
|
||||
current.Value = new BeatmapSetFavouriteState(false, 0);
|
||||
updateState();
|
||||
TooltipText = BeatmapsetsStrings.ShowDetailsFavouriteLogin;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
if (current.Value.Favourited)
|
||||
{
|
||||
Background.Colour = colours.Green;
|
||||
Icon.Icon = FontAwesome.Solid.Heart;
|
||||
TooltipText = BeatmapsetsStrings.ShowDetailsUnfavourite;
|
||||
}
|
||||
else
|
||||
{
|
||||
Background.Colour = colours.Gray4;
|
||||
Icon.Icon = FontAwesome.Regular.Heart;
|
||||
TooltipText = BeatmapsetsStrings.ShowDetailsFavourite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user