1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-16 17:42:54 +08:00

Merge branch 'master' into taikostatacc

This commit is contained in:
Dan Balasescu 2023-08-05 11:38:24 +02:00 committed by GitHub
commit 3a609c9b53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 1313 additions and 504 deletions

View File

@ -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.

View File

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
@ -25,6 +26,22 @@ namespace osu.Game.Rulesets.Mania.UI
private readonly List<Stage> stages = new List<Stage>();
public override Quad SkinnableComponentScreenSpaceDrawQuad
{
get
{
if (Stages.Count == 1)
return Stages.First().ScreenSpaceDrawQuad;
RectangleF area = RectangleF.Empty;
foreach (var stage in Stages)
area = RectangleF.Union(area, stage.ScreenSpaceDrawQuad.AABBFloat);
return area;
}
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => stages.Any(s => s.ReceivePositionalInputAt(screenSpacePos));
public ManiaPlayfield(List<StageDefinition> stageDefinitions)

View 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,
})
{
}
}
}
}

View File

@ -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>

View 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);
}
}

View File

@ -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>

View File

@ -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)
{

View File

@ -125,13 +125,13 @@ namespace osu.Game.Tests.Visual.Background
createFakeStoryboard();
AddStep("Enable Storyboard", () =>
{
player.ReplacesBackground.Value = true;
player.StoryboardReplacesBackground.Value = true;
player.StoryboardEnabled.Value = true;
});
AddUntilStep("Background is invisible, storyboard is visible", () => songSelect.IsBackgroundInvisible() && player.IsStoryboardVisible);
AddUntilStep("Background is black, storyboard is visible", () => songSelect.IsBackgroundVisible() && songSelect.IsBackgroundBlack() && player.IsStoryboardVisible);
AddStep("Disable Storyboard", () =>
{
player.ReplacesBackground.Value = false;
player.StoryboardReplacesBackground.Value = false;
player.StoryboardEnabled.Value = false;
});
AddUntilStep("Background is visible, storyboard is invisible", () => songSelect.IsBackgroundVisible() && !player.IsStoryboardVisible);
@ -173,7 +173,7 @@ namespace osu.Game.Tests.Visual.Background
createFakeStoryboard();
AddStep("Enable Storyboard", () =>
{
player.ReplacesBackground.Value = true;
player.StoryboardReplacesBackground.Value = true;
player.StoryboardEnabled.Value = true;
});
AddStep("Enable user dim", () => player.DimmableStoryboard.IgnoreUserSettings.Value = false);
@ -188,7 +188,7 @@ namespace osu.Game.Tests.Visual.Background
{
performFullSetup();
createFakeStoryboard();
AddStep("Enable replacing background", () => player.ReplacesBackground.Value = true);
AddStep("Enable replacing background", () => player.StoryboardReplacesBackground.Value = true);
AddUntilStep("Storyboard is invisible", () => !player.IsStoryboardVisible);
AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible());
@ -199,11 +199,11 @@ namespace osu.Game.Tests.Visual.Background
player.DimmableStoryboard.IgnoreUserSettings.Value = true;
});
AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
AddUntilStep("Background is invisible", () => songSelect.IsBackgroundInvisible());
AddUntilStep("Background is dimmed", () => songSelect.IsBackgroundVisible() && songSelect.IsBackgroundBlack());
AddStep("Disable background replacement", () => player.ReplacesBackground.Value = false);
AddStep("Disable background replacement", () => player.StoryboardReplacesBackground.Value = false);
AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible());
AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible() && !songSelect.IsBackgroundBlack());
}
/// <summary>
@ -257,7 +257,7 @@ namespace osu.Game.Tests.Visual.Background
private void createFakeStoryboard() => AddStep("Create storyboard", () =>
{
player.StoryboardEnabled.Value = false;
player.ReplacesBackground.Value = false;
player.StoryboardReplacesBackground.Value = false;
player.DimmableStoryboard.Add(new OsuSpriteText
{
Size = new Vector2(500, 50),
@ -323,6 +323,8 @@ namespace osu.Game.Tests.Visual.Background
config.BindWith(OsuSetting.BlurLevel, BlurLevel);
}
public bool IsBackgroundBlack() => background.CurrentColour == OsuColour.Gray(0);
public bool IsBackgroundDimmed() => background.CurrentColour == OsuColour.Gray(1f - background.CurrentDim);
public bool IsBackgroundUndimmed() => background.CurrentColour == Color4.White;
@ -331,8 +333,6 @@ namespace osu.Game.Tests.Visual.Background
public bool IsUserBlurDisabled() => background.CurrentBlur == new Vector2(0);
public bool IsBackgroundInvisible() => background.CurrentAlpha == 0;
public bool IsBackgroundVisible() => background.CurrentAlpha == 1;
public bool IsBackgroundBlur() => Precision.AlmostEquals(background.CurrentBlur, new Vector2(BACKGROUND_BLUR), 0.1f);
@ -367,7 +367,7 @@ namespace osu.Game.Tests.Visual.Background
{
base.OnEntering(e);
ApplyToBackground(b => ReplacesBackground.BindTo(b.StoryboardReplacesBackground));
ApplyToBackground(b => StoryboardReplacesBackground.BindTo(b.StoryboardReplacesBackground));
}
public new DimmableStoryboard DimmableStoryboard => base.DimmableStoryboard;
@ -376,7 +376,7 @@ namespace osu.Game.Tests.Visual.Background
public bool BlockLoad;
public Bindable<bool> StoryboardEnabled;
public readonly Bindable<bool> ReplacesBackground = new Bindable<bool>();
public readonly Bindable<bool> StoryboardReplacesBackground = new Bindable<bool>();
public readonly Bindable<bool> IsPaused = new Bindable<bool>();
public LoadBlockingTestPlayer(bool allowPause = true)

View File

@ -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]

View File

@ -8,6 +8,7 @@ using osu.Game.Overlays;
using osu.Game.Overlays.SkinEditor;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Skinning;
namespace osu.Game.Tests.Visual.Gameplay
{
@ -19,7 +20,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestToggleEditor()
{
AddStep("show available components", () => SetContents(_ => new SkinComponentToolbox
var skinComponentsContainer = new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.SongSelect));
AddStep("show available components", () => SetContents(_ => new SkinComponentToolbox(skinComponentsContainer, null)
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,

View File

@ -65,6 +65,34 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("clear playing users", () => playingUsers.Clear());
}
[TestCase(1)]
[TestCase(4)]
[TestCase(9)]
public void TestGeneral(int count)
{
int[] userIds = getPlayerIds(count);
start(userIds);
loadSpectateScreen();
sendFrames(userIds, 1000);
AddWaitStep("wait a bit", 20);
}
[Test]
public void TestMultipleStartRequests()
{
int[] userIds = getPlayerIds(2);
start(userIds);
loadSpectateScreen();
sendFrames(userIds, 1000);
AddWaitStep("wait a bit", 20);
start(userIds);
}
[Test]
public void TestDelayedStart()
{
@ -88,18 +116,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("two players added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 2);
}
[Test]
public void TestGeneral()
{
int[] userIds = getPlayerIds(4);
start(userIds);
loadSpectateScreen();
sendFrames(userIds, 1000);
AddWaitStep("wait a bit", 20);
}
[Test]
public void TestSpectatorPlayerInteractiveElementsHidden()
{

View File

@ -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;

View File

@ -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()
{

View File

@ -154,7 +154,14 @@ namespace osu.Game.Tests.Visual.Online
Type = ChangelogEntryType.Misc,
Category = "Code quality",
Title = "Clean up another thing"
}
},
new APIChangelogEntry
{
Type = ChangelogEntryType.Add,
Category = "osu!",
Title = "Add entry with news url",
Url = "https://osu.ppy.sh/home/news/2023-07-27-summer-splash"
},
}
});

View File

@ -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

View File

@ -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)

View File

@ -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;

View File

@ -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);

View File

@ -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
},
}
},
}

