mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 19:22:54 +08:00
Merge branch 'master' into leaderboard-toggle
This commit is contained in:
commit
054ca5e59c
@ -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.
|
||||
|
147
osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs
Normal file
147
osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerJudgement.cs
Normal file
@ -0,0 +1,147 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public partial class TestSceneSpinnerJudgement : RateAdjustedBeatmapTestScene
|
||||
{
|
||||
private const double time_spinner_start = 2000;
|
||||
private const double time_spinner_end = 4000;
|
||||
|
||||
private List<JudgementResult> judgementResults = new List<JudgementResult>();
|
||||
private ScoreAccessibleReplayPlayer currentPlayer = null!;
|
||||
|
||||
[Test]
|
||||
public void TestHitNothing()
|
||||
{
|
||||
performTest(new List<ReplayFrame>());
|
||||
|
||||
AddAssert("all min judgements", () => judgementResults.All(result => result.Type == result.Judgement.MinResult));
|
||||
}
|
||||
|
||||
[TestCase(1)]
|
||||
[TestCase(2)]
|
||||
[TestCase(5)]
|
||||
public void TestNumberOfSpins(int spins)
|
||||
{
|
||||
performTest(generateReplay(spins));
|
||||
|
||||
for (int i = 0; i < spins; ++i)
|
||||
assertResult<SpinnerTick>(i, HitResult.SmallBonus);
|
||||
|
||||
assertResult<SpinnerTick>(spins, HitResult.IgnoreMiss);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitEverything()
|
||||
{
|
||||
performTest(generateReplay(20));
|
||||
|
||||
AddAssert("all max judgements", () => judgementResults.All(result => result.Type == result.Judgement.MaxResult));
|
||||
}
|
||||
|
||||
private static List<ReplayFrame> generateReplay(int spins)
|
||||
{
|
||||
var replayFrames = new List<ReplayFrame>();
|
||||
|
||||
const int frames_per_spin = 30;
|
||||
|
||||
for (int i = 0; i < spins * frames_per_spin; ++i)
|
||||
{
|
||||
float totalProgress = i / (float)(spins * frames_per_spin);
|
||||
float spinProgress = (i % frames_per_spin) / (float)frames_per_spin;
|
||||
double time = time_spinner_start + (time_spinner_end - time_spinner_start) * totalProgress;
|
||||
float posX = MathF.Cos(2 * MathF.PI * spinProgress);
|
||||
float posY = MathF.Sin(2 * MathF.PI * spinProgress);
|
||||
Vector2 finalPos = OsuPlayfield.BASE_SIZE / 2 + new Vector2(posX, posY) * 50;
|
||||
|
||||
replayFrames.Add(new OsuReplayFrame(time, finalPos, OsuAction.LeftButton));
|
||||
}
|
||||
|
||||
return replayFrames;
|
||||
}
|
||||
|
||||
private void performTest(List<ReplayFrame> frames)
|
||||
{
|
||||
AddStep("load player", () =>
|
||||
{
|
||||
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Spinner
|
||||
{
|
||||
StartTime = time_spinner_start,
|
||||
EndTime = time_spinner_end,
|
||||
Position = OsuPlayfield.BASE_SIZE / 2
|
||||
}
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
Difficulty = new BeatmapDifficulty(),
|
||||
Ruleset = new OsuRuleset().RulesetInfo
|
||||
},
|
||||
});
|
||||
|
||||
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
|
||||
|
||||
p.OnLoadComplete += _ =>
|
||||
{
|
||||
p.ScoreProcessor.NewJudgement += result =>
|
||||
{
|
||||
if (currentPlayer == p) judgementResults.Add(result);
|
||||
};
|
||||
};
|
||||
|
||||
LoadScreen(currentPlayer = p);
|
||||
judgementResults = new List<JudgementResult>();
|
||||
});
|
||||
|
||||
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
||||
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
||||
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
||||
}
|
||||
|
||||
private void assertResult<T>(int index, HitResult expectedResult)
|
||||
{
|
||||
AddAssert($"{typeof(T).ReadableName()} ({index}) judged as {expectedResult}",
|
||||
() => judgementResults.Where(j => j.HitObject is T).OrderBy(j => j.HitObject.StartTime).ElementAt(index).Type,
|
||||
() => Is.EqualTo(expectedResult));
|
||||
}
|
||||
|
||||
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
|
||||
{
|
||||
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
|
||||
|
||||
protected override bool PauseOnFocusLost => false;
|
||||
|
||||
public ScoreAccessibleReplayPlayer(Score score)
|
||||
: base(score, new PlayerConfiguration
|
||||
{
|
||||
AllowPause = false,
|
||||
ShowResults = false,
|
||||
})
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -72,9 +72,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
|
||||
lastAngle = thisAngle;
|
||||
|
||||
IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation / 2 - Rotation) > 5f;
|
||||
IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation - Rotation) > 10f;
|
||||
|
||||
Rotation = (float)Interpolation.Damp(Rotation, currentRotation / 2, 0.99, Math.Abs(Time.Elapsed));
|
||||
Rotation = (float)Interpolation.Damp(Rotation, currentRotation, 0.99, Math.Abs(Time.Elapsed));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
{
|
||||
public partial class TaikoHitObjectComposer : HitObjectComposer<TaikoHitObject>
|
||||
{
|
||||
protected override bool ApplyHorizontalCentering => false;
|
||||
|
||||
public TaikoHitObjectComposer(TaikoRuleset ruleset)
|
||||
: base(ruleset)
|
||||
{
|
||||
|
@ -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]
|
||||
|
@ -65,6 +65,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddStep("clear playing users", () => playingUsers.Clear());
|
||||
}
|
||||
|
||||
[TestCase(1)]
|
||||
[TestCase(4)]
|
||||
public void TestGeneral(int count)
|
||||
{
|
||||
int[] userIds = getPlayerIds(count);
|
||||
|
||||
start(userIds);
|
||||
loadSpectateScreen();
|
||||
|
||||
sendFrames(userIds, 1000);
|
||||
AddWaitStep("wait a bit", 20);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDelayedStart()
|
||||
{
|
||||
@ -88,18 +101,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()
|
||||
{
|
||||
|
@ -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)
|
||||
|
@ -136,11 +136,11 @@ namespace osu.Game.Tournament.Components
|
||||
if (match.NewValue != null)
|
||||
match.NewValue.PicksBans.CollectionChanged += picksBansOnCollectionChanged;
|
||||
|
||||
updateState();
|
||||
Scheduler.AddOnce(updateState);
|
||||
}
|
||||
|
||||
private void picksBansOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
=> updateState();
|
||||
=> Scheduler.AddOnce(updateState);
|
||||
|
||||
private BeatmapChoice? choice;
|
||||
|
||||
|
@ -37,6 +37,8 @@ namespace osu.Game.Tournament.Screens.Editors
|
||||
|
||||
private WarningBox rightClickMessage;
|
||||
|
||||
private RectangularPositionSnapGrid grid;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
[CanBeNull]
|
||||
private IDialogOverlay dialogOverlay { get; set; }
|
||||
@ -53,10 +55,12 @@ namespace osu.Game.Tournament.Screens.Editors
|
||||
|
||||
AddInternal(rightClickMessage = new WarningBox("Right click to place and link matches"));
|
||||
|
||||
ScrollContent.Add(new RectangularPositionSnapGrid(Vector2.Zero)
|
||||
ScrollContent.Add(grid = new RectangularPositionSnapGrid(Vector2.Zero)
|
||||
{
|
||||
Spacing = new Vector2(GRID_SPACING),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
BypassAutoSizeAxes = Axes.Both,
|
||||
Depth = float.MaxValue
|
||||
});
|
||||
|
||||
@ -64,6 +68,22 @@ namespace osu.Game.Tournament.Screens.Editors
|
||||
updateMessage();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// Expand grid with the content to allow going beyond the bounds of the screen.
|
||||
grid.Size = ScrollContent.Size + new Vector2(GRID_SPACING * 2);
|
||||
}
|
||||
|
||||
private Vector2 lastMatchesContainerMouseDownPosition;
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
lastMatchesContainerMouseDownPosition = MatchesContainer.ToLocalSpace(e.ScreenSpaceMouseDownPosition);
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
private void updateMessage()
|
||||
{
|
||||
rightClickMessage.Alpha = LadderInfo.Matches.Count > 0 ? 0 : 1;
|
||||
@ -85,7 +105,8 @@ namespace osu.Game.Tournament.Screens.Editors
|
||||
{
|
||||
new OsuMenuItem("Create new match", MenuItemType.Highlighted, () =>
|
||||
{
|
||||
Vector2 pos = MatchesContainer.ToLocalSpace(GetContainingInputManager().CurrentState.Mouse.Position);
|
||||
Vector2 pos = MatchesContainer.Count == 0 ? Vector2.Zero : lastMatchesContainerMouseDownPosition;
|
||||
|
||||
TournamentMatch newMatch = new TournamentMatch { Position = { Value = new Point((int)pos.X, (int)pos.Y) } };
|
||||
|
||||
LadderInfo.Matches.Add(newMatch);
|
||||
|
@ -57,12 +57,15 @@ namespace osu.Game.Tournament.Screens.Ladder
|
||||
},
|
||||
ScrollContent = new LadderDragContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
paths = new Container<Path> { RelativeSizeAxes = Axes.Both },
|
||||
headings = new Container { RelativeSizeAxes = Axes.Both },
|
||||
MatchesContainer = new Container<DrawableTournamentMatch> { RelativeSizeAxes = Axes.Both },
|
||||
MatchesContainer = new Container<DrawableTournamentMatch>
|
||||
{
|
||||
AutoSizeAxes = Axes.Both
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -44,6 +44,11 @@ namespace osu.Game.Rulesets.Edit
|
||||
public abstract partial class HitObjectComposer<TObject> : HitObjectComposer, IPlacementHandler
|
||||
where TObject : HitObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the playfield should be centered horizontally. Should be disabled for playfields which span the full horizontal width.
|
||||
/// </summary>
|
||||
protected virtual bool ApplyHorizontalCentering => true;
|
||||
|
||||
protected IRulesetConfigManager Config { get; private set; }
|
||||
|
||||
// Provides `Playfield`
|
||||
@ -119,8 +124,6 @@ namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
Name = "Playfield content",
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
// layers below playfield
|
||||
@ -241,8 +244,23 @@ namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// Ensure that the playfield is always centered but also doesn't get cut off by toolboxes.
|
||||
PlayfieldContentContainer.Width = Math.Max(1024, DrawWidth) - TOOLBOX_CONTRACTED_SIZE_RIGHT * 2;
|
||||
if (ApplyHorizontalCentering)
|
||||
{
|
||||
PlayfieldContentContainer.Anchor = Anchor.Centre;
|
||||
PlayfieldContentContainer.Origin = Anchor.Centre;
|
||||
|
||||
// Ensure that the playfield is always centered but also doesn't get cut off by toolboxes.
|
||||
PlayfieldContentContainer.Width = Math.Max(1024, DrawWidth) - TOOLBOX_CONTRACTED_SIZE_RIGHT * 2;
|
||||
PlayfieldContentContainer.X = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
PlayfieldContentContainer.Anchor = Anchor.CentreLeft;
|
||||
PlayfieldContentContainer.Origin = Anchor.CentreLeft;
|
||||
|
||||
PlayfieldContentContainer.Width = Math.Max(1024, DrawWidth) - (TOOLBOX_CONTRACTED_SIZE_LEFT + TOOLBOX_CONTRACTED_SIZE_RIGHT);
|
||||
PlayfieldContentContainer.X = TOOLBOX_CONTRACTED_SIZE_LEFT;
|
||||
}
|
||||
}
|
||||
|
||||
public override Playfield Playfield => drawableRulesetWrapper.Playfield;
|
||||
|
@ -54,7 +54,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
if (!gridCache.IsValid)
|
||||
{
|
||||
ClearInternal();
|
||||
createContent();
|
||||
|
||||
if (DrawWidth > 0 && DrawHeight > 0)
|
||||
createContent();
|
||||
|
||||
gridCache.Validate();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,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.
|
||||
|
@ -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(Position, targetPos, 0.01f);
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
{
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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.
|
||||
|
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