mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 15:47:26 +08:00
Merge branch 'master' into fix-break-info-decimal-separator
This commit is contained in:
commit
500a1363ec
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.724.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.801.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
@ -9,6 +9,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -25,6 +26,22 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
private readonly List<Stage> stages = new List<Stage>();
|
||||
|
||||
public override Quad SkinnableComponentScreenSpaceDrawQuad
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Stages.Count == 1)
|
||||
return Stages.First().ScreenSpaceDrawQuad;
|
||||
|
||||
RectangleF area = RectangleF.Empty;
|
||||
|
||||
foreach (var stage in Stages)
|
||||
area = RectangleF.Union(area, stage.ScreenSpaceDrawQuad.AABBFloat);
|
||||
|
||||
return area;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => stages.Any(s => s.ReceivePositionalInputAt(screenSpacePos));
|
||||
|
||||
public ManiaPlayfield(List<StageDefinition> stageDefinitions)
|
||||
|
@ -17,6 +17,7 @@ using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
@ -27,11 +28,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IDistanceSnapProvider? snapProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// During a transform, the initial origin is stored so it can be used throughout the operation.
|
||||
/// </summary>
|
||||
private Vector2? referenceOrigin;
|
||||
|
||||
/// <summary>
|
||||
/// During a transform, the initial path types of a single selected slider are stored so they
|
||||
/// can be maintained throughout the operation.
|
||||
@ -42,9 +38,8 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
base.OnSelectionChanged();
|
||||
|
||||
Quad quad = selectedMovableObjects.Length > 0 ? getSurroundingQuad(selectedMovableObjects) : new Quad();
|
||||
Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad();
|
||||
|
||||
SelectionBox.CanRotate = quad.Width > 0 || quad.Height > 0;
|
||||
SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0;
|
||||
SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0;
|
||||
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
|
||||
@ -53,7 +48,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
protected override void OnOperationEnded()
|
||||
{
|
||||
base.OnOperationEnded();
|
||||
referenceOrigin = null;
|
||||
referencePathTypes = null;
|
||||
}
|
||||
|
||||
@ -109,13 +103,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
var hitObjects = selectedMovableObjects;
|
||||
|
||||
var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : getSurroundingQuad(hitObjects);
|
||||
var flipQuad = flipOverOrigin ? new Quad(0, 0, OsuPlayfield.BASE_SIZE.X, OsuPlayfield.BASE_SIZE.Y) : GeometryUtils.GetSurroundingQuad(hitObjects);
|
||||
|
||||
bool didFlip = false;
|
||||
|
||||
foreach (var h in hitObjects)
|
||||
{
|
||||
var flippedPosition = GetFlippedPosition(direction, flipQuad, h.Position);
|
||||
var flippedPosition = GeometryUtils.GetFlippedPosition(direction, flipQuad, h.Position);
|
||||
|
||||
if (!Precision.AlmostEquals(flippedPosition, h.Position))
|
||||
{
|
||||
@ -169,34 +163,13 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
|
||||
}
|
||||
|
||||
public override bool HandleRotation(float delta)
|
||||
{
|
||||
var hitObjects = selectedMovableObjects;
|
||||
|
||||
Quad quad = getSurroundingQuad(hitObjects);
|
||||
|
||||
referenceOrigin ??= quad.Centre;
|
||||
|
||||
foreach (var h in hitObjects)
|
||||
{
|
||||
h.Position = RotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta);
|
||||
|
||||
if (h is IHasPath path)
|
||||
{
|
||||
foreach (PathControlPoint cp in path.Path.ControlPoints)
|
||||
cp.Position = RotatePointAroundOrigin(cp.Position, Vector2.Zero, delta);
|
||||
}
|
||||
}
|
||||
|
||||
// this isn't always the case but let's be lenient for now.
|
||||
return true;
|
||||
}
|
||||
public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler();
|
||||
|
||||
private void scaleSlider(Slider slider, Vector2 scale)
|
||||
{
|
||||
referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type).ToList();
|
||||
|
||||
Quad sliderQuad = GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position));
|
||||
Quad sliderQuad = GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position));
|
||||
|
||||
// Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
|
||||
scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size;
|
||||
@ -222,7 +195,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
slider.SnapTo(snapProvider);
|
||||
|
||||
//if sliderhead or sliderend end up outside playfield, revert scaling.
|
||||
Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider });
|
||||
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
|
||||
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
|
||||
|
||||
if (xInBounds && yInBounds && slider.Path.HasValidLength)
|
||||
@ -238,10 +211,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
|
||||
{
|
||||
scale = getClampedScale(hitObjects, reference, scale);
|
||||
Quad selectionQuad = getSurroundingQuad(hitObjects);
|
||||
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);
|
||||
|
||||
foreach (var h in hitObjects)
|
||||
h.Position = GetScaledPosition(reference, scale, selectionQuad, h.Position);
|
||||
h.Position = GeometryUtils.GetScaledPosition(reference, scale, selectionQuad, h.Position);
|
||||
}
|
||||
|
||||
private (bool X, bool Y) isQuadInBounds(Quad quad)
|
||||
@ -256,7 +229,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
var hitObjects = selectedMovableObjects;
|
||||
|
||||
Quad quad = getSurroundingQuad(hitObjects);
|
||||
Quad quad = GeometryUtils.GetSurroundingQuad(hitObjects);
|
||||
|
||||
Vector2 delta = Vector2.Zero;
|
||||
|
||||
@ -286,7 +259,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
|
||||
float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
|
||||
|
||||
Quad selectionQuad = getSurroundingQuad(hitObjects);
|
||||
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);
|
||||
|
||||
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
|
||||
Quad scaledQuad = new Quad(selectionQuad.TopLeft.X + xOffset, selectionQuad.TopLeft.Y + yOffset, selectionQuad.Width + scale.X, selectionQuad.Height + scale.Y);
|
||||
@ -311,26 +284,6 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
return scale;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a gamefield-space quad surrounding the provided hit objects.
|
||||
/// </summary>
|
||||
/// <param name="hitObjects">The hit objects to calculate a quad for.</param>
|
||||
private Quad getSurroundingQuad(OsuHitObject[] hitObjects) =>
|
||||
GetSurroundingQuad(hitObjects.SelectMany(h =>
|
||||
{
|
||||
if (h is IHasPath path)
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
h.Position,
|
||||
// can't use EndPosition for reverse slider cases.
|
||||
h.Position + path.Path.PositionAt(1)
|
||||
};
|
||||
}
|
||||
|
||||
return new[] { h.Position };
|
||||
}));
|
||||
|
||||
/// <summary>
|
||||
/// All osu! hitobjects which can be moved/rotated/scaled.
|
||||
/// </summary>
|
||||
|
107
osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs
Normal file
107
osu.Game.Rulesets.Osu/Edit/OsuSelectionRotationHandler.cs
Normal file
@ -0,0 +1,107 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public partial class OsuSelectionRotationHandler : SelectionRotationHandler
|
||||
{
|
||||
[Resolved]
|
||||
private IEditorChangeHandler? changeHandler { get; set; }
|
||||
|
||||
private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(EditorBeatmap editorBeatmap)
|
||||
{
|
||||
selectedItems.BindTo(editorBeatmap.SelectedHitObjects);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
selectedItems.CollectionChanged += (_, __) => updateState();
|
||||
updateState();
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects);
|
||||
CanRotate.Value = quad.Width > 0 || quad.Height > 0;
|
||||
}
|
||||
|
||||
private OsuHitObject[]? objectsInRotation;
|
||||
|
||||
private Vector2? defaultOrigin;
|
||||
private Dictionary<OsuHitObject, Vector2>? originalPositions;
|
||||
private Dictionary<IHasPath, Vector2[]>? originalPathControlPointPositions;
|
||||
|
||||
public override void Begin()
|
||||
{
|
||||
if (objectsInRotation != null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!");
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
objectsInRotation = selectedMovableObjects.ToArray();
|
||||
defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation).Centre;
|
||||
originalPositions = objectsInRotation.ToDictionary(obj => obj, obj => obj.Position);
|
||||
originalPathControlPointPositions = objectsInRotation.OfType<IHasPath>().ToDictionary(
|
||||
obj => obj,
|
||||
obj => obj.Path.ControlPoints.Select(point => point.Position).ToArray());
|
||||
}
|
||||
|
||||
public override void Update(float rotation, Vector2? origin = null)
|
||||
{
|
||||
if (objectsInRotation == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!");
|
||||
|
||||
Debug.Assert(originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null);
|
||||
|
||||
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
|
||||
|
||||
foreach (var ho in objectsInRotation)
|
||||
{
|
||||
ho.Position = GeometryUtils.RotatePointAroundOrigin(originalPositions[ho], actualOrigin, rotation);
|
||||
|
||||
if (ho is IHasPath withPath)
|
||||
{
|
||||
var originalPath = originalPathControlPointPositions[withPath];
|
||||
|
||||
for (int i = 0; i < withPath.Path.ControlPoints.Count; ++i)
|
||||
withPath.Path.ControlPoints[i].Position = GeometryUtils.RotatePointAroundOrigin(originalPath[i], Vector2.Zero, rotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Commit()
|
||||
{
|
||||
if (objectsInRotation == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
|
||||
|
||||
changeHandler?.EndChange();
|
||||
|
||||
objectsInRotation = null;
|
||||
originalPositions = null;
|
||||
originalPathControlPointPositions = null;
|
||||
defaultOrigin = null;
|
||||
}
|
||||
|
||||
private IEnumerable<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()
|
||||
.Where(h => h is not Spinner);
|
||||
}
|
||||
}
|
@ -125,13 +125,13 @@ namespace osu.Game.Tests.Visual.Background
|
||||
createFakeStoryboard();
|
||||
AddStep("Enable Storyboard", () =>
|
||||
{
|
||||
player.ReplacesBackground.Value = true;
|
||||
player.StoryboardReplacesBackground.Value = true;
|
||||
player.StoryboardEnabled.Value = true;
|
||||
});
|
||||
AddUntilStep("Background is invisible, storyboard is visible", () => songSelect.IsBackgroundInvisible() && player.IsStoryboardVisible);
|
||||
AddUntilStep("Background is black, storyboard is visible", () => songSelect.IsBackgroundVisible() && songSelect.IsBackgroundBlack() && player.IsStoryboardVisible);
|
||||
AddStep("Disable Storyboard", () =>
|
||||
{
|
||||
player.ReplacesBackground.Value = false;
|
||||
player.StoryboardReplacesBackground.Value = false;
|
||||
player.StoryboardEnabled.Value = false;
|
||||
});
|
||||
AddUntilStep("Background is visible, storyboard is invisible", () => songSelect.IsBackgroundVisible() && !player.IsStoryboardVisible);
|
||||
@ -173,7 +173,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
createFakeStoryboard();
|
||||
AddStep("Enable Storyboard", () =>
|
||||
{
|
||||
player.ReplacesBackground.Value = true;
|
||||
player.StoryboardReplacesBackground.Value = true;
|
||||
player.StoryboardEnabled.Value = true;
|
||||
});
|
||||
AddStep("Enable user dim", () => player.DimmableStoryboard.IgnoreUserSettings.Value = false);
|
||||
@ -188,7 +188,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
{
|
||||
performFullSetup();
|
||||
createFakeStoryboard();
|
||||
AddStep("Enable replacing background", () => player.ReplacesBackground.Value = true);
|
||||
AddStep("Enable replacing background", () => player.StoryboardReplacesBackground.Value = true);
|
||||
|
||||
AddUntilStep("Storyboard is invisible", () => !player.IsStoryboardVisible);
|
||||
AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible());
|
||||
@ -199,11 +199,11 @@ namespace osu.Game.Tests.Visual.Background
|
||||
player.DimmableStoryboard.IgnoreUserSettings.Value = true;
|
||||
});
|
||||
AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
|
||||
AddUntilStep("Background is invisible", () => songSelect.IsBackgroundInvisible());
|
||||
AddUntilStep("Background is dimmed", () => songSelect.IsBackgroundVisible() && songSelect.IsBackgroundBlack());
|
||||
|
||||
AddStep("Disable background replacement", () => player.ReplacesBackground.Value = false);
|
||||
AddStep("Disable background replacement", () => player.StoryboardReplacesBackground.Value = false);
|
||||
AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
|
||||
AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible());
|
||||
AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible() && !songSelect.IsBackgroundBlack());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -257,7 +257,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
private void createFakeStoryboard() => AddStep("Create storyboard", () =>
|
||||
{
|
||||
player.StoryboardEnabled.Value = false;
|
||||
player.ReplacesBackground.Value = false;
|
||||
player.StoryboardReplacesBackground.Value = false;
|
||||
player.DimmableStoryboard.Add(new OsuSpriteText
|
||||
{
|
||||
Size = new Vector2(500, 50),
|
||||
@ -323,6 +323,8 @@ namespace osu.Game.Tests.Visual.Background
|
||||
config.BindWith(OsuSetting.BlurLevel, BlurLevel);
|
||||
}
|
||||
|
||||
public bool IsBackgroundBlack() => background.CurrentColour == OsuColour.Gray(0);
|
||||
|
||||
public bool IsBackgroundDimmed() => background.CurrentColour == OsuColour.Gray(1f - background.CurrentDim);
|
||||
|
||||
public bool IsBackgroundUndimmed() => background.CurrentColour == Color4.White;
|
||||
@ -331,8 +333,6 @@ namespace osu.Game.Tests.Visual.Background
|
||||
|
||||
public bool IsUserBlurDisabled() => background.CurrentBlur == new Vector2(0);
|
||||
|
||||
public bool IsBackgroundInvisible() => background.CurrentAlpha == 0;
|
||||
|
||||
public bool IsBackgroundVisible() => background.CurrentAlpha == 1;
|
||||
|
||||
public bool IsBackgroundBlur() => Precision.AlmostEquals(background.CurrentBlur, new Vector2(BACKGROUND_BLUR), 0.1f);
|
||||
@ -367,7 +367,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
{
|
||||
base.OnEntering(e);
|
||||
|
||||
ApplyToBackground(b => ReplacesBackground.BindTo(b.StoryboardReplacesBackground));
|
||||
ApplyToBackground(b => StoryboardReplacesBackground.BindTo(b.StoryboardReplacesBackground));
|
||||
}
|
||||
|
||||
public new DimmableStoryboard DimmableStoryboard => base.DimmableStoryboard;
|
||||
@ -376,7 +376,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
public bool BlockLoad;
|
||||
|
||||
public Bindable<bool> StoryboardEnabled;
|
||||
public readonly Bindable<bool> ReplacesBackground = new Bindable<bool>();
|
||||
public readonly Bindable<bool> StoryboardReplacesBackground = new Bindable<bool>();
|
||||
public readonly Bindable<bool> IsPaused = new Bindable<bool>();
|
||||
|
||||
public LoadBlockingTestPlayer(bool allowPause = true)
|
||||
|
@ -3,8 +3,11 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
@ -20,6 +23,14 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
private Container selectionArea;
|
||||
private SelectionBox selectionBox;
|
||||
|
||||
[Cached(typeof(SelectionRotationHandler))]
|
||||
private TestSelectionRotationHandler rotationHandler;
|
||||
|
||||
public TestSceneComposeSelectBox()
|
||||
{
|
||||
rotationHandler = new TestSelectionRotationHandler(() => selectionArea);
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
@ -34,13 +45,11 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
|
||||
CanRotate = true,
|
||||
CanScaleX = true,
|
||||
CanScaleY = true,
|
||||
CanFlipX = true,
|
||||
CanFlipY = true,
|
||||
|
||||
OnRotation = handleRotation,
|
||||
OnScale = handleScale
|
||||
}
|
||||
}
|
||||
@ -71,11 +80,48 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool handleRotation(float angle)
|
||||
private partial class TestSelectionRotationHandler : SelectionRotationHandler
|
||||
{
|
||||
// kinda silly and wrong, but just showing that the drag handles work.
|
||||
selectionArea.Rotation += angle;
|
||||
return true;
|
||||
private readonly Func<Container> getTargetContainer;
|
||||
|
||||
public TestSelectionRotationHandler(Func<Container> getTargetContainer)
|
||||
{
|
||||
this.getTargetContainer = getTargetContainer;
|
||||
|
||||
CanRotate.Value = true;
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
private Container targetContainer;
|
||||
|
||||
private float? initialRotation;
|
||||
|
||||
public override void Begin()
|
||||
{
|
||||
if (targetContainer != null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!");
|
||||
|
||||
targetContainer = getTargetContainer();
|
||||
initialRotation = targetContainer!.Rotation;
|
||||
}
|
||||
|
||||
public override void Update(float rotation, Vector2? origin = null)
|
||||
{
|
||||
if (targetContainer == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!");
|
||||
|
||||
// kinda silly and wrong, but just showing that the drag handles work.
|
||||
targetContainer.Rotation = initialRotation!.Value + rotation;
|
||||
}
|
||||
|
||||
public override void Commit()
|
||||
{
|
||||
if (targetContainer == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
|
||||
|
||||
targetContainer = null;
|
||||
initialRotation = null;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -8,6 +8,7 @@ using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.SkinEditor;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
@ -19,7 +20,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestToggleEditor()
|
||||
{
|
||||
AddStep("show available components", () => SetContents(_ => new SkinComponentToolbox
|
||||
var skinComponentsContainer = new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.SongSelect));
|
||||
|
||||
AddStep("show available components", () => SetContents(_ => new SkinComponentToolbox(skinComponentsContainer, null)
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
|
@ -65,6 +65,34 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddStep("clear playing users", () => playingUsers.Clear());
|
||||
}
|
||||
|
||||
[TestCase(1)]
|
||||
[TestCase(4)]
|
||||
[TestCase(9)]
|
||||
public void TestGeneral(int count)
|
||||
{
|
||||
int[] userIds = getPlayerIds(count);
|
||||
|
||||
start(userIds);
|
||||
loadSpectateScreen();
|
||||
|
||||
sendFrames(userIds, 1000);
|
||||
AddWaitStep("wait a bit", 20);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultipleStartRequests()
|
||||
{
|
||||
int[] userIds = getPlayerIds(2);
|
||||
|
||||
start(userIds);
|
||||
loadSpectateScreen();
|
||||
|
||||
sendFrames(userIds, 1000);
|
||||
AddWaitStep("wait a bit", 20);
|
||||
|
||||
start(userIds);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDelayedStart()
|
||||
{
|
||||
@ -88,18 +116,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddUntilStep("two players added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGeneral()
|
||||
{
|
||||
int[] userIds = getPlayerIds(4);
|
||||
|
||||
start(userIds);
|
||||
loadSpectateScreen();
|
||||
|
||||
sendFrames(userIds, 1000);
|
||||
AddWaitStep("wait a bit", 20);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSpectatorPlayerInteractiveElementsHidden()
|
||||
{
|
||||
|
@ -170,6 +170,39 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddUntilStep("time is correct", () => getEditor().ChildrenOfType<EditorClock>().First().CurrentTime, () => Is.EqualTo(1234));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAttemptGlobalMusicOperationFromEditor()
|
||||
{
|
||||
BeatmapSetInfo beatmapSet = null!;
|
||||
|
||||
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
||||
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
||||
|
||||
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
||||
AddUntilStep("wait for song select",
|
||||
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
||||
&& songSelect.IsLoaded);
|
||||
|
||||
AddUntilStep("wait for music playing", () => Game.MusicController.IsPlaying);
|
||||
AddStep("user request stop", () => Game.MusicController.Stop(requestedByUser: true));
|
||||
AddUntilStep("wait for music stopped", () => !Game.MusicController.IsPlaying);
|
||||
|
||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||
|
||||
AddUntilStep("music still stopped", () => !Game.MusicController.IsPlaying);
|
||||
AddStep("user request play", () => Game.MusicController.Play(requestedByUser: true));
|
||||
AddUntilStep("music still stopped", () => !Game.MusicController.IsPlaying);
|
||||
|
||||
AddStep("exit to song select", () => Game.PerformFromScreen(_ => { }, typeof(PlaySongSelect).Yield()));
|
||||
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
|
||||
|
||||
AddUntilStep("wait for music playing", () => Game.MusicController.IsPlaying);
|
||||
AddStep("user request stop", () => Game.MusicController.Stop(requestedByUser: true));
|
||||
AddUntilStep("wait for music stopped", () => !Game.MusicController.IsPlaying);
|
||||
}
|
||||
|
||||
private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType<EditorBeatmap>().Single();
|
||||
|
||||
private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen;
|
||||
|
@ -13,6 +13,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.Play;
|
||||
@ -86,6 +87,29 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddAssert("did perform", () => actionPerformed);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPerformAtMenuFromPlayerLoaderWithAutoplayShortcut()
|
||||
{
|
||||
importAndWaitForSongSelect();
|
||||
|
||||
AddStep("press ctrl+enter", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Key(Key.Enter);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
|
||||
AddUntilStep("Wait for new screen", () => Game.ScreenStack.CurrentScreen is PlayerLoader);
|
||||
|
||||
AddAssert("Mods include autoplay", () => Game.SelectedMods.Value.Any(m => m is ModAutoplay));
|
||||
|
||||
AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true));
|
||||
AddUntilStep("returned to main menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
|
||||
AddAssert("did perform", () => actionPerformed);
|
||||
|
||||
AddAssert("Mods don't include autoplay", () => !Game.SelectedMods.Value.Any(m => m is ModAutoplay));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPerformEnsuresScreenIsLoaded()
|
||||
{
|
||||
|
@ -154,7 +154,14 @@ namespace osu.Game.Tests.Visual.Online
|
||||
Type = ChangelogEntryType.Misc,
|
||||
Category = "Code quality",
|
||||
Title = "Clean up another thing"
|
||||
}
|
||||
},
|
||||
new APIChangelogEntry
|
||||
{
|
||||
Type = ChangelogEntryType.Add,
|
||||
Category = "osu!",
|
||||
Title = "Add entry with news url",
|
||||
Url = "https://osu.ppy.sh/home/news/2023-07-27-summer-splash"
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -56,38 +56,38 @@ namespace osu.Game.Tests.Visual
|
||||
public void AllowTrackAdjustmentsTest()
|
||||
{
|
||||
AddStep("push allowing screen", () => stack.Push(loadNewScreen<AllowScreen>()));
|
||||
AddAssert("allows adjustments 1", () => musicController.AllowTrackAdjustments);
|
||||
AddAssert("allows adjustments 1", () => musicController.ApplyModTrackAdjustments);
|
||||
|
||||
AddStep("push inheriting screen", () => stack.Push(loadNewScreen<InheritScreen>()));
|
||||
AddAssert("allows adjustments 2", () => musicController.AllowTrackAdjustments);
|
||||
AddAssert("allows adjustments 2", () => musicController.ApplyModTrackAdjustments);
|
||||
|
||||
AddStep("push disallowing screen", () => stack.Push(loadNewScreen<DisallowScreen>()));
|
||||
AddAssert("disallows adjustments 3", () => !musicController.AllowTrackAdjustments);
|
||||
AddAssert("disallows adjustments 3", () => !musicController.ApplyModTrackAdjustments);
|
||||
|
||||
AddStep("push inheriting screen", () => stack.Push(loadNewScreen<InheritScreen>()));
|
||||
AddAssert("disallows adjustments 4", () => !musicController.AllowTrackAdjustments);
|
||||
AddAssert("disallows adjustments 4", () => !musicController.ApplyModTrackAdjustments);
|
||||
|
||||
AddStep("push inheriting screen", () => stack.Push(loadNewScreen<InheritScreen>()));
|
||||
AddAssert("disallows adjustments 5", () => !musicController.AllowTrackAdjustments);
|
||||
AddAssert("disallows adjustments 5", () => !musicController.ApplyModTrackAdjustments);
|
||||
|
||||
AddStep("push allowing screen", () => stack.Push(loadNewScreen<AllowScreen>()));
|
||||
AddAssert("allows adjustments 6", () => musicController.AllowTrackAdjustments);
|
||||
AddAssert("allows adjustments 6", () => musicController.ApplyModTrackAdjustments);
|
||||
|
||||
// Now start exiting from screens
|
||||
AddStep("exit screen", () => stack.Exit());
|
||||
AddAssert("disallows adjustments 7", () => !musicController.AllowTrackAdjustments);
|
||||
AddAssert("disallows adjustments 7", () => !musicController.ApplyModTrackAdjustments);
|
||||
|
||||
AddStep("exit screen", () => stack.Exit());
|
||||
AddAssert("disallows adjustments 8", () => !musicController.AllowTrackAdjustments);
|
||||
AddAssert("disallows adjustments 8", () => !musicController.ApplyModTrackAdjustments);
|
||||
|
||||
AddStep("exit screen", () => stack.Exit());
|
||||
AddAssert("disallows adjustments 9", () => !musicController.AllowTrackAdjustments);
|
||||
AddAssert("disallows adjustments 9", () => !musicController.ApplyModTrackAdjustments);
|
||||
|
||||
AddStep("exit screen", () => stack.Exit());
|
||||
AddAssert("allows adjustments 10", () => musicController.AllowTrackAdjustments);
|
||||
AddAssert("allows adjustments 10", () => musicController.ApplyModTrackAdjustments);
|
||||
|
||||
AddStep("exit screen", () => stack.Exit());
|
||||
AddAssert("allows adjustments 11", () => musicController.AllowTrackAdjustments);
|
||||
AddAssert("allows adjustments 11", () => musicController.ApplyModTrackAdjustments);
|
||||
}
|
||||
|
||||
public partial class TestScreen : ScreenWithBeatmapBackground
|
||||
@ -129,12 +129,12 @@ namespace osu.Game.Tests.Visual
|
||||
|
||||
private partial class AllowScreen : OsuScreen
|
||||
{
|
||||
public override bool? AllowTrackAdjustments => true;
|
||||
public override bool? ApplyModTrackAdjustments => true;
|
||||
}
|
||||
|
||||
public partial class DisallowScreen : OsuScreen
|
||||
{
|
||||
public override bool? AllowTrackAdjustments => false;
|
||||
public override bool? ApplyModTrackAdjustments => false;
|
||||
}
|
||||
|
||||
private partial class InheritScreen : OsuScreen
|
||||
|
@ -99,16 +99,18 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
TestUpdateableOnlineBeatmapSetCover updateableCover = null;
|
||||
|
||||
AddStep("setup cover", () => Child = updateableCover = new TestUpdateableOnlineBeatmapSetCover
|
||||
AddStep("setup cover", () => Child = updateableCover = new TestUpdateableOnlineBeatmapSetCover(400)
|
||||
{
|
||||
OnlineInfo = CreateAPIBeatmapSet(),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
});
|
||||
|
||||
AddStep("change model", () => updateableCover.OnlineInfo = null);
|
||||
AddWaitStep("wait some", 5);
|
||||
AddAssert("no cover added", () => !updateableCover.ChildrenOfType<DelayedLoadUnloadWrapper>().Any());
|
||||
AddStep("change model to null", () => updateableCover.OnlineInfo = null);
|
||||
|
||||
AddUntilStep("wait for load", () => updateableCover.DelayedLoadFinished);
|
||||
|
||||
AddAssert("no cover added", () => !updateableCover.ChildrenOfType<TestOnlineBeatmapSetCover>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -143,11 +145,19 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
private readonly int loadDelay;
|
||||
|
||||
public bool DelayedLoadFinished;
|
||||
|
||||
public TestUpdateableOnlineBeatmapSetCover(int loadDelay = 10000)
|
||||
{
|
||||
this.loadDelay = loadDelay;
|
||||
}
|
||||
|
||||
protected override void OnLoadFinished()
|
||||
{
|
||||
base.OnLoadFinished();
|
||||
DelayedLoadFinished = true;
|
||||
}
|
||||
|
||||
protected override Drawable CreateDrawable(IBeatmapSetOnlineInfo model)
|
||||
{
|
||||
if (model == null)
|
||||
|
@ -124,7 +124,7 @@ namespace osu.Game.Beatmaps
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (Source != null && Source is not IAdjustableClock && Source.CurrentTime < decoupledClock.CurrentTime)
|
||||
if (Source != null && Source is not IAdjustableClock && Source.CurrentTime < decoupledClock.CurrentTime - 100)
|
||||
{
|
||||
// InterpolatingFramedClock won't interpolate backwards unless its source has an ElapsedFrameTime.
|
||||
// See https://github.com/ppy/osu-framework/blob/ba1385330cc501f34937e08257e586c84e35d772/osu.Framework/Timing/InterpolatingFramedClock.cs#L91-L93
|
||||
|
@ -24,15 +24,13 @@ namespace osu.Game.Graphics.Containers
|
||||
public const double BACKGROUND_FADE_DURATION = 800;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not user-configured settings relating to brightness of elements should be ignored
|
||||
/// Whether or not user-configured settings relating to brightness of elements should be ignored.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For best or worst, this also bypasses storyboard disable. Not sure this is correct but leaving it as to not break anything.
|
||||
/// </remarks>
|
||||
public readonly Bindable<bool> IgnoreUserSettings = new Bindable<bool>();
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the storyboard loaded should completely hide the background behind it.
|
||||
/// </summary>
|
||||
public readonly Bindable<bool> StoryboardReplacesBackground = new Bindable<bool>();
|
||||
|
||||
/// <summary>
|
||||
/// Whether player is in break time.
|
||||
/// Must be bound to <see cref="BreakTracker.IsBreakTime"/> to allow for dim adjustments in gameplay.
|
||||
@ -57,7 +55,7 @@ namespace osu.Game.Graphics.Containers
|
||||
|
||||
private float breakLightening => LightenDuringBreaks.Value && IsBreakTime.Value ? BREAK_LIGHTEN_AMOUNT : 0;
|
||||
|
||||
protected float DimLevel => Math.Max(!IgnoreUserSettings.Value ? (float)UserDimLevel.Value - breakLightening : DimWhenUserSettingsIgnored.Value, 0);
|
||||
protected virtual float DimLevel => Math.Max(!IgnoreUserSettings.Value ? (float)UserDimLevel.Value - breakLightening : DimWhenUserSettingsIgnored.Value, 0);
|
||||
|
||||
protected override Container<Drawable> Content => dimContent;
|
||||
|
||||
@ -83,7 +81,6 @@ namespace osu.Game.Graphics.Containers
|
||||
LightenDuringBreaks.ValueChanged += _ => UpdateVisuals();
|
||||
IsBreakTime.ValueChanged += _ => UpdateVisuals();
|
||||
ShowStoryboard.ValueChanged += _ => UpdateVisuals();
|
||||
StoryboardReplacesBackground.ValueChanged += _ => UpdateVisuals();
|
||||
IgnoreUserSettings.ValueChanged += _ => UpdateVisuals();
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,7 @@ namespace osu.Game.IO.Archives
|
||||
{
|
||||
ZipArchiveEntry entry = archive.Entries.SingleOrDefault(e => e.Key == name);
|
||||
if (entry == null)
|
||||
throw new FileNotFoundException();
|
||||
return null;
|
||||
|
||||
var owner = MemoryAllocator.Default.Allocate<byte>((int)entry.Size);
|
||||
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@ -29,6 +30,9 @@ namespace osu.Game.Overlays.Changelog
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private ILinkHandler? linkHandler { get; set; }
|
||||
|
||||
private FontUsage fontLarge;
|
||||
private FontUsage fontMedium;
|
||||
|
||||
@ -86,11 +90,21 @@ namespace osu.Game.Overlays.Changelog
|
||||
}
|
||||
};
|
||||
|
||||
title.AddText(entry.Title, t =>
|
||||
if (string.IsNullOrEmpty(entry.Url))
|
||||
{
|
||||
t.Font = fontLarge;
|
||||
t.Colour = entryColour;
|
||||
});
|
||||
title.AddText(entry.Title, t =>
|
||||
{
|
||||
t.Font = fontLarge;
|
||||
t.Colour = entryColour;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
title.AddLink(entry.Title, () => linkHandler?.HandleLink(entry.Url), entry.Url, t =>
|
||||
{
|
||||
t.Font = fontLarge;
|
||||
});
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(entry.Repository) && !string.IsNullOrEmpty(entry.GithubUrl))
|
||||
addRepositoryReference(title, entryColour);
|
||||
|
@ -104,7 +104,7 @@ namespace osu.Game.Overlays.FirstRunSetup
|
||||
{
|
||||
protected override bool ControlGlobalMusic => false;
|
||||
|
||||
public override bool? AllowTrackAdjustments => false;
|
||||
public override bool? ApplyModTrackAdjustments => false;
|
||||
}
|
||||
|
||||
private partial class UIScaleSlider : RoundedSliderBar<float>
|
||||
|
@ -30,20 +30,14 @@ namespace osu.Game.Overlays.Music
|
||||
[Resolved]
|
||||
private OnScreenDisplay? onScreenDisplay { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private OsuGame game { get; set; } = null!;
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
if (e.Repeat)
|
||||
if (e.Repeat || !musicController.AllowTrackControl.Value)
|
||||
return false;
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.MusicPlay:
|
||||
if (game.LocalUserPlaying.Value)
|
||||
return false;
|
||||
|
||||
// use previous state as TogglePause may not update the track's state immediately (state update is run on the audio thread see https://github.com/ppy/osu/issues/9880#issuecomment-674668842)
|
||||
bool wasPlaying = musicController.IsPlaying;
|
||||
|
||||
|
@ -40,6 +40,11 @@ namespace osu.Game.Overlays
|
||||
/// </summary>
|
||||
public bool UserPauseRequested { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether user control of the global track should be allowed.
|
||||
/// </summary>
|
||||
public readonly BindableBool AllowTrackControl = new BindableBool(true);
|
||||
|
||||
/// <summary>
|
||||
/// Fired when the global <see cref="WorkingBeatmap"/> has changed.
|
||||
/// Includes direction information for display purposes.
|
||||
@ -92,8 +97,10 @@ namespace osu.Game.Overlays
|
||||
seekDelegate?.Cancel();
|
||||
seekDelegate = Schedule(() =>
|
||||
{
|
||||
if (!beatmap.Disabled)
|
||||
CurrentTrack.Seek(position);
|
||||
if (beatmap.Disabled || !AllowTrackControl.Value)
|
||||
return;
|
||||
|
||||
CurrentTrack.Seek(position);
|
||||
});
|
||||
}
|
||||
|
||||
@ -107,7 +114,7 @@ namespace osu.Game.Overlays
|
||||
|
||||
if (CurrentTrack.IsDummyDevice || beatmap.Value.BeatmapSetInfo.DeletePending)
|
||||
{
|
||||
if (beatmap.Disabled)
|
||||
if (beatmap.Disabled || !AllowTrackControl.Value)
|
||||
return;
|
||||
|
||||
Logger.Log($"{nameof(MusicController)} skipping next track to {nameof(EnsurePlayingSomething)}");
|
||||
@ -132,6 +139,9 @@ namespace osu.Game.Overlays
|
||||
/// <returns>Whether the operation was successful.</returns>
|
||||
public bool Play(bool restart = false, bool requestedByUser = false)
|
||||
{
|
||||
if (requestedByUser && !AllowTrackControl.Value)
|
||||
return false;
|
||||
|
||||
if (requestedByUser)
|
||||
UserPauseRequested = false;
|
||||
|
||||
@ -153,6 +163,9 @@ namespace osu.Game.Overlays
|
||||
/// </param>
|
||||
public void Stop(bool requestedByUser = false)
|
||||
{
|
||||
if (requestedByUser && !AllowTrackControl.Value)
|
||||
return;
|
||||
|
||||
UserPauseRequested |= requestedByUser;
|
||||
if (CurrentTrack.IsRunning)
|
||||
CurrentTrack.StopAsync();
|
||||
@ -164,6 +177,9 @@ namespace osu.Game.Overlays
|
||||
/// <returns>Whether the operation was successful.</returns>
|
||||
public bool TogglePause()
|
||||
{
|
||||
if (!AllowTrackControl.Value)
|
||||
return false;
|
||||
|
||||
if (CurrentTrack.IsRunning)
|
||||
Stop(true);
|
||||
else
|
||||
@ -189,7 +205,7 @@ namespace osu.Game.Overlays
|
||||
/// <returns>The <see cref="PreviousTrackResult"/> that indicate the decided action.</returns>
|
||||
private PreviousTrackResult prev()
|
||||
{
|
||||
if (beatmap.Disabled)
|
||||
if (beatmap.Disabled || !AllowTrackControl.Value)
|
||||
return PreviousTrackResult.None;
|
||||
|
||||
double currentTrackPosition = CurrentTrack.CurrentTime;
|
||||
@ -229,7 +245,7 @@ namespace osu.Game.Overlays
|
||||
|
||||
private bool next()
|
||||
{
|
||||
if (beatmap.Disabled)
|
||||
if (beatmap.Disabled || !AllowTrackControl.Value)
|
||||
return false;
|
||||
|
||||
queuedDirection = TrackChangeDirection.Next;
|
||||
@ -352,24 +368,24 @@ namespace osu.Game.Overlays
|
||||
|
||||
private void onTrackCompleted()
|
||||
{
|
||||
if (!CurrentTrack.Looping && !beatmap.Disabled)
|
||||
if (!CurrentTrack.Looping && !beatmap.Disabled && AllowTrackControl.Value)
|
||||
NextTrack();
|
||||
}
|
||||
|
||||
private bool allowTrackAdjustments;
|
||||
private bool applyModTrackAdjustments;
|
||||
|
||||
/// <summary>
|
||||
/// Whether mod track adjustments are allowed to be applied.
|
||||
/// </summary>
|
||||
public bool AllowTrackAdjustments
|
||||
public bool ApplyModTrackAdjustments
|
||||
{
|
||||
get => allowTrackAdjustments;
|
||||
get => applyModTrackAdjustments;
|
||||
set
|
||||
{
|
||||
if (allowTrackAdjustments == value)
|
||||
if (applyModTrackAdjustments == value)
|
||||
return;
|
||||
|
||||
allowTrackAdjustments = value;
|
||||
applyModTrackAdjustments = value;
|
||||
ResetTrackAdjustments();
|
||||
}
|
||||
}
|
||||
@ -377,7 +393,7 @@ namespace osu.Game.Overlays
|
||||
private AudioAdjustments modTrackAdjustments;
|
||||
|
||||
/// <summary>
|
||||
/// Resets the adjustments currently applied on <see cref="CurrentTrack"/> and applies the mod adjustments if <see cref="AllowTrackAdjustments"/> is <c>true</c>.
|
||||
/// Resets the adjustments currently applied on <see cref="CurrentTrack"/> and applies the mod adjustments if <see cref="ApplyModTrackAdjustments"/> is <c>true</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Does not reset any adjustments applied directly to the beatmap track.
|
||||
@ -390,7 +406,7 @@ namespace osu.Game.Overlays
|
||||
CurrentTrack.RemoveAllAdjustments(AdjustableProperty.Tempo);
|
||||
CurrentTrack.RemoveAllAdjustments(AdjustableProperty.Volume);
|
||||
|
||||
if (allowTrackAdjustments)
|
||||
if (applyModTrackAdjustments)
|
||||
{
|
||||
CurrentTrack.BindAdjustments(modTrackAdjustments = new AudioAdjustments());
|
||||
|
||||
|
@ -1,13 +1,12 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
@ -40,33 +39,35 @@ namespace osu.Game.Overlays
|
||||
private const float bottom_black_area_height = 55;
|
||||
private const float margin = 10;
|
||||
|
||||
private Drawable background;
|
||||
private ProgressBar progressBar;
|
||||
private Drawable background = null!;
|
||||
private ProgressBar progressBar = null!;
|
||||
|
||||
private IconButton prevButton;
|
||||
private IconButton playButton;
|
||||
private IconButton nextButton;
|
||||
private IconButton playlistButton;
|
||||
private IconButton prevButton = null!;
|
||||
private IconButton playButton = null!;
|
||||
private IconButton nextButton = null!;
|
||||
private IconButton playlistButton = null!;
|
||||
|
||||
private SpriteText title, artist;
|
||||
private SpriteText title = null!, artist = null!;
|
||||
|
||||
private PlaylistOverlay playlist;
|
||||
private PlaylistOverlay? playlist;
|
||||
|
||||
private Container dragContainer;
|
||||
private Container playerContainer;
|
||||
private Container playlistContainer;
|
||||
private Container dragContainer = null!;
|
||||
private Container playerContainer = null!;
|
||||
private Container playlistContainer = null!;
|
||||
|
||||
protected override string PopInSampleName => "UI/now-playing-pop-in";
|
||||
protected override string PopOutSampleName => "UI/now-playing-pop-out";
|
||||
|
||||
[Resolved]
|
||||
private MusicController musicController { get; set; }
|
||||
private MusicController musicController { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private Bindable<WorkingBeatmap> beatmap { get; set; }
|
||||
private Bindable<WorkingBeatmap> beatmap { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
private Bindable<bool> allowTrackControl = null!;
|
||||
|
||||
public NowPlayingOverlay()
|
||||
{
|
||||
@ -220,8 +221,10 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
beatmap.BindDisabledChanged(_ => Scheduler.AddOnce(beatmapDisabledChanged));
|
||||
beatmapDisabledChanged();
|
||||
beatmap.BindDisabledChanged(_ => Scheduler.AddOnce(updateEnabledStates));
|
||||
|
||||
allowTrackControl = musicController.AllowTrackControl.GetBoundCopy();
|
||||
allowTrackControl.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledStates), true);
|
||||
|
||||
musicController.TrackChanged += trackChanged;
|
||||
trackChanged(beatmap.Value);
|
||||
@ -286,31 +289,34 @@ namespace osu.Game.Overlays
|
||||
}
|
||||
}
|
||||
|
||||
private Action pendingBeatmapSwitch;
|
||||
private Action? pendingBeatmapSwitch;
|
||||
|
||||
private CancellationTokenSource? backgroundLoadCancellation;
|
||||
|
||||
private WorkingBeatmap? currentBeatmap;
|
||||
|
||||
private void trackChanged(WorkingBeatmap beatmap, TrackChangeDirection direction = TrackChangeDirection.None)
|
||||
{
|
||||
currentBeatmap = beatmap;
|
||||
|
||||
// avoid using scheduler as our scheduler may not be run for a long time, holding references to beatmaps.
|
||||
pendingBeatmapSwitch = delegate
|
||||
{
|
||||
// todo: this can likely be replaced with WorkingBeatmap.GetBeatmapAsync()
|
||||
Task.Run(() =>
|
||||
{
|
||||
if (beatmap?.Beatmap == null) // this is not needed if a placeholder exists
|
||||
{
|
||||
title.Text = @"Nothing to play";
|
||||
artist.Text = @"Nothing to play";
|
||||
}
|
||||
else
|
||||
{
|
||||
BeatmapMetadata metadata = beatmap.Metadata;
|
||||
title.Text = new RomanisableString(metadata.TitleUnicode, metadata.Title);
|
||||
artist.Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist);
|
||||
}
|
||||
});
|
||||
BeatmapMetadata metadata = beatmap.Metadata;
|
||||
|
||||
title.Text = new RomanisableString(metadata.TitleUnicode, metadata.Title);
|
||||
artist.Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist);
|
||||
|
||||
backgroundLoadCancellation?.Cancel();
|
||||
|
||||
LoadComponentAsync(new Background(beatmap) { Depth = float.MaxValue }, newBackground =>
|
||||
{
|
||||
if (beatmap != currentBeatmap)
|
||||
{
|
||||
newBackground.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case TrackChangeDirection.Next:
|
||||
@ -330,27 +336,29 @@ namespace osu.Game.Overlays
|
||||
background = newBackground;
|
||||
|
||||
playerContainer.Add(newBackground);
|
||||
});
|
||||
}, (backgroundLoadCancellation = new CancellationTokenSource()).Token);
|
||||
};
|
||||
}
|
||||
|
||||
private void beatmapDisabledChanged()
|
||||
private void updateEnabledStates()
|
||||
{
|
||||
bool disabled = beatmap.Disabled;
|
||||
bool beatmapDisabled = beatmap.Disabled;
|
||||
bool trackControlDisabled = !musicController.AllowTrackControl.Value;
|
||||
|
||||
if (disabled)
|
||||
if (beatmapDisabled || trackControlDisabled)
|
||||
playlist?.Hide();
|
||||
|
||||
prevButton.Enabled.Value = !disabled;
|
||||
nextButton.Enabled.Value = !disabled;
|
||||
playlistButton.Enabled.Value = !disabled;
|
||||
prevButton.Enabled.Value = !beatmapDisabled && !trackControlDisabled;
|
||||
nextButton.Enabled.Value = !beatmapDisabled && !trackControlDisabled;
|
||||
playlistButton.Enabled.Value = !beatmapDisabled && !trackControlDisabled;
|
||||
playButton.Enabled.Value = !trackControlDisabled;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (musicController != null)
|
||||
if (musicController.IsNotNull())
|
||||
musicController.TrackChanged -= trackChanged;
|
||||
}
|
||||
|
||||
@ -383,7 +391,7 @@ namespace osu.Game.Overlays
|
||||
private readonly Sprite sprite;
|
||||
private readonly WorkingBeatmap beatmap;
|
||||
|
||||
public Background(WorkingBeatmap beatmap = null)
|
||||
public Background(WorkingBeatmap beatmap)
|
||||
: base(cachedFrameBuffer: true)
|
||||
{
|
||||
this.beatmap = beatmap;
|
||||
@ -413,7 +421,7 @@ namespace osu.Game.Overlays
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(LargeTextureStore textures)
|
||||
{
|
||||
sprite.Texture = beatmap?.GetBackground() ?? textures.Get(@"Backgrounds/bg4");
|
||||
sprite.Texture = beatmap.GetBackground() ?? textures.Get(@"Backgrounds/bg4");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ using osu.Framework.Threading;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.Edit.Components;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
@ -23,14 +24,22 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
public Action<Type>? RequestPlacement;
|
||||
|
||||
private readonly SkinComponentsContainer? target;
|
||||
private readonly SkinComponentsContainer target;
|
||||
|
||||
private readonly RulesetInfo? ruleset;
|
||||
|
||||
private FillFlowContainer fill = null!;
|
||||
|
||||
public SkinComponentToolbox(SkinComponentsContainer? target = null)
|
||||
: base(target?.Lookup.Ruleset == null ? SkinEditorStrings.Components : LocalisableString.Interpolate($"{SkinEditorStrings.Components} ({target.Lookup.Ruleset.Name})"))
|
||||
/// <summary>
|
||||
/// Create a new component toolbox for the specified taget.
|
||||
/// </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)
|
||||
: base(ruleset == null ? SkinEditorStrings.Components : LocalisableString.Interpolate($"{SkinEditorStrings.Components} ({ruleset.Name})"))
|
||||
{
|
||||
this.target = target;
|
||||
this.ruleset = ruleset;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -51,7 +60,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
fill.Clear();
|
||||
|
||||
var skinnableTypes = SerialisedDrawableInfo.GetAllAvailableDrawables(target?.Lookup.Ruleset);
|
||||
var skinnableTypes = SerialisedDrawableInfo.GetAllAvailableDrawables(ruleset);
|
||||
foreach (var type in skinnableTypes)
|
||||
attemptAddComponent(type);
|
||||
}
|
||||
|
@ -356,7 +356,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
new SettingsDropdown<SkinComponentsContainerLookup?>
|
||||
{
|
||||
Items = availableTargets.Select(t => t.Lookup),
|
||||
Items = availableTargets.Select(t => t.Lookup).Distinct(),
|
||||
Current = selectedTarget,
|
||||
}
|
||||
}
|
||||
@ -366,14 +366,14 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
// If the new target has a ruleset, let's show ruleset-specific items at the top, and the rest below.
|
||||
if (target.NewValue.Ruleset != null)
|
||||
{
|
||||
componentsSidebar.Add(new SkinComponentToolbox(skinComponentsContainer)
|
||||
componentsSidebar.Add(new SkinComponentToolbox(skinComponentsContainer, target.NewValue.Ruleset)
|
||||
{
|
||||
RequestPlacement = requestPlacement
|
||||
});
|
||||
}
|
||||
|
||||
// Remove the ruleset from the lookup to get base components.
|
||||
componentsSidebar.Add(new SkinComponentToolbox(getTarget(new SkinComponentsContainerLookup(target.NewValue.Target)))
|
||||
componentsSidebar.Add(new SkinComponentToolbox(skinComponentsContainer, null)
|
||||
{
|
||||
RequestPlacement = requestPlacement
|
||||
});
|
||||
|
@ -16,6 +16,7 @@ using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Screens.Edit.Components.Menus;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.SkinEditor
|
||||
@ -25,31 +26,10 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
[Resolved]
|
||||
private SkinEditor skinEditor { get; set; } = null!;
|
||||
|
||||
public override bool HandleRotation(float angle)
|
||||
public override SelectionRotationHandler CreateRotationHandler() => new SkinSelectionRotationHandler
|
||||
{
|
||||
if (SelectedBlueprints.Count == 1)
|
||||
{
|
||||
// for single items, rotate around the origin rather than the selection centre.
|
||||
((Drawable)SelectedBlueprints.First().Item).Rotation += angle;
|
||||
}
|
||||
else
|
||||
{
|
||||
var selectionQuad = getSelectionQuad();
|
||||
|
||||
foreach (var b in SelectedBlueprints)
|
||||
{
|
||||
var drawableItem = (Drawable)b.Item;
|
||||
|
||||
var rotatedPosition = RotatePointAroundOrigin(b.ScreenSpaceSelectionPoint, selectionQuad.Centre, angle);
|
||||
updateDrawablePosition(drawableItem, rotatedPosition);
|
||||
|
||||
drawableItem.Rotation += angle;
|
||||
}
|
||||
}
|
||||
|
||||
// this isn't always the case but let's be lenient for now.
|
||||
return true;
|
||||
}
|
||||
UpdatePosition = updateDrawablePosition
|
||||
};
|
||||
|
||||
public override bool HandleScale(Vector2 scale, Anchor anchor)
|
||||
{
|
||||
@ -137,7 +117,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
var drawableItem = (Drawable)b.Item;
|
||||
|
||||
var flippedPosition = GetFlippedPosition(direction, flipOverOrigin ? drawableItem.Parent.ScreenSpaceDrawQuad : selectionQuad, b.ScreenSpaceSelectionPoint);
|
||||
var flippedPosition = GeometryUtils.GetFlippedPosition(direction, flipOverOrigin ? drawableItem.Parent.ScreenSpaceDrawQuad : selectionQuad, b.ScreenSpaceSelectionPoint);
|
||||
|
||||
updateDrawablePosition(drawableItem, flippedPosition);
|
||||
|
||||
@ -171,7 +151,6 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
base.OnSelectionChanged();
|
||||
|
||||
SelectionBox.CanRotate = true;
|
||||
SelectionBox.CanScaleX = true;
|
||||
SelectionBox.CanScaleY = true;
|
||||
SelectionBox.CanFlipX = true;
|
||||
@ -275,7 +254,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private Quad getSelectionQuad() =>
|
||||
GetSurroundingQuad(SelectedBlueprints.SelectMany(b => b.Item.ScreenSpaceDrawQuad.GetVertices().ToArray()));
|
||||
GeometryUtils.GetSurroundingQuad(SelectedBlueprints.SelectMany(b => b.Item.ScreenSpaceDrawQuad.GetVertices().ToArray()));
|
||||
|
||||
private void applyFixedAnchors(Anchor anchor)
|
||||
{
|
||||
|
104
osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs
Normal file
104
osu.Game/Overlays/SkinEditor/SkinSelectionRotationHandler.cs
Normal file
@ -0,0 +1,104 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
public partial class SkinSelectionRotationHandler : SelectionRotationHandler
|
||||
{
|
||||
public Action<Drawable, Vector2> UpdatePosition { get; init; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IEditorChangeHandler? changeHandler { get; set; }
|
||||
|
||||
private BindableList<ISerialisableDrawable> selectedItems { get; } = new BindableList<ISerialisableDrawable>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(SkinEditor skinEditor)
|
||||
{
|
||||
selectedItems.BindTo(skinEditor.SelectedComponents);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
selectedItems.CollectionChanged += (_, __) => updateState();
|
||||
updateState();
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
CanRotate.Value = selectedItems.Count > 0;
|
||||
}
|
||||
|
||||
private Drawable[]? objectsInRotation;
|
||||
|
||||
private Vector2? defaultOrigin;
|
||||
private Dictionary<Drawable, float>? originalRotations;
|
||||
private Dictionary<Drawable, Vector2>? originalPositions;
|
||||
|
||||
public override void Begin()
|
||||
{
|
||||
if (objectsInRotation != null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!");
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
objectsInRotation = selectedItems.Cast<Drawable>().ToArray();
|
||||
originalRotations = objectsInRotation.ToDictionary(d => d, d => d.Rotation);
|
||||
originalPositions = objectsInRotation.ToDictionary(d => d, d => d.ToScreenSpace(d.OriginPosition));
|
||||
defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray())).Centre;
|
||||
}
|
||||
|
||||
public override void Update(float rotation, Vector2? origin = null)
|
||||
{
|
||||
if (objectsInRotation == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!");
|
||||
|
||||
Debug.Assert(originalRotations != null && originalPositions != null && defaultOrigin != null);
|
||||
|
||||
if (objectsInRotation.Length == 1 && origin == null)
|
||||
{
|
||||
// for single items, rotate around the origin rather than the selection centre by default.
|
||||
objectsInRotation[0].Rotation = originalRotations.Single().Value + rotation;
|
||||
return;
|
||||
}
|
||||
|
||||
var actualOrigin = origin ?? defaultOrigin.Value;
|
||||
|
||||
foreach (var drawableItem in objectsInRotation)
|
||||
{
|
||||
var rotatedPosition = GeometryUtils.RotatePointAroundOrigin(originalPositions[drawableItem], actualOrigin, rotation);
|
||||
UpdatePosition(drawableItem, rotatedPosition);
|
||||
|
||||
drawableItem.Rotation = originalRotations[drawableItem] + rotation;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Commit()
|
||||
{
|
||||
if (objectsInRotation == null)
|
||||
throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
|
||||
|
||||
changeHandler?.EndChange();
|
||||
|
||||
objectsInRotation = null;
|
||||
originalPositions = null;
|
||||
originalRotations = null;
|
||||
defaultOrigin = null;
|
||||
}
|
||||
}
|
||||
}
|
@ -23,6 +23,7 @@ using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osu.Game.Rulesets.Objects.Pooling;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
|
||||
namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
@ -94,6 +95,16 @@ namespace osu.Game.Rulesets.UI
|
||||
/// </summary>
|
||||
public readonly BindableBool DisplayJudgements = new BindableBool(true);
|
||||
|
||||
/// <summary>
|
||||
/// A screen space draw quad which resembles the edges of the playfield for skinning purposes.
|
||||
/// This will allow users / components to snap objects to the "edge" of the playfield.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rulesets which reduce the visible area further than the full relative playfield space itself
|
||||
/// should retarget this to the ScreenSpaceDrawQuad of the appropriate container.
|
||||
/// </remarks>
|
||||
public virtual Quad SkinnableComponentScreenSpaceDrawQuad => ScreenSpaceDrawQuad;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
[CanBeNull]
|
||||
protected IReadOnlyList<Mod> Mods { get; private set; }
|
||||
|
@ -36,6 +36,9 @@ namespace osu.Game.Screens.Backgrounds
|
||||
/// </remarks>
|
||||
public readonly Bindable<bool> IgnoreUserSettings = new Bindable<bool>(true);
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the storyboard loaded should completely hide the background behind it.
|
||||
/// </summary>
|
||||
public readonly Bindable<bool> StoryboardReplacesBackground = new Bindable<bool>();
|
||||
|
||||
/// <summary>
|
||||
@ -60,12 +63,11 @@ namespace osu.Game.Screens.Backgrounds
|
||||
|
||||
InternalChild = dimmable = CreateFadeContainer();
|
||||
|
||||
dimmable.StoryboardReplacesBackground.BindTo(StoryboardReplacesBackground);
|
||||
dimmable.IgnoreUserSettings.BindTo(IgnoreUserSettings);
|
||||
dimmable.IsBreakTime.BindTo(IsBreakTime);
|
||||
dimmable.BlurAmount.BindTo(BlurAmount);
|
||||
dimmable.DimWhenUserSettingsIgnored.BindTo(DimWhenUserSettingsIgnored);
|
||||
|
||||
StoryboardReplacesBackground.BindTo(dimmable.StoryboardReplacesBackground);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -144,6 +146,8 @@ namespace osu.Game.Screens.Backgrounds
|
||||
/// </remarks>
|
||||
public readonly Bindable<float> BlurAmount = new BindableFloat();
|
||||
|
||||
public readonly Bindable<bool> StoryboardReplacesBackground = new Bindable<bool>();
|
||||
|
||||
public Background Background
|
||||
{
|
||||
get => background;
|
||||
@ -187,11 +191,19 @@ namespace osu.Game.Screens.Backgrounds
|
||||
|
||||
userBlurLevel.ValueChanged += _ => UpdateVisuals();
|
||||
BlurAmount.ValueChanged += _ => UpdateVisuals();
|
||||
StoryboardReplacesBackground.ValueChanged += _ => UpdateVisuals();
|
||||
}
|
||||
|
||||
protected override bool ShowDimContent
|
||||
// The background needs to be hidden in the case of it being replaced by the storyboard
|
||||
=> (!ShowStoryboard.Value && !IgnoreUserSettings.Value) || !StoryboardReplacesBackground.Value;
|
||||
protected override float DimLevel
|
||||
{
|
||||
get
|
||||
{
|
||||
if ((IgnoreUserSettings.Value || ShowStoryboard.Value) && StoryboardReplacesBackground.Value)
|
||||
return 1;
|
||||
|
||||
return base.DimLevel;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateVisuals()
|
||||
{
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -22,7 +23,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
private const float button_padding = 5;
|
||||
|
||||
public Func<float, bool>? OnRotation;
|
||||
[Resolved]
|
||||
private SelectionRotationHandler? rotationHandler { get; set; }
|
||||
|
||||
public Func<Vector2, Anchor, bool>? OnScale;
|
||||
public Func<Direction, bool, bool>? OnFlip;
|
||||
public Func<bool>? OnReverse;
|
||||
@ -51,22 +54,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
}
|
||||
}
|
||||
|
||||
private bool canRotate;
|
||||
|
||||
/// <summary>
|
||||
/// Whether rotation support should be enabled.
|
||||
/// </summary>
|
||||
public bool CanRotate
|
||||
{
|
||||
get => canRotate;
|
||||
set
|
||||
{
|
||||
if (canRotate == value) return;
|
||||
|
||||
canRotate = value;
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
private readonly IBindable<bool> canRotate = new BindableBool();
|
||||
|
||||
private bool canScaleX;
|
||||
|
||||
@ -161,7 +149,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load() => recreate();
|
||||
private void load()
|
||||
{
|
||||
if (rotationHandler != null)
|
||||
canRotate.BindTo(rotationHandler.CanRotate);
|
||||
|
||||
canRotate.BindValueChanged(_ => recreate(), true);
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
@ -174,10 +168,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
return CanReverse && reverseButton?.TriggerClick() == true;
|
||||
|
||||
case Key.Comma:
|
||||
return CanRotate && rotateCounterClockwiseButton?.TriggerClick() == true;
|
||||
return canRotate.Value && rotateCounterClockwiseButton?.TriggerClick() == true;
|
||||
|
||||
case Key.Period:
|
||||
return CanRotate && rotateClockwiseButton?.TriggerClick() == true;
|
||||
return canRotate.Value && rotateClockwiseButton?.TriggerClick() == true;
|
||||
}
|
||||
|
||||
return base.OnKeyDown(e);
|
||||
@ -254,14 +248,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
if (CanScaleY) addYScaleComponents();
|
||||
if (CanFlipX) addXFlipComponents();
|
||||
if (CanFlipY) addYFlipComponents();
|
||||
if (CanRotate) addRotationComponents();
|
||||
if (canRotate.Value) addRotationComponents();
|
||||
if (CanReverse) reverseButton = addButton(FontAwesome.Solid.Backward, "Reverse pattern (Ctrl-G)", () => OnReverse?.Invoke());
|
||||
}
|
||||
|
||||
private void addRotationComponents()
|
||||
{
|
||||
rotateCounterClockwiseButton = addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise (Ctrl-<)", () => OnRotation?.Invoke(-90));
|
||||
rotateClockwiseButton = addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise (Ctrl->)", () => OnRotation?.Invoke(90));
|
||||
rotateCounterClockwiseButton = addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise (Ctrl-<)", () => rotationHandler?.Rotate(-90));
|
||||
rotateClockwiseButton = addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise (Ctrl->)", () => rotationHandler?.Rotate(90));
|
||||
|
||||
addRotateHandle(Anchor.TopLeft);
|
||||
addRotateHandle(Anchor.TopRight);
|
||||
@ -331,7 +325,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
var handle = new SelectionBoxRotationHandle
|
||||
{
|
||||
Anchor = anchor,
|
||||
HandleRotate = angle => OnRotation?.Invoke(angle)
|
||||
};
|
||||
|
||||
handle.OperationStarted += operationStarted;
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -15,24 +13,25 @@ using osu.Framework.Localisation;
|
||||
using osu.Game.Localisation;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using Key = osuTK.Input.Key;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
public partial class SelectionBoxRotationHandle : SelectionBoxDragHandle, IHasTooltip
|
||||
{
|
||||
public Action<float> HandleRotate { get; set; }
|
||||
|
||||
public LocalisableString TooltipText { get; private set; }
|
||||
|
||||
private SpriteIcon icon;
|
||||
private SpriteIcon icon = null!;
|
||||
|
||||
private const float snap_step = 15;
|
||||
|
||||
private readonly Bindable<float?> cumulativeRotation = new Bindable<float?>();
|
||||
|
||||
[Resolved]
|
||||
private SelectionBox selectionBox { get; set; }
|
||||
private SelectionBox selectionBox { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private SelectionRotationHandler? rotationHandler { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
@ -63,10 +62,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
bool handle = base.OnDragStart(e);
|
||||
if (handle)
|
||||
cumulativeRotation.Value = 0;
|
||||
return handle;
|
||||
if (rotationHandler == null) return false;
|
||||
|
||||
rotationHandler.Begin();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnDrag(DragEvent e)
|
||||
@ -99,7 +98,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e)
|
||||
{
|
||||
base.OnDragEnd(e);
|
||||
rotationHandler?.Commit();
|
||||
UpdateHoverState();
|
||||
|
||||
cumulativeRotation.Value = null;
|
||||
rawCumulativeRotation = 0;
|
||||
TooltipText = default;
|
||||
@ -116,14 +117,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
private void applyRotation(bool shouldSnap)
|
||||
{
|
||||
float oldRotation = cumulativeRotation.Value ?? 0;
|
||||
|
||||
float newRotation = shouldSnap ? snap(rawCumulativeRotation, snap_step) : MathF.Round(rawCumulativeRotation);
|
||||
newRotation = (newRotation - 180) % 360 + 180;
|
||||
|
||||
cumulativeRotation.Value = newRotation;
|
||||
|
||||
HandleRotate?.Invoke(newRotation - oldRotation);
|
||||
rotationHandler?.Update(newRotation);
|
||||
TooltipText = shouldSnap ? EditorStrings.RotationSnapped(newRotation) : EditorStrings.RotationUnsnapped(newRotation);
|
||||
}
|
||||
|
||||
|
@ -16,7 +16,6 @@ using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
@ -56,6 +55,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
[Resolved(CanBeNull = true)]
|
||||
protected IEditorChangeHandler ChangeHandler { get; private set; }
|
||||
|
||||
protected SelectionRotationHandler RotationHandler { get; private set; }
|
||||
|
||||
protected SelectionHandler()
|
||||
{
|
||||
selectedBlueprints = new List<SelectionBlueprint<T>>();
|
||||
@ -64,10 +65,21 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
AlwaysPresent = true;
|
||||
}
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
dependencies.CacheAs(RotationHandler = CreateRotationHandler());
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChild = SelectionBox = CreateSelectionBox();
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
RotationHandler,
|
||||
SelectionBox = CreateSelectionBox(),
|
||||
});
|
||||
|
||||
SelectedItems.CollectionChanged += (_, _) =>
|
||||
{
|
||||
@ -81,7 +93,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
OperationStarted = OnOperationBegan,
|
||||
OperationEnded = OnOperationEnded,
|
||||
|
||||
OnRotation = HandleRotation,
|
||||
OnScale = HandleScale,
|
||||
OnFlip = HandleFlip,
|
||||
OnReverse = HandleReverse,
|
||||
@ -133,6 +144,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// <returns>Whether any items could be rotated.</returns>
|
||||
public virtual bool HandleRotation(float angle) => false;
|
||||
|
||||
/// <summary>
|
||||
/// Creates the handler to use for rotation operations.
|
||||
/// </summary>
|
||||
public virtual SelectionRotationHandler CreateRotationHandler() => new SelectionRotationHandler();
|
||||
|
||||
/// <summary>
|
||||
/// Handles the selected items being scaled.
|
||||
/// </summary>
|
||||
@ -401,98 +417,5 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
=> Enumerable.Empty<MenuItem>();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
/// <summary>
|
||||
/// Rotate a point around an arbitrary origin.
|
||||
/// </summary>
|
||||
/// <param name="point">The point.</param>
|
||||
/// <param name="origin">The centre origin to rotate around.</param>
|
||||
/// <param name="angle">The angle to rotate (in degrees).</param>
|
||||
protected static Vector2 RotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle)
|
||||
{
|
||||
angle = -angle;
|
||||
|
||||
point.X -= origin.X;
|
||||
point.Y -= origin.Y;
|
||||
|
||||
Vector2 ret;
|
||||
ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle));
|
||||
ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle));
|
||||
|
||||
ret.X += origin.X;
|
||||
ret.Y += origin.Y;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a flip direction, a surrounding quad for all selected objects, and a position,
|
||||
/// will return the flipped position in screen space coordinates.
|
||||
/// </summary>
|
||||
protected static Vector2 GetFlippedPosition(Direction direction, Quad quad, Vector2 position)
|
||||
{
|
||||
var centre = quad.Centre;
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case Direction.Horizontal:
|
||||
position.X = centre.X - (position.X - centre.X);
|
||||
break;
|
||||
|
||||
case Direction.Vertical:
|
||||
position.Y = centre.Y - (position.Y - centre.Y);
|
||||
break;
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a scale vector, a surrounding quad for all selected objects, and a position,
|
||||
/// will return the scaled position in screen space coordinates.
|
||||
/// </summary>
|
||||
protected static Vector2 GetScaledPosition(Anchor reference, Vector2 scale, Quad selectionQuad, Vector2 position)
|
||||
{
|
||||
// adjust the direction of scale depending on which side the user is dragging.
|
||||
float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
|
||||
float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
|
||||
|
||||
// guard against no-ops and NaN.
|
||||
if (scale.X != 0 && selectionQuad.Width > 0)
|
||||
position.X = selectionQuad.TopLeft.X + xOffset + (position.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X);
|
||||
|
||||
if (scale.Y != 0 && selectionQuad.Height > 0)
|
||||
position.Y = selectionQuad.TopLeft.Y + yOffset + (position.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y);
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a quad surrounding the provided points.
|
||||
/// </summary>
|
||||
/// <param name="points">The points to calculate a quad for.</param>
|
||||
protected static Quad GetSurroundingQuad(IEnumerable<Vector2> points)
|
||||
{
|
||||
if (!points.Any())
|
||||
return new Quad();
|
||||
|
||||
Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
|
||||
Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
|
||||
|
||||
// Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
|
||||
foreach (var p in points)
|
||||
{
|
||||
minPosition = Vector2.ComponentMin(minPosition, p);
|
||||
maxPosition = Vector2.ComponentMax(maxPosition, p);
|
||||
}
|
||||
|
||||
Vector2 size = maxPosition - minPosition;
|
||||
|
||||
return new Quad(minPosition.X, minPosition.Y, size.X, size.Y);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,85 @@
|
||||
// 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 osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Base handler for editor rotation operations.
|
||||
/// </summary>
|
||||
public partial class SelectionRotationHandler : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the rotation can currently be performed.
|
||||
/// </summary>
|
||||
public Bindable<bool> CanRotate { get; private set; } = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// Performs a single, instant, atomic rotation operation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is intended to be used in atomic contexts (such as when pressing a single button).
|
||||
/// For continuous operations, see the <see cref="Begin"/>-<see cref="Update"/>-<see cref="Commit"/> flow.
|
||||
/// </remarks>
|
||||
/// <param name="rotation">Rotation to apply in degrees.</param>
|
||||
/// <param name="origin">
|
||||
/// The origin point to rotate around.
|
||||
/// If the default <see langword="null"/> value is supplied, a sane implementation-defined default will be used.
|
||||
/// </param>
|
||||
public void Rotate(float rotation, Vector2? origin = null)
|
||||
{
|
||||
Begin();
|
||||
Update(rotation, origin);
|
||||
Commit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Begins a continuous rotation operation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This flow is intended to be used when a rotation operation is made incrementally (such as when dragging a rotation handle or slider).
|
||||
/// For instantaneous, atomic operations, use the convenience <see cref="Rotate"/> method.
|
||||
/// </remarks>
|
||||
public virtual void Begin()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a continuous rotation operation.
|
||||
/// Must be preceded by a <see cref="Begin"/> call.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This flow is intended to be used when a rotation operation is made incrementally (such as when dragging a rotation handle or slider).
|
||||
/// As such, the values of <paramref name="rotation"/> and <paramref name="origin"/> supplied should be relative to the state of the objects being rotated
|
||||
/// when <see cref="Begin"/> was called, rather than instantaneous deltas.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For instantaneous, atomic operations, use the convenience <see cref="Rotate"/> method.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="rotation">Rotation to apply in degrees.</param>
|
||||
/// <param name="origin">
|
||||
/// The origin point to rotate around.
|
||||
/// If the default <see langword="null"/> value is supplied, a sane implementation-defined default will be used.
|
||||
/// </param>
|
||||
public virtual void Update(float rotation, Vector2? origin = null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends a continuous rotation operation.
|
||||
/// Must be preceded by a <see cref="Begin"/> call.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This flow is intended to be used when a rotation operation is made incrementally (such as when dragging a rotation handle or slider).
|
||||
/// For instantaneous, atomic operations, use the convenience <see cref="Rotate"/> method.
|
||||
/// </remarks>
|
||||
public virtual void Commit()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -66,7 +66,7 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
public override bool DisallowExternalBeatmapRulesetChanges => true;
|
||||
|
||||
public override bool? AllowTrackAdjustments => false;
|
||||
public override bool? ApplyModTrackAdjustments => false;
|
||||
|
||||
protected override bool PlayExitSound => !ExitConfirmed && !switchingDifficulty;
|
||||
|
||||
|
@ -42,6 +42,8 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
public override bool DisallowExternalBeatmapRulesetChanges => true;
|
||||
|
||||
public override bool? AllowGlobalTrackControl => false;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmapManager { get; set; }
|
||||
|
||||
|
@ -67,7 +67,13 @@ namespace osu.Game.Screens
|
||||
/// Whether mod track adjustments should be applied on entering this screen.
|
||||
/// A <see langword="null"/> value means that the parent screen's value of this setting will be used.
|
||||
/// </summary>
|
||||
bool? AllowTrackAdjustments { get; }
|
||||
bool? ApplyModTrackAdjustments { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether control of the global track should be allowed via the music controller / now playing overlay.
|
||||
/// A <see langword="null"/> value means that the parent screen's value of this setting will be used.
|
||||
/// </summary>
|
||||
bool? AllowGlobalTrackControl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the back button has been pressed to close any overlays before exiting this <see cref="IOsuScreen"/>.
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -202,6 +203,9 @@ namespace osu.Game.Screens.Menu
|
||||
dialogOverlay?.Push(new StorageErrorDialog(osuStorage, osuStorage.Error));
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
private Drawable proxiedLogo;
|
||||
|
||||
protected override void LogoArriving(OsuLogo logo, bool resuming)
|
||||
{
|
||||
base.LogoArriving(logo, resuming);
|
||||
@ -211,7 +215,7 @@ namespace osu.Game.Screens.Menu
|
||||
logo.FadeColour(Color4.White, 100, Easing.OutQuint);
|
||||
logo.FadeIn(100, Easing.OutQuint);
|
||||
|
||||
logo.ProxyToContainer(logoTarget);
|
||||
proxiedLogo = logo.ProxyToContainer(logoTarget);
|
||||
|
||||
if (resuming)
|
||||
{
|
||||
@ -250,12 +254,27 @@ namespace osu.Game.Screens.Menu
|
||||
var seq = logo.FadeOut(300, Easing.InSine)
|
||||
.ScaleTo(0.2f, 300, Easing.InSine);
|
||||
|
||||
logo.ReturnProxy();
|
||||
if (proxiedLogo != null)
|
||||
{
|
||||
logo.ReturnProxy();
|
||||
proxiedLogo = null;
|
||||
}
|
||||
|
||||
seq.OnComplete(_ => Buttons.SetOsuLogo(null));
|
||||
seq.OnAbort(_ => Buttons.SetOsuLogo(null));
|
||||
}
|
||||
|
||||
protected override void LogoExiting(OsuLogo logo)
|
||||
{
|
||||
base.LogoExiting(logo);
|
||||
|
||||
if (proxiedLogo != null)
|
||||
{
|
||||
logo.ReturnProxy();
|
||||
proxiedLogo = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnSuspending(ScreenTransitionEvent e)
|
||||
{
|
||||
base.OnSuspending(e);
|
||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
[Cached(typeof(IBindable<PlaylistItem>))]
|
||||
public readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
|
||||
|
||||
public override bool? AllowTrackAdjustments => true;
|
||||
public override bool? ApplyModTrackAdjustments => true;
|
||||
|
||||
protected override BackgroundScreen CreateBackground() => new RoomBackgroundScreen(Room.Playlist.FirstOrDefault())
|
||||
{
|
||||
|
@ -265,7 +265,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () =>
|
||||
{
|
||||
exitConfirmed = true;
|
||||
this.Exit();
|
||||
if (this.IsCurrentScreen())
|
||||
this.Exit();
|
||||
}));
|
||||
}
|
||||
|
||||
@ -370,9 +371,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
|
||||
private void onLoadRequested()
|
||||
{
|
||||
if (BeatmapAvailability.Value.State != DownloadState.LocallyAvailable)
|
||||
return;
|
||||
|
||||
// In the case of spectating, IMultiplayerClient.LoadRequested can be fired while the game is still spectating a previous session.
|
||||
// For now, we want to game to switch to the new game so need to request exiting from the play screen.
|
||||
if (!ParentScreen.IsCurrentScreen())
|
||||
@ -390,6 +388,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
if (client.LocalUser?.State == MultiplayerUserState.Spectating && (SelectedItem.Value == null || Beatmap.IsDefault))
|
||||
return;
|
||||
|
||||
if (BeatmapAvailability.Value.State != DownloadState.LocallyAvailable)
|
||||
return;
|
||||
|
||||
StartPlay();
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Scheduler.AddDelayed(() => StatisticsPanel.ToggleVisibility(), 1000);
|
||||
}
|
||||
|
||||
protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback) => null;
|
||||
|
||||
protected override APIRequest FetchNextPage(int direction, Action<IEnumerable<ScoreInfo>> scoresCallback) => null;
|
||||
|
@ -2,12 +2,13 @@
|
||||
// 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.Audio;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
@ -29,7 +30,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
public override bool DisallowExternalBeatmapRulesetChanges => true;
|
||||
|
||||
// We are managing our own adjustments. For now, this happens inside the Player instances themselves.
|
||||
public override bool? AllowTrackAdjustments => false;
|
||||
public override bool? ApplyModTrackAdjustments => false;
|
||||
|
||||
public override bool HideOverlaysOnEnter => true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether all spectating players have finished loading.
|
||||
@ -196,15 +199,29 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
|
||||
private void performInitialSeek()
|
||||
{
|
||||
// Seek the master clock to the gameplay time.
|
||||
// This is chosen as the first available frame in the players' replays, which matches the seek by each individual SpectatorPlayer.
|
||||
double startTime = instances.Where(i => i.Score != null)
|
||||
.SelectMany(i => i.Score.AsNonNull().Replay.Frames)
|
||||
.Select(f => f.Time)
|
||||
.DefaultIfEmpty(0)
|
||||
.Min();
|
||||
// We want to start showing gameplay as soon as possible.
|
||||
// Each client may be in a different place in the beatmap, so we need to do our best to find a common
|
||||
// starting point.
|
||||
//
|
||||
// Preferring a lower value ensures that we don't have some clients stuttering to keep up.
|
||||
List<double> minFrameTimes = new List<double>();
|
||||
|
||||
foreach (var instance in instances)
|
||||
{
|
||||
if (instance.Score == null)
|
||||
continue;
|
||||
|
||||
minFrameTimes.Add(instance.Score.Replay.Frames.MinBy(f => f.Time)?.Time ?? 0);
|
||||
}
|
||||
|
||||
// Remove any outliers (only need to worry about removing those lower than the mean since we will take a Min() after).
|
||||
double mean = minFrameTimes.Average();
|
||||
minFrameTimes.RemoveAll(t => mean - t > 1000);
|
||||
|
||||
double startTime = minFrameTimes.Min();
|
||||
|
||||
masterClockContainer.Reset(startTime, true);
|
||||
Logger.Log($"Multiplayer spectator seeking to initial time of {startTime}");
|
||||
}
|
||||
|
||||
protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState)
|
||||
@ -212,7 +229,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
}
|
||||
|
||||
protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState)
|
||||
=> instances.Single(i => i.UserId == userId).LoadScore(spectatorGameplayState.Score);
|
||||
{
|
||||
var playerArea = instances.Single(i => i.UserId == userId);
|
||||
|
||||
// The multiplayer spectator flow requires the client to return to a higher level screen
|
||||
// (ie. StartGameplay should only be called once per player).
|
||||
//
|
||||
// Meanwhile, the solo spectator flow supports multiple `StartGameplay` calls.
|
||||
// To ensure we don't crash out in an edge case where this is called more than once in multiplayer,
|
||||
// guard against re-entry for the same player.
|
||||
if (playerArea.Score != null)
|
||||
return;
|
||||
|
||||
playerArea.LoadScore(spectatorGameplayState.Score);
|
||||
}
|
||||
|
||||
protected override void QuitGameplay(int userId)
|
||||
{
|
||||
@ -230,6 +260,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
return base.OnBackButton();
|
||||
|
||||
// On a manual exit, set the player back to idle unless gameplay has finished.
|
||||
// Of note, this doesn't cover exiting using alt-f4 or menu home option.
|
||||
if (multiplayerClient.Room.State != MultiplayerRoomState.Open)
|
||||
multiplayerClient.ChangeState(MultiplayerUserState.Idle);
|
||||
|
||||
|
@ -67,7 +67,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
SpectatorPlayerClock = clock;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Masking = true;
|
||||
|
||||
AudioContainer audioContainer;
|
||||
InternalChildren = new Drawable[]
|
||||
|
@ -7,6 +7,7 @@ using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
@ -15,20 +16,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
/// </summary>
|
||||
public partial class PlayerGrid : CompositeDrawable
|
||||
{
|
||||
public const float ANIMATION_DELAY = 400;
|
||||
|
||||
/// <summary>
|
||||
/// A temporary limitation on the number of players, because only layouts up to 16 players are supported for a single screen.
|
||||
/// Todo: Can be removed in the future with scrolling support + performance improvements.
|
||||
/// </summary>
|
||||
public const int MAX_PLAYERS = 16;
|
||||
|
||||
private const float player_spacing = 5;
|
||||
private const float player_spacing = 6;
|
||||
|
||||
/// <summary>
|
||||
/// The currently-maximised facade.
|
||||
/// </summary>
|
||||
public Drawable MaximisedFacade => maximisedFacade;
|
||||
public Facade MaximisedFacade { get; }
|
||||
|
||||
private readonly Facade maximisedFacade;
|
||||
private readonly Container paddingContainer;
|
||||
private readonly FillFlowContainer<Facade> facadeContainer;
|
||||
private readonly Container<Cell> cellContainer;
|
||||
@ -48,12 +50,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = facadeContainer = new FillFlowContainer<Facade>
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(player_spacing),
|
||||
}
|
||||
},
|
||||
maximisedFacade = new Facade { RelativeSizeAxes = Axes.Both }
|
||||
MaximisedFacade = new Facade
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(0.8f),
|
||||
}
|
||||
}
|
||||
},
|
||||
cellContainer = new Container<Cell> { RelativeSizeAxes = Axes.Both }
|
||||
@ -75,8 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
var facade = new Facade();
|
||||
facadeContainer.Add(facade);
|
||||
|
||||
var cell = new Cell(index, content) { ToggleMaximisationState = toggleMaximisationState };
|
||||
cell.SetFacade(facade);
|
||||
var cell = new Cell(index, content, facade) { ToggleMaximisationState = toggleMaximisationState };
|
||||
|
||||
cellContainer.Add(cell);
|
||||
}
|
||||
@ -91,26 +98,28 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
|
||||
private void toggleMaximisationState(Cell target)
|
||||
{
|
||||
// Iterate through all cells to ensure only one is maximised at any time.
|
||||
foreach (var i in cellContainer.ToList())
|
||||
{
|
||||
if (i == target)
|
||||
i.IsMaximised = !i.IsMaximised;
|
||||
else
|
||||
i.IsMaximised = false;
|
||||
// in the case the target is the already maximised cell (or there is only one cell), no cell should be maximised.
|
||||
bool hasMaximised = !target.IsMaximised && cellContainer.Count > 1;
|
||||
|
||||
if (i.IsMaximised)
|
||||
// Iterate through all cells to ensure only one is maximised at any time.
|
||||
foreach (var cell in cellContainer.ToList())
|
||||
{
|
||||
if (hasMaximised && cell == target)
|
||||
{
|
||||
// Transfer cell to the maximised facade.
|
||||
i.SetFacade(maximisedFacade);
|
||||
cellContainer.ChangeChildDepth(i, maximisedInstanceDepth -= 0.001f);
|
||||
cell.SetFacade(MaximisedFacade, true);
|
||||
cellContainer.ChangeChildDepth(cell, maximisedInstanceDepth -= 0.001f);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Transfer cell back to its original facade.
|
||||
i.SetFacade(facadeContainer[i.FacadeIndex]);
|
||||
cell.SetFacade(facadeContainer[cell.FacadeIndex], false);
|
||||
}
|
||||
|
||||
cell.FadeColour(hasMaximised && cell != target ? Color4.Gray : Color4.White, ANIMATION_DELAY, Easing.OutQuint);
|
||||
}
|
||||
|
||||
facadeContainer.ScaleTo(hasMaximised ? 0.95f : 1, ANIMATION_DELAY, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
@ -169,5 +178,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
foreach (var cell in facadeContainer)
|
||||
cell.Size = cellSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A facade of the grid which is used as a dummy object to store the required position/size of cells.
|
||||
/// </summary>
|
||||
public partial class Facade : Drawable
|
||||
{
|
||||
public Facade()
|
||||
{
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,12 @@
|
||||
// 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 JetBrains.Annotations;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
@ -32,68 +31,79 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
/// <summary>
|
||||
/// An action that toggles the maximisation state of this cell.
|
||||
/// </summary>
|
||||
public Action<Cell> ToggleMaximisationState;
|
||||
public Action<Cell>? ToggleMaximisationState;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this cell is currently maximised.
|
||||
/// </summary>
|
||||
public bool IsMaximised;
|
||||
public bool IsMaximised { get; private set; }
|
||||
|
||||
private Facade facade;
|
||||
private bool isTracking = true;
|
||||
|
||||
public Cell(int facadeIndex, Drawable content)
|
||||
private bool isAnimating;
|
||||
|
||||
public Cell(int facadeIndex, Drawable content, Facade facade)
|
||||
{
|
||||
FacadeIndex = facadeIndex;
|
||||
this.facade = facade;
|
||||
|
||||
Origin = Anchor.Centre;
|
||||
InternalChild = Content = content;
|
||||
|
||||
Masking = true;
|
||||
CornerRadius = 5;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (isTracking)
|
||||
{
|
||||
Position = getFinalPosition();
|
||||
Size = getFinalSize();
|
||||
}
|
||||
var targetPos = getFinalPosition();
|
||||
var targetSize = getFinalSize();
|
||||
|
||||
double duration = isAnimating ? 60 : 0;
|
||||
|
||||
Position = new Vector2(
|
||||
(float)Interpolation.DampContinuously(Position.X, targetPos.X, duration, Time.Elapsed),
|
||||
(float)Interpolation.DampContinuously(Position.Y, targetPos.Y, duration, Time.Elapsed)
|
||||
);
|
||||
|
||||
Size = new Vector2(
|
||||
(float)Interpolation.DampContinuously(Size.X, targetSize.X, duration, Time.Elapsed),
|
||||
(float)Interpolation.DampContinuously(Size.Y, targetSize.Y, duration, Time.Elapsed)
|
||||
);
|
||||
|
||||
// If we don't track the animating state, the animation will also occur when resizing the window.
|
||||
isAnimating &= !Precision.AlmostEquals(Size, targetSize, 0.5f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes this cell track a new facade.
|
||||
/// </summary>
|
||||
public void SetFacade([NotNull] Facade newFacade)
|
||||
public void SetFacade(Facade newFacade, bool isMaximised)
|
||||
{
|
||||
Facade lastFacade = facade;
|
||||
facade = newFacade;
|
||||
IsMaximised = isMaximised;
|
||||
isAnimating = true;
|
||||
|
||||
if (lastFacade == null || lastFacade == newFacade)
|
||||
return;
|
||||
|
||||
isTracking = false;
|
||||
|
||||
this.MoveTo(getFinalPosition(), 400, Easing.OutQuint).ResizeTo(getFinalSize(), 400, Easing.OutQuint)
|
||||
.Then()
|
||||
.OnComplete(_ =>
|
||||
{
|
||||
if (facade == newFacade)
|
||||
isTracking = true;
|
||||
});
|
||||
TweenEdgeEffectTo(new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Radius = isMaximised ? 30 : 10,
|
||||
Colour = Colour4.Black.Opacity(isMaximised ? 0.5f : 0.2f),
|
||||
}, ANIMATION_DELAY, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private Vector2 getFinalPosition()
|
||||
{
|
||||
var topLeft = Parent.ToLocalSpace(facade.ToScreenSpace(Vector2.Zero));
|
||||
return topLeft + facade.DrawSize / 2;
|
||||
}
|
||||
private Vector2 getFinalPosition() =>
|
||||
Parent.ToLocalSpace(facade.ScreenSpaceDrawQuad.Centre);
|
||||
|
||||
private Vector2 getFinalSize() => facade.DrawSize;
|
||||
private Vector2 getFinalSize() =>
|
||||
Parent.ToLocalSpace(facade.ScreenSpaceDrawQuad.BottomRight)
|
||||
- Parent.ToLocalSpace(facade.ScreenSpaceDrawQuad.TopLeft);
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
ToggleMaximisationState(this);
|
||||
ToggleMaximisationState?.Invoke(this);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +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.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
{
|
||||
public partial class PlayerGrid
|
||||
{
|
||||
/// <summary>
|
||||
/// A facade of the grid which is used as a dummy object to store the required position/size of cells.
|
||||
/// </summary>
|
||||
private partial class Facade : Drawable
|
||||
{
|
||||
public Facade()
|
||||
{
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -182,7 +182,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
return;
|
||||
|
||||
masterState = newState;
|
||||
Logger.Log($"{nameof(SpectatorSyncManager)}'s master clock become {masterState}");
|
||||
Logger.Log($"{nameof(SpectatorSyncManager)}'s master clock became {masterState}");
|
||||
|
||||
switch (masterState)
|
||||
{
|
||||
|
@ -85,7 +85,9 @@ namespace osu.Game.Screens
|
||||
[Resolved]
|
||||
private MusicController musicController { get; set; }
|
||||
|
||||
public virtual bool? AllowTrackAdjustments => null;
|
||||
public virtual bool? ApplyModTrackAdjustments => null;
|
||||
|
||||
public virtual bool? AllowGlobalTrackControl => null;
|
||||
|
||||
public Bindable<WorkingBeatmap> Beatmap { get; private set; }
|
||||
|
||||
@ -95,7 +97,9 @@ namespace osu.Game.Screens
|
||||
|
||||
private OsuScreenDependencies screenDependencies;
|
||||
|
||||
private bool? trackAdjustmentStateAtSuspend;
|
||||
private bool? globalMusicControlStateAtSuspend;
|
||||
|
||||
private bool? modTrackAdjustmentStateAtSuspend;
|
||||
|
||||
internal void CreateLeasedDependencies(IReadOnlyDependencyContainer dependencies) => createDependencies(dependencies);
|
||||
|
||||
@ -178,8 +182,10 @@ namespace osu.Game.Screens
|
||||
|
||||
// it's feasible to resume to a screen if the target screen never loaded successfully.
|
||||
// in such a case there's no need to restore this value.
|
||||
if (trackAdjustmentStateAtSuspend != null)
|
||||
musicController.AllowTrackAdjustments = trackAdjustmentStateAtSuspend.Value;
|
||||
if (modTrackAdjustmentStateAtSuspend != null)
|
||||
musicController.ApplyModTrackAdjustments = modTrackAdjustmentStateAtSuspend.Value;
|
||||
if (globalMusicControlStateAtSuspend != null)
|
||||
musicController.AllowTrackControl.Value = globalMusicControlStateAtSuspend.Value;
|
||||
|
||||
base.OnResuming(e);
|
||||
}
|
||||
@ -188,7 +194,8 @@ namespace osu.Game.Screens
|
||||
{
|
||||
base.OnSuspending(e);
|
||||
|
||||
trackAdjustmentStateAtSuspend = musicController.AllowTrackAdjustments;
|
||||
modTrackAdjustmentStateAtSuspend = musicController.ApplyModTrackAdjustments;
|
||||
globalMusicControlStateAtSuspend = musicController.AllowTrackControl.Value;
|
||||
|
||||
onSuspendingLogo();
|
||||
}
|
||||
@ -197,8 +204,11 @@ namespace osu.Game.Screens
|
||||
{
|
||||
applyArrivingDefaults(false);
|
||||
|
||||
if (AllowTrackAdjustments != null)
|
||||
musicController.AllowTrackAdjustments = AllowTrackAdjustments.Value;
|
||||
if (ApplyModTrackAdjustments != null)
|
||||
musicController.ApplyModTrackAdjustments = ApplyModTrackAdjustments.Value;
|
||||
|
||||
if (AllowGlobalTrackControl != null)
|
||||
musicController.AllowTrackControl.Value = AllowGlobalTrackControl.Value;
|
||||
|
||||
if (backgroundStack?.Push(ownedBackground = CreateBackground()) != true)
|
||||
{
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Configuration;
|
||||
@ -69,7 +70,9 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public Bindable<bool> ShowHealthBar = new Bindable<bool>(true);
|
||||
|
||||
[CanBeNull]
|
||||
private readonly DrawableRuleset drawableRuleset;
|
||||
|
||||
private readonly IReadOnlyList<Mod> mods;
|
||||
|
||||
/// <summary>
|
||||
@ -103,10 +106,11 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private readonly List<Drawable> hideTargets;
|
||||
|
||||
public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods, bool alwaysShowLeaderboard = true)
|
||||
private readonly Drawable playfieldComponents;
|
||||
|
||||
public HUDOverlay([CanBeNull] DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods, bool alwaysShowLeaderboard = true)
|
||||
{
|
||||
Drawable rulesetComponents;
|
||||
|
||||
this.drawableRuleset = drawableRuleset;
|
||||
this.mods = mods;
|
||||
|
||||
@ -123,6 +127,9 @@ namespace osu.Game.Screens.Play
|
||||
rulesetComponents = drawableRuleset != null
|
||||
? new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, }
|
||||
: Empty(),
|
||||
playfieldComponents = drawableRuleset != null
|
||||
? new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.Playfield, drawableRuleset.Ruleset.RulesetInfo)) { AlwaysPresent = true, }
|
||||
: Empty(),
|
||||
topRightElements = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
@ -162,7 +169,7 @@ namespace osu.Game.Screens.Play
|
||||
},
|
||||
};
|
||||
|
||||
hideTargets = new List<Drawable> { mainComponents, rulesetComponents, topRightElements };
|
||||
hideTargets = new List<Drawable> { mainComponents, rulesetComponents, playfieldComponents, topRightElements };
|
||||
|
||||
if (!alwaysShowLeaderboard)
|
||||
hideTargets.Add(LeaderboardFlow);
|
||||
@ -230,6 +237,16 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (drawableRuleset != null)
|
||||
{
|
||||
Quad playfieldScreenSpaceDrawQuad = drawableRuleset.Playfield.SkinnableComponentScreenSpaceDrawQuad;
|
||||
|
||||
playfieldComponents.Position = ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft);
|
||||
playfieldComponents.Width = (ToLocalSpace(playfieldScreenSpaceDrawQuad.TopRight) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft)).Length;
|
||||
playfieldComponents.Height = (ToLocalSpace(playfieldScreenSpaceDrawQuad.BottomLeft) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft)).Length;
|
||||
playfieldComponents.Rotation = drawableRuleset.Playfield.Rotation;
|
||||
}
|
||||
|
||||
float? lowestTopScreenSpaceLeft = null;
|
||||
float? lowestTopScreenSpaceRight = null;
|
||||
|
||||
|
@ -70,7 +70,7 @@ namespace osu.Game.Screens.Play
|
||||
protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered;
|
||||
|
||||
// We are managing our own adjustments (see OnEntering/OnExiting).
|
||||
public override bool? AllowTrackAdjustments => false;
|
||||
public override bool? ApplyModTrackAdjustments => false;
|
||||
|
||||
private readonly IBindable<bool> gameActive = new Bindable<bool>(true);
|
||||
|
||||
@ -1049,8 +1049,6 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime);
|
||||
|
||||
DimmableStoryboard.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground);
|
||||
|
||||
storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable;
|
||||
|
||||
foreach (var mod in GameplayState.Mods.OfType<IApplicableToPlayer>())
|
||||
|
@ -46,6 +46,8 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public override bool DisallowExternalBeatmapRulesetChanges => true;
|
||||
|
||||
public override bool? AllowGlobalTrackControl => false;
|
||||
|
||||
// Here because IsHovered will not update unless we do so.
|
||||
public override bool HandlePositionalInput => true;
|
||||
|
||||
|
@ -83,6 +83,10 @@ namespace osu.Game.Screens.Ranking
|
||||
|
||||
Score.BindValueChanged(score =>
|
||||
{
|
||||
// An export may be pending from the last score.
|
||||
// Reset this to meet user expectations (a new score which has just been switched to shouldn't export)
|
||||
State.ValueChanged -= exportWhenReady;
|
||||
|
||||
downloadTracker?.RemoveAndDisposeImmediately();
|
||||
|
||||
if (score.NewValue != null)
|
||||
@ -117,11 +121,17 @@ namespace osu.Game.Screens.Ranking
|
||||
return true;
|
||||
|
||||
case GlobalAction.ExportReplay:
|
||||
State.BindValueChanged(exportWhenReady, true);
|
||||
|
||||
// start the import via button
|
||||
if (State.Value != DownloadState.LocallyAvailable)
|
||||
if (State.Value == DownloadState.LocallyAvailable)
|
||||
{
|
||||
State.BindValueChanged(exportWhenReady, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// A download needs to be performed before we can export this replay.
|
||||
button.TriggerClick();
|
||||
if (button.Enabled.Value)
|
||||
State.BindValueChanged(exportWhenReady, true);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -36,6 +36,8 @@ namespace osu.Game.Screens.Ranking
|
||||
|
||||
public override bool DisallowExternalBeatmapRulesetChanges => true;
|
||||
|
||||
public override bool? AllowGlobalTrackControl => true;
|
||||
|
||||
// Temporary for now to stop dual transitions. Should respect the current toolbar mode, but there's no way to do so currently.
|
||||
public override bool HideOverlaysOnEnter => true;
|
||||
|
||||
@ -53,7 +55,8 @@ namespace osu.Game.Screens.Ranking
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
private StatisticsPanel statisticsPanel;
|
||||
protected StatisticsPanel StatisticsPanel { get; private set; }
|
||||
|
||||
private Drawable bottomPanel;
|
||||
private Container<ScorePanel> detachedPanelContainer;
|
||||
|
||||
@ -96,7 +99,7 @@ namespace osu.Game.Screens.Ranking
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
statisticsPanel = CreateStatisticsPanel().With(panel =>
|
||||
StatisticsPanel = CreateStatisticsPanel().With(panel =>
|
||||
{
|
||||
panel.RelativeSizeAxes = Axes.Both;
|
||||
panel.Score.BindTarget = SelectedScore;
|
||||
@ -105,7 +108,7 @@ namespace osu.Game.Screens.Ranking
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
SelectedScore = { BindTarget = SelectedScore },
|
||||
PostExpandAction = () => statisticsPanel.ToggleVisibility()
|
||||
PostExpandAction = () => StatisticsPanel.ToggleVisibility()
|
||||
},
|
||||
detachedPanelContainer = new Container<ScorePanel>
|
||||
{
|
||||
@ -192,7 +195,7 @@ namespace osu.Game.Screens.Ranking
|
||||
if (req != null)
|
||||
api.Queue(req);
|
||||
|
||||
statisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true);
|
||||
StatisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
@ -232,7 +235,7 @@ namespace osu.Game.Screens.Ranking
|
||||
protected virtual APIRequest FetchNextPage(int direction, Action<IEnumerable<ScoreInfo>> scoresCallback) => null;
|
||||
|
||||
/// <summary>
|
||||
/// Creates the <see cref="StatisticsPanel"/> to be used to display extended information about scores.
|
||||
/// Creates the <see cref="Statistics.StatisticsPanel"/> to be used to display extended information about scores.
|
||||
/// </summary>
|
||||
protected virtual StatisticsPanel CreateStatisticsPanel() => new StatisticsPanel();
|
||||
|
||||
@ -270,9 +273,9 @@ namespace osu.Game.Screens.Ranking
|
||||
|
||||
public override bool OnBackButton()
|
||||
{
|
||||
if (statisticsPanel.State.Value == Visibility.Visible)
|
||||
if (StatisticsPanel.State.Value == Visibility.Visible)
|
||||
{
|
||||
statisticsPanel.Hide();
|
||||
StatisticsPanel.Hide();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -351,7 +354,7 @@ namespace osu.Game.Screens.Ranking
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.Select:
|
||||
statisticsPanel.ToggleVisibility();
|
||||
StatisticsPanel.ToggleVisibility();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -146,12 +146,24 @@ namespace osu.Game.Screens.Select
|
||||
public override void OnResuming(ScreenTransitionEvent e)
|
||||
{
|
||||
base.OnResuming(e);
|
||||
revertMods();
|
||||
}
|
||||
|
||||
if (playerLoader != null)
|
||||
{
|
||||
Mods.Value = modsAtGameplayStart;
|
||||
playerLoader = null;
|
||||
}
|
||||
public override bool OnExiting(ScreenExitEvent e)
|
||||
{
|
||||
if (base.OnExiting(e))
|
||||
return true;
|
||||
|
||||
revertMods();
|
||||
return false;
|
||||
}
|
||||
|
||||
private void revertMods()
|
||||
{
|
||||
if (playerLoader == null) return;
|
||||
|
||||
Mods.Value = modsAtGameplayStart;
|
||||
playerLoader = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
protected virtual bool ShowFooter => true;
|
||||
|
||||
public override bool? AllowTrackAdjustments => true;
|
||||
public override bool? ApplyModTrackAdjustments => true;
|
||||
|
||||
/// <summary>
|
||||
/// Can be null if <see cref="ShowFooter"/> is false.
|
||||
|
@ -68,7 +68,10 @@ namespace osu.Game.Skinning
|
||||
MainHUDComponents,
|
||||
|
||||
[Description("Song select")]
|
||||
SongSelect
|
||||
SongSelect,
|
||||
|
||||
[Description("Playfield")]
|
||||
Playfield
|
||||
}
|
||||
}
|
||||
}
|
||||
|
126
osu.Game/Utils/GeometryUtils.cs
Normal file
126
osu.Game/Utils/GeometryUtils.cs
Normal file
@ -0,0 +1,126 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Utils
|
||||
{
|
||||
public static class GeometryUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Rotate a point around an arbitrary origin.
|
||||
/// </summary>
|
||||
/// <param name="point">The point.</param>
|
||||
/// <param name="origin">The centre origin to rotate around.</param>
|
||||
/// <param name="angle">The angle to rotate (in degrees).</param>
|
||||
public static Vector2 RotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle)
|
||||
{
|
||||
angle = -angle;
|
||||
|
||||
point.X -= origin.X;
|
||||
point.Y -= origin.Y;
|
||||
|
||||
Vector2 ret;
|
||||
ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle));
|
||||
ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle));
|
||||
|
||||
ret.X += origin.X;
|
||||
ret.Y += origin.Y;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a flip direction, a surrounding quad for all selected objects, and a position,
|
||||
/// will return the flipped position in screen space coordinates.
|
||||
/// </summary>
|
||||
public static Vector2 GetFlippedPosition(Direction direction, Quad quad, Vector2 position)
|
||||
{
|
||||
var centre = quad.Centre;
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case Direction.Horizontal:
|
||||
position.X = centre.X - (position.X - centre.X);
|
||||
break;
|
||||
|
||||
case Direction.Vertical:
|
||||
position.Y = centre.Y - (position.Y - centre.Y);
|
||||
break;
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a scale vector, a surrounding quad for all selected objects, and a position,
|
||||
/// will return the scaled position in screen space coordinates.
|
||||
/// </summary>
|
||||
public static Vector2 GetScaledPosition(Anchor reference, Vector2 scale, Quad selectionQuad, Vector2 position)
|
||||
{
|
||||
// adjust the direction of scale depending on which side the user is dragging.
|
||||
float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
|
||||
float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
|
||||
|
||||
// guard against no-ops and NaN.
|
||||
if (scale.X != 0 && selectionQuad.Width > 0)
|
||||
position.X = selectionQuad.TopLeft.X + xOffset + (position.X - selectionQuad.TopLeft.X) / selectionQuad.Width * (selectionQuad.Width + scale.X);
|
||||
|
||||
if (scale.Y != 0 && selectionQuad.Height > 0)
|
||||
position.Y = selectionQuad.TopLeft.Y + yOffset + (position.Y - selectionQuad.TopLeft.Y) / selectionQuad.Height * (selectionQuad.Height + scale.Y);
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a quad surrounding the provided points.
|
||||
/// </summary>
|
||||
/// <param name="points">The points to calculate a quad for.</param>
|
||||
public static Quad GetSurroundingQuad(IEnumerable<Vector2> points)
|
||||
{
|
||||
if (!points.Any())
|
||||
return new Quad();
|
||||
|
||||
Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
|
||||
Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
|
||||
|
||||
// Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
|
||||
foreach (var p in points)
|
||||
{
|
||||
minPosition = Vector2.ComponentMin(minPosition, p);
|
||||
maxPosition = Vector2.ComponentMax(maxPosition, p);
|
||||
}
|
||||
|
||||
Vector2 size = maxPosition - minPosition;
|
||||
|
||||
return new Quad(minPosition.X, minPosition.Y, size.X, size.Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a gamefield-space quad surrounding the provided hit objects.
|
||||
/// </summary>
|
||||
/// <param name="hitObjects">The hit objects to calculate a quad for.</param>
|
||||
public static Quad GetSurroundingQuad(IEnumerable<IHasPosition> hitObjects) =>
|
||||
GetSurroundingQuad(hitObjects.SelectMany(h =>
|
||||
{
|
||||
if (h is IHasPath path)
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
h.Position,
|
||||
// can't use EndPosition for reverse slider cases.
|
||||
h.Position + path.Path.PositionAt(1)
|
||||
};
|
||||
}
|
||||
|
||||
return new[] { h.Position };
|
||||
}));
|
||||
}
|
||||
}
|
@ -36,7 +36,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="11.1.2" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2023.724.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2023.801.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.719.0" />
|
||||
<PackageReference Include="Sentry" Version="3.28.1" />
|
||||
<PackageReference Include="SharpCompress" Version="0.32.2" />
|
||||
|
@ -23,6 +23,6 @@
|
||||
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.724.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.801.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
Loading…
Reference in New Issue
Block a user