View File

@ -124,7 +124,7 @@ namespace osu.Game.Beatmaps
{
base.Update();
if (Source != null && Source is not IAdjustableClock && Source.CurrentTime < decoupledClock.CurrentTime)
if (Source != null && Source is not IAdjustableClock && Source.CurrentTime < decoupledClock.CurrentTime - 100)
{
// InterpolatingFramedClock won't interpolate backwards unless its source has an ElapsedFrameTime.
// See https://github.com/ppy/osu-framework/blob/ba1385330cc501f34937e08257e586c84e35d772/osu.Framework/Timing/InterpolatingFramedClock.cs#L91-L93

View File

@ -24,15 +24,13 @@ namespace osu.Game.Graphics.Containers
public const double BACKGROUND_FADE_DURATION = 800;
/// <summary>
/// Whether or not user-configured settings relating to brightness of elements should be ignored
/// Whether or not user-configured settings relating to brightness of elements should be ignored.
/// </summary>
/// <remarks>
/// For best or worst, this also bypasses storyboard disable. Not sure this is correct but leaving it as to not break anything.
/// </remarks>
public readonly Bindable<bool> IgnoreUserSettings = new Bindable<bool>();
/// <summary>
/// Whether or not the storyboard loaded should completely hide the background behind it.
/// </summary>
public readonly Bindable<bool> StoryboardReplacesBackground = new Bindable<bool>();
/// <summary>
/// Whether player is in break time.
/// Must be bound to <see cref="BreakTracker.IsBreakTime"/> to allow for dim adjustments in gameplay.
@ -57,7 +55,7 @@ namespace osu.Game.Graphics.Containers
private float breakLightening => LightenDuringBreaks.Value && IsBreakTime.Value ? BREAK_LIGHTEN_AMOUNT : 0;
protected float DimLevel => Math.Max(!IgnoreUserSettings.Value ? (float)UserDimLevel.Value - breakLightening : DimWhenUserSettingsIgnored.Value, 0);
protected virtual float DimLevel => Math.Max(!IgnoreUserSettings.Value ? (float)UserDimLevel.Value - breakLightening : DimWhenUserSettingsIgnored.Value, 0);
protected override Container<Drawable> Content => dimContent;
@ -83,7 +81,6 @@ namespace osu.Game.Graphics.Containers
LightenDuringBreaks.ValueChanged += _ => UpdateVisuals();
IsBreakTime.ValueChanged += _ => UpdateVisuals();
ShowStoryboard.ValueChanged += _ => UpdateVisuals();
StoryboardReplacesBackground.ValueChanged += _ => UpdateVisuals();
IgnoreUserSettings.ValueChanged += _ => UpdateVisuals();
}

View File

@ -31,7 +31,7 @@ namespace osu.Game.IO.Archives
{
ZipArchiveEntry entry = archive.Entries.SingleOrDefault(e => e.Key == name);
if (entry == null)
throw new FileNotFoundException();
return null;
var owner = MemoryAllocator.Default.Allocate<byte>((int)entry.Size);

View File

@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osuTK;
using osuTK.Graphics;
@ -29,6 +30,9 @@ namespace osu.Game.Overlays.Changelog
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[Resolved]
private ILinkHandler? linkHandler { get; set; }
private FontUsage fontLarge;
private FontUsage fontMedium;
@ -86,11 +90,21 @@ namespace osu.Game.Overlays.Changelog
}
};
title.AddText(entry.Title, t =>
if (string.IsNullOrEmpty(entry.Url))
{
t.Font = fontLarge;
t.Colour = entryColour;
});
title.AddText(entry.Title, t =>
{
t.Font = fontLarge;
t.Colour = entryColour;
});
}
else
{
title.AddLink(entry.Title, () => linkHandler?.HandleLink(entry.Url), entry.Url, t =>
{
t.Font = fontLarge;
});
}
if (!string.IsNullOrEmpty(entry.Repository) && !string.IsNullOrEmpty(entry.GithubUrl))
addRepositoryReference(title, entryColour);

View File

@ -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>

View File

@ -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;

View File

@ -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());

View File

@ -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");
}
}

View File

@ -13,6 +13,7 @@ using osu.Framework.Threading;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Rulesets;
using osu.Game.Screens.Edit.Components;
using osu.Game.Skinning;
using osuTK;
@ -23,14 +24,22 @@ namespace osu.Game.Overlays.SkinEditor
{
public Action<Type>? RequestPlacement;
private readonly SkinComponentsContainer? target;
private readonly SkinComponentsContainer target;
private readonly RulesetInfo? ruleset;
private FillFlowContainer fill = null!;
public SkinComponentToolbox(SkinComponentsContainer? target = null)
: base(target?.Lookup.Ruleset == null ? SkinEditorStrings.Components : LocalisableString.Interpolate($"{SkinEditorStrings.Components} ({target.Lookup.Ruleset.Name})"))
/// <summary>
/// Create a new component toolbox for the specified taget.
/// </summary>
/// <param name="target">The target. This is mainly used as a dependency source to find candidate components.</param>
/// <param name="ruleset">A ruleset to filter components by. If null, only components which are not ruleset-specific will be included.</param>
public SkinComponentToolbox(SkinComponentsContainer target, RulesetInfo? ruleset)
: base(ruleset == null ? SkinEditorStrings.Components : LocalisableString.Interpolate($"{SkinEditorStrings.Components} ({ruleset.Name})"))
{
this.target = target;
this.ruleset = ruleset;
}
[BackgroundDependencyLoader]
@ -51,7 +60,7 @@ namespace osu.Game.Overlays.SkinEditor
{
fill.Clear();
var skinnableTypes = SerialisedDrawableInfo.GetAllAvailableDrawables(target?.Lookup.Ruleset);
var skinnableTypes = SerialisedDrawableInfo.GetAllAvailableDrawables(ruleset);
foreach (var type in skinnableTypes)
attemptAddComponent(type);
}

View File

@ -356,7 +356,7 @@ namespace osu.Game.Overlays.SkinEditor
{
new SettingsDropdown<SkinComponentsContainerLookup?>
{
Items = availableTargets.Select(t => t.Lookup),
Items = availableTargets.Select(t => t.Lookup).Distinct(),
Current = selectedTarget,
}
}
@ -366,14 +366,14 @@ namespace osu.Game.Overlays.SkinEditor
// If the new target has a ruleset, let's show ruleset-specific items at the top, and the rest below.
if (target.NewValue.Ruleset != null)
{
componentsSidebar.Add(new SkinComponentToolbox(skinComponentsContainer)
componentsSidebar.Add(new SkinComponentToolbox(skinComponentsContainer, target.NewValue.Ruleset)
{
RequestPlacement = requestPlacement
});
}
// Remove the ruleset from the lookup to get base components.
componentsSidebar.Add(new SkinComponentToolbox(getTarget(new SkinComponentsContainerLookup(target.NewValue.Target)))
componentsSidebar.Add(new SkinComponentToolbox(skinComponentsContainer, null)
{
RequestPlacement = requestPlacement
});

View File

@ -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)
{

View 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;
}
}
}

View File

@ -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;

View File

@ -23,6 +23,7 @@ using osu.Game.Skinning;
using osuTK;
using osu.Game.Rulesets.Objects.Pooling;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Primitives;
namespace osu.Game.Rulesets.UI
{
@ -94,6 +95,16 @@ namespace osu.Game.Rulesets.UI
/// </summary>
public readonly BindableBool DisplayJudgements = new BindableBool(true);
/// <summary>
/// A screen space draw quad which resembles the edges of the playfield for skinning purposes.
/// This will allow users / components to snap objects to the "edge" of the playfield.
/// </summary>
/// <remarks>
/// Rulesets which reduce the visible area further than the full relative playfield space itself
/// should retarget this to the ScreenSpaceDrawQuad of the appropriate container.
/// </remarks>
public virtual Quad SkinnableComponentScreenSpaceDrawQuad => ScreenSpaceDrawQuad;
[Resolved(CanBeNull = true)]
[CanBeNull]
protected IReadOnlyList<Mod> Mods { get; private set; }

View File

@ -36,6 +36,9 @@ namespace osu.Game.Screens.Backgrounds
/// </remarks>
public readonly Bindable<bool> IgnoreUserSettings = new Bindable<bool>(true);
/// <summary>
/// Whether or not the storyboard loaded should completely hide the background behind it.
/// </summary>
public readonly Bindable<bool> StoryboardReplacesBackground = new Bindable<bool>();
/// <summary>
@ -60,12 +63,11 @@ namespace osu.Game.Screens.Backgrounds
InternalChild = dimmable = CreateFadeContainer();
dimmable.StoryboardReplacesBackground.BindTo(StoryboardReplacesBackground);
dimmable.IgnoreUserSettings.BindTo(IgnoreUserSettings);
dimmable.IsBreakTime.BindTo(IsBreakTime);
dimmable.BlurAmount.BindTo(BlurAmount);
dimmable.DimWhenUserSettingsIgnored.BindTo(DimWhenUserSettingsIgnored);
StoryboardReplacesBackground.BindTo(dimmable.StoryboardReplacesBackground);
}
[BackgroundDependencyLoader]
@ -144,6 +146,8 @@ namespace osu.Game.Screens.Backgrounds
/// </remarks>
public readonly Bindable<float> BlurAmount = new BindableFloat();
public readonly Bindable<bool> StoryboardReplacesBackground = new Bindable<bool>();
public Background Background
{
get => background;
@ -187,11 +191,19 @@ namespace osu.Game.Screens.Backgrounds
userBlurLevel.ValueChanged += _ => UpdateVisuals();
BlurAmount.ValueChanged += _ => UpdateVisuals();
StoryboardReplacesBackground.ValueChanged += _ => UpdateVisuals();
}
protected override bool ShowDimContent
// The background needs to be hidden in the case of it being replaced by the storyboard
=> (!ShowStoryboard.Value && !IgnoreUserSettings.Value) || !StoryboardReplacesBackground.Value;
protected override float DimLevel
{
get
{
if ((IgnoreUserSettings.Value || ShowStoryboard.Value) && StoryboardReplacesBackground.Value)
return 1;
return base.DimLevel;
}
}
protected override void UpdateVisuals()
{

View File

@ -54,7 +54,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (!gridCache.IsValid)
{
ClearInternal();
createContent();
if (DrawWidth > 0 && DrawHeight > 0)
createContent();
gridCache.Validate();
}
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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
}
}

View File

@ -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()
{
}
}
}

View File

@ -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;

View File

@ -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; }

View File

@ -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"/>.

View File

@ -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);

View File

@ -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())
{

View File

@ -265,7 +265,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () =>
{
exitConfirmed = true;
this.Exit();
if (this.IsCurrentScreen())
this.Exit();
}));
}
@ -370,9 +371,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private void onLoadRequested()
{
if (BeatmapAvailability.Value.State != DownloadState.LocallyAvailable)
return;
// In the case of spectating, IMultiplayerClient.LoadRequested can be fired while the game is still spectating a previous session.
// For now, we want to game to switch to the new game so need to request exiting from the play screen.
if (!ParentScreen.IsCurrentScreen())
@ -390,6 +388,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (client.LocalUser?.State == MultiplayerUserState.Spectating && (SelectedItem.Value == null || Beatmap.IsDefault))
return;
if (BeatmapAvailability.Value.State != DownloadState.LocallyAvailable)
return;
StartPlay();
}

View File

@ -18,6 +18,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
}
protected override void LoadComplete()
{
base.LoadComplete();
Scheduler.AddDelayed(() => StatisticsPanel.ToggleVisibility(), 1000);
}
protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback) => null;
protected override APIRequest FetchNextPage(int direction, Action<IEnumerable<ScoreInfo>> scoresCallback) => null;

View File

@ -2,12 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Game.Graphics;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
@ -29,7 +30,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
public override bool DisallowExternalBeatmapRulesetChanges => true;
// We are managing our own adjustments. For now, this happens inside the Player instances themselves.
public override bool? AllowTrackAdjustments => false;
public override bool? ApplyModTrackAdjustments => false;
public override bool HideOverlaysOnEnter => true;
/// <summary>
/// Whether all spectating players have finished loading.
@ -196,15 +199,29 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
private void performInitialSeek()
{
// Seek the master clock to the gameplay time.
// This is chosen as the first available frame in the players' replays, which matches the seek by each individual SpectatorPlayer.
double startTime = instances.Where(i => i.Score != null)
.SelectMany(i => i.Score.AsNonNull().Replay.Frames)
.Select(f => f.Time)
.DefaultIfEmpty(0)
.Min();
// We want to start showing gameplay as soon as possible.
// Each client may be in a different place in the beatmap, so we need to do our best to find a common
// starting point.
//
// Preferring a lower value ensures that we don't have some clients stuttering to keep up.
List<double> minFrameTimes = new List<double>();
foreach (var instance in instances)
{
if (instance.Score == null)
continue;
minFrameTimes.Add(instance.Score.Replay.Frames.MinBy(f => f.Time)?.Time ?? 0);
}
// Remove any outliers (only need to worry about removing those lower than the mean since we will take a Min() after).
double mean = minFrameTimes.Average();
minFrameTimes.RemoveAll(t => mean - t > 1000);
double startTime = minFrameTimes.Min();
masterClockContainer.Reset(startTime, true);
Logger.Log($"Multiplayer spectator seeking to initial time of {startTime}");
}
protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState)
@ -212,7 +229,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
}
protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState)
=> instances.Single(i => i.UserId == userId).LoadScore(spectatorGameplayState.Score);
{
var playerArea = instances.Single(i => i.UserId == userId);
// The multiplayer spectator flow requires the client to return to a higher level screen
// (ie. StartGameplay should only be called once per player).
//
// Meanwhile, the solo spectator flow supports multiple `StartGameplay` calls.
// To ensure we don't crash out in an edge case where this is called more than once in multiplayer,
// guard against re-entry for the same player.
if (playerArea.Score != null)
return;
playerArea.LoadScore(spectatorGameplayState.Score);
}
protected override void QuitGameplay(int userId)
{
@ -230,6 +260,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
return base.OnBackButton();
// On a manual exit, set the player back to idle unless gameplay has finished.
// Of note, this doesn't cover exiting using alt-f4 or menu home option.
if (multiplayerClient.Room.State != MultiplayerRoomState.Open)
multiplayerClient.ChangeState(MultiplayerUserState.Idle);

View File

@ -67,7 +67,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
SpectatorPlayerClock = clock;
RelativeSizeAxes = Axes.Both;
Masking = true;
AudioContainer audioContainer;
InternalChildren = new Drawable[]

View File

@ -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;
}
}
}
}

View File

@ -1,13 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using JetBrains.Annotations;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
@ -32,68 +31,79 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// <summary>
/// An action that toggles the maximisation state of this cell.
/// </summary>
public Action<Cell> ToggleMaximisationState;
public Action<Cell>? ToggleMaximisationState;
/// <summary>
/// Whether this cell is currently maximised.
/// </summary>
public bool IsMaximised;
public bool IsMaximised { get; private set; }
private Facade facade;
private bool isTracking = true;
public Cell(int facadeIndex, Drawable content)
private bool isAnimating;
public Cell(int facadeIndex, Drawable content, Facade facade)
{
FacadeIndex = facadeIndex;
this.facade = facade;
Origin = Anchor.Centre;
InternalChild = Content = content;
Masking = true;
CornerRadius = 5;
}
protected override void Update()
{
base.Update();
if (isTracking)
{
Position = getFinalPosition();
Size = getFinalSize();
}
var targetPos = getFinalPosition();
var targetSize = getFinalSize();
double duration = isAnimating ? 60 : 0;
Position = new Vector2(
(float)Interpolation.DampContinuously(Position.X, targetPos.X, duration, Time.Elapsed),
(float)Interpolation.DampContinuously(Position.Y, targetPos.Y, duration, Time.Elapsed)
);
Size = new Vector2(
(float)Interpolation.DampContinuously(Size.X, targetSize.X, duration, Time.Elapsed),
(float)Interpolation.DampContinuously(Size.Y, targetSize.Y, duration, Time.Elapsed)
);
// If we don't track the animating state, the animation will also occur when resizing the window.
isAnimating &= !Precision.AlmostEquals(Size, targetSize, 0.5f);
}
/// <summary>
/// Makes this cell track a new facade.
/// </summary>
public void SetFacade([NotNull] Facade newFacade)
public void SetFacade(Facade newFacade, bool isMaximised)
{
Facade lastFacade = facade;
facade = newFacade;
IsMaximised = isMaximised;
isAnimating = true;
if (lastFacade == null || lastFacade == newFacade)
return;
isTracking = false;
this.MoveTo(getFinalPosition(), 400, Easing.OutQuint).ResizeTo(getFinalSize(), 400, Easing.OutQuint)
.Then()
.OnComplete(_ =>
{
if (facade == newFacade)
isTracking = true;
});
TweenEdgeEffectTo(new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = isMaximised ? 30 : 10,
Colour = Colour4.Black.Opacity(isMaximised ? 0.5f : 0.2f),
}, ANIMATION_DELAY, Easing.OutQuint);
}
private Vector2 getFinalPosition()
{
var topLeft = Parent.ToLocalSpace(facade.ToScreenSpace(Vector2.Zero));
return topLeft + facade.DrawSize / 2;
}
private Vector2 getFinalPosition() =>
Parent.ToLocalSpace(facade.ScreenSpaceDrawQuad.Centre);
private Vector2 getFinalSize() => facade.DrawSize;
private Vector2 getFinalSize() =>
Parent.ToLocalSpace(facade.ScreenSpaceDrawQuad.BottomRight)
- Parent.ToLocalSpace(facade.ScreenSpaceDrawQuad.TopLeft);
protected override bool OnClick(ClickEvent e)
{
ToggleMaximisationState(this);
ToggleMaximisationState?.Invoke(this);
return true;
}
}

View File

@ -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;
}
}
}
}

View File

@ -182,7 +182,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
return;
masterState = newState;
Logger.Log($"{nameof(SpectatorSyncManager)}'s master clock become {masterState}");
Logger.Log($"{nameof(SpectatorSyncManager)}'s master clock became {masterState}");
switch (masterState)
{

View File

@ -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)
{

View File

@ -12,6 +12,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
@ -69,7 +70,9 @@ namespace osu.Game.Screens.Play
public Bindable<bool> ShowHealthBar = new Bindable<bool>(true);
[CanBeNull]
private readonly DrawableRuleset drawableRuleset;
private readonly IReadOnlyList<Mod> mods;
/// <summary>
@ -103,10 +106,11 @@ namespace osu.Game.Screens.Play
private readonly List<Drawable> hideTargets;
public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods, bool alwaysShowLeaderboard = true)
private readonly Drawable playfieldComponents;
public HUDOverlay([CanBeNull] DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods, bool alwaysShowLeaderboard = true)
{
Drawable rulesetComponents;
this.drawableRuleset = drawableRuleset;
this.mods = mods;
@ -123,6 +127,9 @@ namespace osu.Game.Screens.Play
rulesetComponents = drawableRuleset != null
? new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, }
: Empty(),
playfieldComponents = drawableRuleset != null
? new SkinComponentsContainer(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.Playfield, drawableRuleset.Ruleset.RulesetInfo)) { AlwaysPresent = true, }
: Empty(),
topRightElements = new FillFlowContainer
{
Anchor = Anchor.TopRight,
@ -162,7 +169,7 @@ namespace osu.Game.Screens.Play
},
};
hideTargets = new List<Drawable> { mainComponents, rulesetComponents, topRightElements };
hideTargets = new List<Drawable> { mainComponents, rulesetComponents, playfieldComponents, topRightElements };
if (!alwaysShowLeaderboard)
hideTargets.Add(LeaderboardFlow);
@ -230,6 +237,16 @@ namespace osu.Game.Screens.Play
{
base.Update();
if (drawableRuleset != null)
{
Quad playfieldScreenSpaceDrawQuad = drawableRuleset.Playfield.SkinnableComponentScreenSpaceDrawQuad;
playfieldComponents.Position = ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft);
playfieldComponents.Width = (ToLocalSpace(playfieldScreenSpaceDrawQuad.TopRight) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft)).Length;
playfieldComponents.Height = (ToLocalSpace(playfieldScreenSpaceDrawQuad.BottomLeft) - ToLocalSpace(playfieldScreenSpaceDrawQuad.TopLeft)).Length;
playfieldComponents.Rotation = drawableRuleset.Playfield.Rotation;
}
float? lowestTopScreenSpaceLeft = null;
float? lowestTopScreenSpaceRight = null;

View File

@ -70,7 +70,7 @@ namespace osu.Game.Screens.Play
protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered;
// We are managing our own adjustments (see OnEntering/OnExiting).
public override bool? AllowTrackAdjustments => false;
public override bool? ApplyModTrackAdjustments => false;
private readonly IBindable<bool> gameActive = new Bindable<bool>(true);
@ -1049,8 +1049,6 @@ namespace osu.Game.Screens.Play
DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime);
DimmableStoryboard.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground);
storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable;
foreach (var mod in GameplayState.Mods.OfType<IApplicableToPlayer>())

View File

@ -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;

View File

@ -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;
}

View File

@ -36,6 +36,8 @@ namespace osu.Game.Screens.Ranking
public override bool DisallowExternalBeatmapRulesetChanges => true;
public override bool? AllowGlobalTrackControl => true;
// Temporary for now to stop dual transitions. Should respect the current toolbar mode, but there's no way to do so currently.
public override bool HideOverlaysOnEnter => true;
@ -53,7 +55,8 @@ namespace osu.Game.Screens.Ranking
[Resolved]
private IAPIProvider api { get; set; }
private StatisticsPanel statisticsPanel;
protected StatisticsPanel StatisticsPanel { get; private set; }
private Drawable bottomPanel;
private Container<ScorePanel> detachedPanelContainer;
@ -96,7 +99,7 @@ namespace osu.Game.Screens.Ranking
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
statisticsPanel = CreateStatisticsPanel().With(panel =>
StatisticsPanel = CreateStatisticsPanel().With(panel =>
{
panel.RelativeSizeAxes = Axes.Both;
panel.Score.BindTarget = SelectedScore;
@ -105,7 +108,7 @@ namespace osu.Game.Screens.Ranking
{
RelativeSizeAxes = Axes.Both,
SelectedScore = { BindTarget = SelectedScore },
PostExpandAction = () => statisticsPanel.ToggleVisibility()
PostExpandAction = () => StatisticsPanel.ToggleVisibility()
},
detachedPanelContainer = new Container<ScorePanel>
{
@ -192,7 +195,7 @@ namespace osu.Game.Screens.Ranking
if (req != null)
api.Queue(req);
statisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true);
StatisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true);
}
protected override void Update()
@ -232,7 +235,7 @@ namespace osu.Game.Screens.Ranking
protected virtual APIRequest FetchNextPage(int direction, Action<IEnumerable<ScoreInfo>> scoresCallback) => null;
/// <summary>
/// Creates the <see cref="StatisticsPanel"/> to be used to display extended information about scores.
/// Creates the <see cref="Statistics.StatisticsPanel"/> to be used to display extended information about scores.
/// </summary>
protected virtual StatisticsPanel CreateStatisticsPanel() => new StatisticsPanel();
@ -270,9 +273,9 @@ namespace osu.Game.Screens.Ranking
public override bool OnBackButton()
{
if (statisticsPanel.State.Value == Visibility.Visible)
if (StatisticsPanel.State.Value == Visibility.Visible)
{
statisticsPanel.Hide();
StatisticsPanel.Hide();
return true;
}
@ -351,7 +354,7 @@ namespace osu.Game.Screens.Ranking
switch (e.Action)
{
case GlobalAction.Select:
statisticsPanel.ToggleVisibility();
StatisticsPanel.ToggleVisibility();
return true;
}

View File

@ -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;
}
}
}

View File

@ -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.

View File

@ -68,7 +68,10 @@ namespace osu.Game.Skinning
MainHUDComponents,
[Description("Song select")]
SongSelect
SongSelect,
[Description("Playfield")]
Playfield
}
}
}

View 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 };
}));
}
}

View File

@ -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" />

View File

@ -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>