1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-14 14:27:25 +08:00

Merge branch 'master' into footer-v2-back-button

This commit is contained in:
Dean Herbert 2024-05-28 15:50:34 +09:00 committed by GitHub
commit 4408039b3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
115 changed files with 2234 additions and 749 deletions

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.509.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2024.523.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged. <!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -273,7 +273,11 @@ namespace osu.Desktop
private static string clampLength(string str) private static string clampLength(string str)
{ {
// For whatever reason, discord decides that strings shorter than 2 characters cannot possibly be valid input, because... reasons? // Empty strings are fine to discord even though single-character strings are not. Make it make sense.
if (string.IsNullOrEmpty(str))
return str;
// As above, discord decides that *non-empty* strings shorter than 2 characters cannot possibly be valid input, because... reasons?
// And yes, that is two *characters*, or *codepoints*, not *bytes* as further down below (as determined by empirical testing). // And yes, that is two *characters*, or *codepoints*, not *bytes* as further down below (as determined by empirical testing).
// That seems very questionable, and isn't even documented anywhere. So to *make it* accept such valid input, // That seems very questionable, and isn't even documented anywhere. So to *make it* accept such valid input,
// just tack on enough of U+200B ZERO WIDTH SPACEs at the end. // just tack on enough of U+200B ZERO WIDTH SPACEs at the end.

View File

@ -22,7 +22,6 @@ using osu.Game.IPC;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Performance; using osu.Game.Performance;
using osu.Game.Utils; using osu.Game.Utils;
using SDL;
namespace osu.Desktop namespace osu.Desktop
{ {
@ -161,7 +160,7 @@ namespace osu.Desktop
host.Window.Title = Name; host.Window.Title = Name;
} }
protected override BatteryInfo CreateBatteryInfo() => new SDL3BatteryInfo(); protected override BatteryInfo CreateBatteryInfo() => FrameworkEnvironment.UseSDL3 ? new SDL3BatteryInfo() : new SDL2BatteryInfo();
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
@ -169,24 +168,5 @@ namespace osu.Desktop
osuSchemeLinkIPCChannel?.Dispose(); osuSchemeLinkIPCChannel?.Dispose();
archiveImportIPCChannel?.Dispose(); archiveImportIPCChannel?.Dispose();
} }
private unsafe class SDL3BatteryInfo : BatteryInfo
{
public override double? ChargeLevel
{
get
{
int percentage;
SDL3.SDL_GetPowerInfo(null, &percentage);
if (percentage == -1)
return null;
return percentage / 100.0;
}
}
public override bool OnBattery => SDL3.SDL_GetPowerInfo(null, null) == SDL_PowerState.SDL_POWERSTATE_ON_BATTERY;
}
} }
} }

View File

@ -0,0 +1,25 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Utils;
namespace osu.Desktop
{
internal class SDL2BatteryInfo : BatteryInfo
{
public override double? ChargeLevel
{
get
{
SDL2.SDL.SDL_GetPowerInfo(out _, out int percentage);
if (percentage == -1)
return null;
return percentage / 100.0;
}
}
public override bool OnBattery => SDL2.SDL.SDL_GetPowerInfo(out _, out _) == SDL2.SDL.SDL_PowerState.SDL_POWERSTATE_ON_BATTERY;
}
}

View File

@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Utils;
using SDL;
namespace osu.Desktop
{
internal unsafe class SDL3BatteryInfo : BatteryInfo
{
public override double? ChargeLevel
{
get
{
int percentage;
SDL3.SDL_GetPowerInfo(null, &percentage);
if (percentage == -1)
return null;
return percentage / 100.0;
}
}
public override bool OnBattery => SDL3.SDL_GetPowerInfo(null, null) == SDL_PowerState.SDL_POWERSTATE_ON_BATTERY;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

@ -36,11 +36,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
while (rhythmStart < historicalNoteCount - 2 && current.StartTime - current.Previous(rhythmStart).StartTime < history_time_max) while (rhythmStart < historicalNoteCount - 2 && current.StartTime - current.Previous(rhythmStart).StartTime < history_time_max)
rhythmStart++; rhythmStart++;
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(rhythmStart);
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)current.Previous(rhythmStart + 1);
for (int i = rhythmStart; i > 0; i--) for (int i = rhythmStart; i > 0; i--)
{ {
OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current.Previous(i - 1); OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current.Previous(i - 1);
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(i);
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)current.Previous(i + 1);
double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now
@ -66,10 +67,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
} }
else else
{ {
if (current.Previous(i - 1).BaseObject is Slider) // bpm change is into slider, this is easy acc window if (currObj.BaseObject is Slider) // bpm change is into slider, this is easy acc window
effectiveRatio *= 0.125; effectiveRatio *= 0.125;
if (current.Previous(i).BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle if (prevObj.BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
effectiveRatio *= 0.25; effectiveRatio *= 0.25;
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet) if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
@ -100,6 +101,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
startRatio = effectiveRatio; startRatio = effectiveRatio;
islandSize = 1; islandSize = 1;
} }
lastObj = prevObj;
prevObj = currObj;
} }
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though) return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though)

View File

@ -8,9 +8,9 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public readonly PathControlPoint ControlPoint; public readonly PathControlPoint ControlPoint;
private readonly T hitObject; private readonly T hitObject;
private readonly Container marker; private readonly Circle circle;
private readonly Drawable markerRing; private readonly Drawable markerRing;
[Resolved] [Resolved]
@ -60,38 +60,22 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Origin = Anchor.Centre; Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[] InternalChildren = new[]
{ {
marker = new Container circle = new Circle
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both, Size = new Vector2(20),
Children = new[] },
{ markerRing = new CircularProgress
new Circle {
{ Anchor = Anchor.Centre,
Anchor = Anchor.Centre, Origin = Anchor.Centre,
Origin = Anchor.Centre, Size = new Vector2(28),
Size = new Vector2(20), Alpha = 0,
}, InnerRadius = 0.1f,
markerRing = new CircularContainer Progress = 1
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(28),
Masking = true,
BorderThickness = 2,
BorderColour = Color4.White,
Alpha = 0,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
}
}
} }
}; };
} }
@ -115,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
} }
// The connecting path is excluded from positional input // The connecting path is excluded from positional input
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => marker.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => circle.ReceivePositionalInputAt(screenSpacePos);
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
@ -209,8 +193,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (IsHovered || IsSelected.Value) if (IsHovered || IsSelected.Value)
colour = colour.Lighten(1); colour = colour.Lighten(1);
marker.Colour = colour; Colour = colour;
marker.Scale = new Vector2(hitObject.Scale); Scale = new Vector2(hitObject.Scale);
} }
private Color4 getColourFromNodeType() private Color4 getColourFromNodeType()

View File

@ -435,10 +435,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ => var item = new TernaryStateRadioMenuItem(type?.Description ?? "Inherit", MenuItemType.Standard, _ =>
{ {
changeHandler?.BeginChange();
foreach (var p in Pieces.Where(p => p.IsSelected.Value)) foreach (var p in Pieces.Where(p => p.IsSelected.Value))
updatePathType(p, type); updatePathType(p, type);
EnsureValidPathTypes(); EnsureValidPathTypes();
changeHandler?.EndChange();
}); });
if (countOfState == totalCount) if (countOfState == totalCount)

View File

@ -3,6 +3,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.Skinning.Default;
@ -27,14 +28,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public SliderBodyPiece() public SliderBodyPiece()
{ {
InternalChild = body = new ManualSliderBody AutoSizeAxes = Axes.Both;
{
AccentColour = Color4.Transparent
};
// SliderSelectionBlueprint relies on calling ReceivePositionalInputAt on this drawable to determine whether selection should occur. // SliderSelectionBlueprint relies on calling ReceivePositionalInputAt on this drawable to determine whether selection should occur.
// Without AlwaysPresent, a movement in a parent container (ie. the editor composer area resizing) could cause incorrect input handling. // Without AlwaysPresent, a movement in a parent container (ie. the editor composer area resizing) could cause incorrect input handling.
AlwaysPresent = true; AlwaysPresent = true;
InternalChild = body = new ManualSliderBody
{
AccentColour = Color4.Transparent
};
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -61,7 +64,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
body.SetVertices(vertices); body.SetVertices(vertices);
} }
Size = body.Size;
OriginPosition = body.PathOffset; OriginPosition = body.PathOffset;
} }

View File

@ -3,7 +3,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
@ -25,33 +24,17 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
public partial class OsuSelectionHandler : EditorSelectionHandler public partial class OsuSelectionHandler : EditorSelectionHandler
{ {
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider? snapProvider { get; set; }
/// <summary>
/// During a transform, the initial path types of a single selected slider are stored so they
/// can be maintained throughout the operation.
/// </summary>
private List<PathType?>? referencePathTypes;
protected override void OnSelectionChanged() protected override void OnSelectionChanged()
{ {
base.OnSelectionChanged(); base.OnSelectionChanged();
Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad(); Quad quad = selectedMovableObjects.Length > 0 ? GeometryUtils.GetSurroundingQuad(selectedMovableObjects) : new Quad();
SelectionBox.CanFlipX = SelectionBox.CanScaleX = quad.Width > 0; SelectionBox.CanFlipX = quad.Width > 0;
SelectionBox.CanFlipY = SelectionBox.CanScaleY = quad.Height > 0; SelectionBox.CanFlipY = quad.Height > 0;
SelectionBox.CanScaleDiagonally = SelectionBox.CanScaleX && SelectionBox.CanScaleY;
SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider); SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
} }
protected override void OnOperationEnded()
{
base.OnOperationEnded();
referencePathTypes = null;
}
protected override bool OnKeyDown(KeyDownEvent e) protected override bool OnKeyDown(KeyDownEvent e)
{ {
if (e.Key == Key.M && e.ControlPressed && e.ShiftPressed) if (e.Key == Key.M && e.ControlPressed && e.ShiftPressed)
@ -149,96 +132,9 @@ namespace osu.Game.Rulesets.Osu.Edit
return didFlip; return didFlip;
} }
public override bool HandleScale(Vector2 scale, Anchor reference)
{
adjustScaleFromAnchor(ref scale, reference);
var hitObjects = selectedMovableObjects;
// for the time being, allow resizing of slider paths only if the slider is
// the only hit object selected. with a group selection, it's likely the user
// is not looking to change the duration of the slider but expand the whole pattern.
if (hitObjects.Length == 1 && hitObjects.First() is Slider slider)
scaleSlider(slider, scale);
else
scaleHitObjects(hitObjects, reference, scale);
moveSelectionInBounds();
return true;
}
private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference)
{
// cancel out scale in axes we don't care about (based on which drag handle was used).
if ((reference & Anchor.x1) > 0) scale.X = 0;
if ((reference & Anchor.y1) > 0) scale.Y = 0;
// reverse the scale direction if dragging from top or left.
if ((reference & Anchor.x0) > 0) scale.X = -scale.X;
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
}
public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler(); public override SelectionRotationHandler CreateRotationHandler() => new OsuSelectionRotationHandler();
private void scaleSlider(Slider slider, Vector2 scale) public override SelectionScaleHandler CreateScaleHandler() => new OsuSelectionScaleHandler();
{
referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type).ToList();
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;
Vector2 pathRelativeDeltaScale = new Vector2(
sliderQuad.Width == 0 ? 0 : 1 + scale.X / sliderQuad.Width,
sliderQuad.Height == 0 ? 0 : 1 + scale.Y / sliderQuad.Height);
Queue<Vector2> oldControlPoints = new Queue<Vector2>();
foreach (var point in slider.Path.ControlPoints)
{
oldControlPoints.Enqueue(point.Position);
point.Position *= pathRelativeDeltaScale;
}
// Maintain the path types in case they were defaulted to bezier at some point during scaling
for (int i = 0; i < slider.Path.ControlPoints.Count; ++i)
slider.Path.ControlPoints[i].Type = referencePathTypes[i];
// Snap the slider's length to the current beat divisor
// to calculate the final resulting duration / bounding box before the final checks.
slider.SnapTo(snapProvider);
//if sliderhead or sliderend end up outside playfield, revert scaling.
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
if (xInBounds && yInBounds && slider.Path.HasValidLength)
return;
foreach (var point in slider.Path.ControlPoints)
point.Position = oldControlPoints.Dequeue();
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
slider.SnapTo(snapProvider);
}
private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
{
scale = getClampedScale(hitObjects, reference, scale);
Quad selectionQuad = GeometryUtils.GetSurroundingQuad(hitObjects);
foreach (var h in hitObjects)
h.Position = GeometryUtils.GetScaledPosition(reference, scale, selectionQuad, h.Position);
}
private (bool X, bool Y) isQuadInBounds(Quad quad)
{
bool xInBounds = (quad.TopLeft.X >= 0) && (quad.BottomRight.X <= DrawWidth);
bool yInBounds = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y <= DrawHeight);
return (xInBounds, yInBounds);
}
private void moveSelectionInBounds() private void moveSelectionInBounds()
{ {
@ -262,43 +158,6 @@ namespace osu.Game.Rulesets.Osu.Edit
h.Position += delta; h.Position += delta;
} }
/// <summary>
/// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip.
/// </summary>
/// <param name="hitObjects">The hitobjects to be scaled</param>
/// <param name="reference">The anchor from which the scale operation is performed</param>
/// <param name="scale">The scale to be clamped</param>
/// <returns>The clamped scale vector</returns>
private Vector2 getClampedScale(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)
{
float xOffset = ((reference & Anchor.x0) > 0) ? -scale.X : 0;
float yOffset = ((reference & Anchor.y0) > 0) ? -scale.Y : 0;
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);
//max Size -> playfield bounds
if (scaledQuad.TopLeft.X < 0)
scale.X += scaledQuad.TopLeft.X;
if (scaledQuad.TopLeft.Y < 0)
scale.Y += scaledQuad.TopLeft.Y;
if (scaledQuad.BottomRight.X > DrawWidth)
scale.X -= scaledQuad.BottomRight.X - DrawWidth;
if (scaledQuad.BottomRight.Y > DrawHeight)
scale.Y -= scaledQuad.BottomRight.Y - DrawHeight;
//min Size -> almost 0. Less than 0 causes the quad to flip, exactly 0 causes scaling to get stuck at minimum scale.
Vector2 scaledSize = selectionQuad.Size + scale;
Vector2 minSize = new Vector2(Precision.FLOAT_EPSILON);
scale = Vector2.ComponentMax(minSize, scaledSize) - selectionQuad.Size;
return scale;
}
/// <summary> /// <summary>
/// All osu! hitobjects which can be moved/rotated/scaled. /// All osu! hitobjects which can be moved/rotated/scaled.
/// </summary> /// </summary>

View File

@ -0,0 +1,220 @@
// 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.Framework.Graphics.Primitives;
using osu.Framework.Utils;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class OsuSelectionScaleHandler : SelectionScaleHandler
{
[Resolved]
private IEditorChangeHandler? changeHandler { get; set; }
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider? snapProvider { 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);
CanScaleX.Value = quad.Width > 0;
CanScaleY.Value = quad.Height > 0;
CanScaleDiagonally.Value = CanScaleX.Value && CanScaleY.Value;
}
private Dictionary<OsuHitObject, OriginalHitObjectState>? objectsInScale;
private Vector2? defaultOrigin;
public override void Begin()
{
if (objectsInScale != null)
throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!");
changeHandler?.BeginChange();
objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho));
OriginalSurroundingQuad = objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider
? GeometryUtils.GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position))
: GeometryUtils.GetSurroundingQuad(objectsInScale.Keys);
defaultOrigin = OriginalSurroundingQuad.Value.Centre;
}
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
{
if (objectsInScale == null)
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null);
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
// for the time being, allow resizing of slider paths only if the slider is
// the only hit object selected. with a group selection, it's likely the user
// is not looking to change the duration of the slider but expand the whole pattern.
if (objectsInScale.Count == 1 && objectsInScale.First().Key is Slider slider)
{
var originalInfo = objectsInScale[slider];
Debug.Assert(originalInfo.PathControlPointPositions != null && originalInfo.PathControlPointTypes != null);
scaleSlider(slider, scale, originalInfo.PathControlPointPositions, originalInfo.PathControlPointTypes);
}
else
{
scale = getClampedScale(OriginalSurroundingQuad.Value, actualOrigin, scale);
foreach (var (ho, originalState) in objectsInScale)
{
ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position);
}
}
moveSelectionInBounds();
}
public override void Commit()
{
if (objectsInScale == null)
throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
changeHandler?.EndChange();
objectsInScale = null;
OriginalSurroundingQuad = null;
defaultOrigin = null;
}
private IEnumerable<OsuHitObject> selectedMovableObjects => selectedItems.Cast<OsuHitObject>()
.Where(h => h is not Spinner);
private void scaleSlider(Slider slider, Vector2 scale, Vector2[] originalPathPositions, PathType?[] originalPathTypes)
{
scale = Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
// Maintain the path types in case they were defaulted to bezier at some point during scaling
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
{
slider.Path.ControlPoints[i].Position = originalPathPositions[i] * scale;
slider.Path.ControlPoints[i].Type = originalPathTypes[i];
}
// Snap the slider's length to the current beat divisor
// to calculate the final resulting duration / bounding box before the final checks.
slider.SnapTo(snapProvider);
//if sliderhead or sliderend end up outside playfield, revert scaling.
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
if (xInBounds && yInBounds && slider.Path.HasValidLength)
return;
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
slider.Path.ControlPoints[i].Position = originalPathPositions[i];
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
slider.SnapTo(snapProvider);
}
private (bool X, bool Y) isQuadInBounds(Quad quad)
{
bool xInBounds = (quad.TopLeft.X >= 0) && (quad.BottomRight.X <= OsuPlayfield.BASE_SIZE.X);
bool yInBounds = (quad.TopLeft.Y >= 0) && (quad.BottomRight.Y <= OsuPlayfield.BASE_SIZE.Y);
return (xInBounds, yInBounds);
}
/// <summary>
/// Clamp scale for multi-object-scaling where selection does not exceed playfield bounds or flip.
/// </summary>
/// <param name="selectionQuad">The quad surrounding the hitobjects</param>
/// <param name="origin">The origin from which the scale operation is performed</param>
/// <param name="scale">The scale to be clamped</param>
/// <returns>The clamped scale vector</returns>
private Vector2 getClampedScale(Quad selectionQuad, Vector2 origin, Vector2 scale)
{
//todo: this is not always correct for selections involving sliders. This approximation assumes each point is scaled independently, but sliderends move with the sliderhead.
var tl1 = Vector2.Divide(-origin, selectionQuad.TopLeft - origin);
var tl2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - origin, selectionQuad.TopLeft - origin);
var br1 = Vector2.Divide(-origin, selectionQuad.BottomRight - origin);
var br2 = Vector2.Divide(OsuPlayfield.BASE_SIZE - origin, selectionQuad.BottomRight - origin);
if (!Precision.AlmostEquals(selectionQuad.TopLeft.X - origin.X, 0))
scale.X = selectionQuad.TopLeft.X - origin.X < 0 ? MathHelper.Clamp(scale.X, tl2.X, tl1.X) : MathHelper.Clamp(scale.X, tl1.X, tl2.X);
if (!Precision.AlmostEquals(selectionQuad.TopLeft.Y - origin.Y, 0))
scale.Y = selectionQuad.TopLeft.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, tl2.Y, tl1.Y) : MathHelper.Clamp(scale.Y, tl1.Y, tl2.Y);
if (!Precision.AlmostEquals(selectionQuad.BottomRight.X - origin.X, 0))
scale.X = selectionQuad.BottomRight.X - origin.X < 0 ? MathHelper.Clamp(scale.X, br2.X, br1.X) : MathHelper.Clamp(scale.X, br1.X, br2.X);
if (!Precision.AlmostEquals(selectionQuad.BottomRight.Y - origin.Y, 0))
scale.Y = selectionQuad.BottomRight.Y - origin.Y < 0 ? MathHelper.Clamp(scale.Y, br2.Y, br1.Y) : MathHelper.Clamp(scale.Y, br1.Y, br2.Y);
return Vector2.ComponentMax(scale, new Vector2(Precision.FLOAT_EPSILON));
}
private void moveSelectionInBounds()
{
Quad quad = GeometryUtils.GetSurroundingQuad(objectsInScale!.Keys);
Vector2 delta = Vector2.Zero;
if (quad.TopLeft.X < 0)
delta.X -= quad.TopLeft.X;
if (quad.TopLeft.Y < 0)
delta.Y -= quad.TopLeft.Y;
if (quad.BottomRight.X > OsuPlayfield.BASE_SIZE.X)
delta.X -= quad.BottomRight.X - OsuPlayfield.BASE_SIZE.X;
if (quad.BottomRight.Y > OsuPlayfield.BASE_SIZE.Y)
delta.Y -= quad.BottomRight.Y - OsuPlayfield.BASE_SIZE.Y;
foreach (var (h, _) in objectsInScale!)
h.Position += delta;
}
private struct OriginalHitObjectState
{
public Vector2 Position { get; }
public Vector2[]? PathControlPointPositions { get; }
public PathType?[]? PathControlPointTypes { get; }
public OriginalHitObjectState(OsuHitObject hitObject)
{
Position = hitObject.Position;
PathControlPointPositions = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Position).ToArray();
PathControlPointTypes = (hitObject as IHasPath)?.Path.ControlPoints.Select(p => p.Type).ToArray();
}
}
}
}

View File

@ -19,6 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override LocalisableString Description => "Put your faith in the approach circles..."; public override LocalisableString Description => "Put your faith in the approach circles...";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override bool Ranked => true;
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) }; public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) };

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Graphics;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning.Default namespace osu.Game.Rulesets.Osu.Skinning.Default
@ -11,10 +12,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
/// </summary> /// </summary>
public partial class ManualSliderBody : SliderBody public partial class ManualSliderBody : SliderBody
{ {
public new void SetVertices(IReadOnlyList<Vector2> vertices) public ManualSliderBody()
{ {
base.SetVertices(vertices); AutoSizeAxes = Axes.Both;
Size = Path.Size;
} }
public new void SetVertices(IReadOnlyList<Vector2> vertices) => base.SetVertices(vertices);
} }
} }

View File

@ -26,12 +26,5 @@ namespace osu.Game.Rulesets.Taiko.Edit
ShowSpeedChanges.BindValueChanged(showChanges => VisualisationMethod = showChanges.NewValue ? ScrollVisualisationMethod.Overlapping : ScrollVisualisationMethod.Constant, true); ShowSpeedChanges.BindValueChanged(showChanges => VisualisationMethod = showChanges.NewValue ? ScrollVisualisationMethod.Overlapping : ScrollVisualisationMethod.Constant, true);
} }
protected override double ComputeTimeRange()
{
// Adjust when we're using constant algorithm to not be sluggish.
double multiplier = ShowSpeedChanges.Value ? 1 : 4;
return base.ComputeTimeRange() / multiplier;
}
} }
} }

View File

@ -0,0 +1,29 @@
// 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.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModConstantSpeed : Mod, IApplicableToDrawableRuleset<TaikoHitObject>
{
public override string Name => "Constant Speed";
public override string Acronym => "CS";
public override double ScoreMultiplier => 0.9;
public override LocalisableString Description => "No more tricky speed changes!";
public override IconUsage? Icon => FontAwesome.Solid.Equals;
public override ModType Type => ModType.Conversion;
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
{
var taikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
taikoRuleset.VisualisationMethod = ScrollVisualisationMethod.Constant;
}
}
}

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
Origin = Anchor.Centre, Origin = Anchor.Centre,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Alpha = 0, Alpha = 0,
Scale = new Vector2(0.7f), Scale = new Vector2(TaikoLegacyHitTarget.SCALE),
Colour = new Colour4(255, 228, 0, 255), Colour = new Colour4(255, 228, 0, 255),
}; };
@ -58,8 +58,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
if (!result.IsHit || !isKiaiActive) if (!result.IsHit || !isKiaiActive)
return; return;
sprite.ScaleTo(0.85f).Then() sprite.ScaleTo(TaikoLegacyHitTarget.SCALE + 0.15f).Then()
.ScaleTo(0.7f, 80, Easing.OutQuad); .ScaleTo(TaikoLegacyHitTarget.SCALE, 80, Easing.OutQuad);
} }
} }
} }

View File

@ -12,6 +12,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{ {
public partial class TaikoLegacyHitTarget : CompositeDrawable public partial class TaikoLegacyHitTarget : CompositeDrawable
{ {
/// <summary>
/// In stable this is 0.7f (see https://github.com/peppy/osu-stable-reference/blob/7519cafd1823f1879c0d9c991ba0e5c7fd3bfa02/osu!/GameModes/Play/Rulesets/Taiko/RulesetTaiko.cs#L592)
/// but for whatever reason this doesn't match visually.
/// </summary>
public const float SCALE = 0.8f;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISkinSource skin) private void load(ISkinSource skin)
{ {
@ -22,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
new Sprite new Sprite
{ {
Texture = skin.GetTexture("approachcircle"), Texture = skin.GetTexture("approachcircle"),
Scale = new Vector2(0.83f), Scale = new Vector2(SCALE + 0.03f),
Alpha = 0.47f, // eyeballed to match stable Alpha = 0.47f, // eyeballed to match stable
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -30,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
new Sprite new Sprite
{ {
Texture = skin.GetTexture("taikobigcircle"), Texture = skin.GetTexture("taikobigcircle"),
Scale = new Vector2(0.8f), Scale = new Vector2(SCALE),
Alpha = 0.22f, // eyeballed to match stable Alpha = 0.22f, // eyeballed to match stable
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -150,6 +150,7 @@ namespace osu.Game.Rulesets.Taiko
new TaikoModClassic(), new TaikoModClassic(),
new TaikoModSwap(), new TaikoModSwap(),
new TaikoModSingleTap(), new TaikoModSingleTap(),
new TaikoModConstantSpeed(),
}; };
case ModType.Automation: case ModType.Automation:

View File

@ -82,7 +82,12 @@ namespace osu.Game.Rulesets.Taiko.UI
TimeRange.Value = ComputeTimeRange(); TimeRange.Value = ComputeTimeRange();
} }
protected virtual double ComputeTimeRange() => PlayfieldAdjustmentContainer.ComputeTimeRange(); protected virtual double ComputeTimeRange()
{
// Adjust when we're using constant algorithm to not be sluggish.
double multiplier = VisualisationMethod == ScrollVisualisationMethod.Constant ? 4 * Beatmap.Difficulty.SliderMultiplier : 1;
return PlayfieldAdjustmentContainer.ComputeTimeRange() / multiplier;
}
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()
{ {

View File

@ -1188,5 +1188,36 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(beatmap.HitObjects[0].GetEndTime(), Is.EqualTo(3153)); Assert.That(beatmap.HitObjects[0].GetEndTime(), Is.EqualTo(3153));
} }
} }
[Test]
public void TestBeatmapDifficultyIsClamped()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("out-of-range-difficulties.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var decoded = decoder.Decode(stream).Difficulty;
Assert.That(decoded.DrainRate, Is.EqualTo(10));
Assert.That(decoded.CircleSize, Is.EqualTo(10));
Assert.That(decoded.OverallDifficulty, Is.EqualTo(10));
Assert.That(decoded.ApproachRate, Is.EqualTo(10));
Assert.That(decoded.SliderMultiplier, Is.EqualTo(3.6));
Assert.That(decoded.SliderTickRate, Is.EqualTo(8));
}
}
[Test]
public void TestManiaBeatmapDifficultyCircleSizeClamp()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("out-of-range-difficulties-mania.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var decoded = decoder.Decode(stream).Difficulty;
Assert.That(decoded.CircleSize, Is.EqualTo(14));
}
}
} }
} }

View File

@ -0,0 +1,5 @@
[General]
Mode: 3
[Difficulty]
CircleSize:14

View File

@ -0,0 +1,10 @@
[General]
Mode: 0
[Difficulty]
HPDrainRate:25
CircleSize:25
OverallDifficulty:25
ApproachRate:30
SliderMultiplier:30
SliderTickRate:30

View File

@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
var beatmapSet = CreateAPIBeatmapSet(Ruleset.Value); var beatmapSet = CreateAPIBeatmapSet(Ruleset.Value);
beatmapSet.OnlineID = 241526; // ID hardcoded to ensure that the preview track exists online. beatmapSet.OnlineID = 241526; // ID hardcoded to ensure that the preview track exists online.
Child = thumbnail = new BeatmapCardThumbnail(beatmapSet) Child = thumbnail = new BeatmapCardThumbnail(beatmapSet, beatmapSet)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -10,9 +10,11 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Utils;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -26,9 +28,13 @@ namespace osu.Game.Tests.Visual.Editing
[Cached(typeof(SelectionRotationHandler))] [Cached(typeof(SelectionRotationHandler))]
private TestSelectionRotationHandler rotationHandler; private TestSelectionRotationHandler rotationHandler;
[Cached(typeof(SelectionScaleHandler))]
private TestSelectionScaleHandler scaleHandler;
public TestSceneComposeSelectBox() public TestSceneComposeSelectBox()
{ {
rotationHandler = new TestSelectionRotationHandler(() => selectionArea); rotationHandler = new TestSelectionRotationHandler(() => selectionArea);
scaleHandler = new TestSelectionScaleHandler(() => selectionArea);
} }
[SetUp] [SetUp]
@ -45,13 +51,8 @@ namespace osu.Game.Tests.Visual.Editing
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
CanScaleX = true,
CanScaleY = true,
CanScaleDiagonally = true,
CanFlipX = true, CanFlipX = true,
CanFlipY = true, CanFlipY = true,
OnScale = handleScale
} }
} }
}; };
@ -60,27 +61,6 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.ReleaseButton(MouseButton.Left); InputManager.ReleaseButton(MouseButton.Left);
}); });
private bool handleScale(Vector2 amount, Anchor reference)
{
if ((reference & Anchor.y1) == 0)
{
int directionY = (reference & Anchor.y0) > 0 ? -1 : 1;
if (directionY < 0)
selectionArea.Y += amount.Y;
selectionArea.Height += directionY * amount.Y;
}
if ((reference & Anchor.x1) == 0)
{
int directionX = (reference & Anchor.x0) > 0 ? -1 : 1;
if (directionX < 0)
selectionArea.X += amount.X;
selectionArea.Width += directionX * amount.X;
}
return true;
}
private partial class TestSelectionRotationHandler : SelectionRotationHandler private partial class TestSelectionRotationHandler : SelectionRotationHandler
{ {
private readonly Func<Container> getTargetContainer; private readonly Func<Container> getTargetContainer;
@ -125,6 +105,51 @@ namespace osu.Game.Tests.Visual.Editing
} }
} }
private partial class TestSelectionScaleHandler : SelectionScaleHandler
{
private readonly Func<Container> getTargetContainer;
public TestSelectionScaleHandler(Func<Container> getTargetContainer)
{
this.getTargetContainer = getTargetContainer;
CanScaleX.Value = true;
CanScaleY.Value = true;
CanScaleDiagonally.Value = true;
}
[CanBeNull]
private Container targetContainer;
public override void Begin()
{
if (targetContainer != null)
throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!");
targetContainer = getTargetContainer();
OriginalSurroundingQuad = new Quad(targetContainer!.X, targetContainer.Y, targetContainer.Width, targetContainer.Height);
}
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
{
if (targetContainer == null)
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
Vector2 actualOrigin = origin ?? Vector2.Zero;
targetContainer.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, OriginalSurroundingQuad!.Value.TopLeft);
targetContainer.Size = OriginalSurroundingQuad!.Value.Size * scale;
}
public override void Commit()
{
if (targetContainer == null)
throw new InvalidOperationException($"Cannot {nameof(Commit)} a scale operation without calling {nameof(Begin)} first!");
targetContainer = null;
}
}
[Test] [Test]
public void TestRotationHandleShownOnHover() public void TestRotationHandleShownOnHover()
{ {

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using Humanizer; using Humanizer;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Input;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -396,7 +397,7 @@ namespace osu.Game.Tests.Visual.Editing
textBox.Current.Value = bank; textBox.Current.Value = bank;
// force a commit via keyboard. // force a commit via keyboard.
// this is needed when testing attempting to set empty bank - which should revert to the previous value, but only on commit. // this is needed when testing attempting to set empty bank - which should revert to the previous value, but only on commit.
InputManager.ChangeFocus(textBox); ((IFocusManager)InputManager).ChangeFocus(textBox);
InputManager.Key(Key.Enter); InputManager.Key(Key.Enter);
}); });

View File

@ -6,6 +6,7 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -62,12 +63,12 @@ namespace osu.Game.Tests.Visual.Editing
createLabelledTimeSignature(TimeSignature.SimpleQuadruple); createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple)); AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
AddStep("focus text box", () => InputManager.ChangeFocus(numeratorTextBox)); AddStep("focus text box", () => ((IFocusManager)InputManager).ChangeFocus(numeratorTextBox));
AddStep("set numerator to 7", () => numeratorTextBox.Current.Value = "7"); AddStep("set numerator to 7", () => numeratorTextBox.Current.Value = "7");
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple)); AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
AddStep("drop focus", () => InputManager.ChangeFocus(null)); AddStep("drop focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
AddAssert("current is 7/4", () => timeSignature.Current.Value.Equals(new TimeSignature(7))); AddAssert("current is 7/4", () => timeSignature.Current.Value.Equals(new TimeSignature(7)));
} }
@ -77,12 +78,12 @@ namespace osu.Game.Tests.Visual.Editing
createLabelledTimeSignature(TimeSignature.SimpleQuadruple); createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple)); AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
AddStep("focus text box", () => InputManager.ChangeFocus(numeratorTextBox)); AddStep("focus text box", () => ((IFocusManager)InputManager).ChangeFocus(numeratorTextBox));
AddStep("set numerator to 0", () => numeratorTextBox.Current.Value = "0"); AddStep("set numerator to 0", () => numeratorTextBox.Current.Value = "0");
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple)); AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
AddStep("drop focus", () => InputManager.ChangeFocus(null)); AddStep("drop focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple)); AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
AddAssert("numerator is 4", () => numeratorTextBox.Current.Value == "4"); AddAssert("numerator is 4", () => numeratorTextBox.Current.Value == "4");
} }

View File

@ -3,17 +3,22 @@
#nullable disable #nullable disable
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Edit.Setup;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing namespace osu.Game.Tests.Visual.Editing
{ {
public partial class TestSceneMetadataSection : OsuTestScene public partial class TestSceneMetadataSection : OsuManualInputManagerTestScene
{ {
[Cached] [Cached]
private EditorBeatmap editorBeatmap = new EditorBeatmap(new Beatmap private EditorBeatmap editorBeatmap = new EditorBeatmap(new Beatmap
@ -26,6 +31,81 @@ namespace osu.Game.Tests.Visual.Editing
private TestMetadataSection metadataSection; private TestMetadataSection metadataSection;
[Test]
public void TestUpdateViaTextBoxOnFocusLoss()
{
AddStep("set metadata", () =>
{
editorBeatmap.Metadata.Artist = "Example Artist";
editorBeatmap.Metadata.ArtistUnicode = string.Empty;
});
createSection();
TextBox textbox;
AddStep("focus first textbox", () =>
{
textbox = metadataSection.ChildrenOfType<TextBox>().First();
InputManager.MoveMouseTo(textbox);
InputManager.Click(MouseButton.Left);
});
AddStep("simulate changing textbox", () =>
{
// Can't simulate text input but this should work.
InputManager.Keys(PlatformAction.SelectAll);
InputManager.Keys(PlatformAction.Copy);
InputManager.Keys(PlatformAction.Paste);
InputManager.Keys(PlatformAction.Paste);
});
assertArtistMetadata("Example Artist");
// It's important values are committed immediately on focus loss so the editor exit sequence detects them.
AddAssert("value immediately changed on focus loss", () =>
{
((IFocusManager)InputManager).TriggerFocusContention(metadataSection);
return editorBeatmap.Metadata.Artist;
}, () => Is.EqualTo("Example ArtistExample Artist"));
}
[Test]
public void TestUpdateViaTextBoxOnCommit()
{
AddStep("set metadata", () =>
{
editorBeatmap.Metadata.Artist = "Example Artist";
editorBeatmap.Metadata.ArtistUnicode = string.Empty;
});
createSection();
TextBox textbox;
AddStep("focus first textbox", () =>
{
textbox = metadataSection.ChildrenOfType<TextBox>().First();
InputManager.MoveMouseTo(textbox);
InputManager.Click(MouseButton.Left);
});
AddStep("simulate changing textbox", () =>
{
// Can't simulate text input but this should work.
InputManager.Keys(PlatformAction.SelectAll);
InputManager.Keys(PlatformAction.Copy);
InputManager.Keys(PlatformAction.Paste);
InputManager.Keys(PlatformAction.Paste);
});
assertArtistMetadata("Example Artist");
AddStep("commit", () => InputManager.Key(Key.Enter));
assertArtistMetadata("Example ArtistExample Artist");
}
[Test] [Test]
public void TestMinimalMetadata() public void TestMinimalMetadata()
{ {
@ -40,7 +120,7 @@ namespace osu.Game.Tests.Visual.Editing
createSection(); createSection();
assertArtist("Example Artist"); assertArtistTextBox("Example Artist");
assertRomanisedArtist("Example Artist", false); assertRomanisedArtist("Example Artist", false);
assertTitle("Example Title"); assertTitle("Example Title");
@ -61,7 +141,7 @@ namespace osu.Game.Tests.Visual.Editing
createSection(); createSection();
assertArtist("*なみりん"); assertArtistTextBox("*なみりん");
assertRomanisedArtist(string.Empty, true); assertRomanisedArtist(string.Empty, true);
assertTitle("コイシテイク・プラネット"); assertTitle("コイシテイク・プラネット");
@ -82,7 +162,7 @@ namespace osu.Game.Tests.Visual.Editing
createSection(); createSection();
assertArtist("*なみりん"); assertArtistTextBox("*なみりん");
assertRomanisedArtist("*namirin", true); assertRomanisedArtist("*namirin", true);
assertTitle("コイシテイク・プラネット"); assertTitle("コイシテイク・プラネット");
@ -104,11 +184,11 @@ namespace osu.Game.Tests.Visual.Editing
createSection(); createSection();
AddStep("set romanised artist name", () => metadataSection.ArtistTextBox.Current.Value = "*namirin"); AddStep("set romanised artist name", () => metadataSection.ArtistTextBox.Current.Value = "*namirin");
assertArtist("*namirin"); assertArtistTextBox("*namirin");
assertRomanisedArtist("*namirin", false); assertRomanisedArtist("*namirin", false);
AddStep("set native artist name", () => metadataSection.ArtistTextBox.Current.Value = "*なみりん"); AddStep("set native artist name", () => metadataSection.ArtistTextBox.Current.Value = "*なみりん");
assertArtist("*なみりん"); assertArtistTextBox("*なみりん");
assertRomanisedArtist("*namirin", true); assertRomanisedArtist("*namirin", true);
AddStep("set romanised title", () => metadataSection.TitleTextBox.Current.Value = "Hitokoto no kyori"); AddStep("set romanised title", () => metadataSection.TitleTextBox.Current.Value = "Hitokoto no kyori");
@ -123,21 +203,24 @@ namespace osu.Game.Tests.Visual.Editing
private void createSection() private void createSection()
=> AddStep("create metadata section", () => Child = metadataSection = new TestMetadataSection()); => AddStep("create metadata section", () => Child = metadataSection = new TestMetadataSection());
private void assertArtist(string expected) private void assertArtistMetadata(string expected)
=> AddAssert($"artist is {expected}", () => metadataSection.ArtistTextBox.Current.Value == expected); => AddAssert($"artist metadata is {expected}", () => editorBeatmap.Metadata.Artist, () => Is.EqualTo(expected));
private void assertArtistTextBox(string expected)
=> AddAssert($"artist textbox is {expected}", () => metadataSection.ArtistTextBox.Current.Value, () => Is.EqualTo(expected));
private void assertRomanisedArtist(string expected, bool editable) private void assertRomanisedArtist(string expected, bool editable)
{ {
AddAssert($"romanised artist is {expected}", () => metadataSection.RomanisedArtistTextBox.Current.Value == expected); AddAssert($"romanised artist is {expected}", () => metadataSection.RomanisedArtistTextBox.Current.Value, () => Is.EqualTo(expected));
AddAssert($"romanised artist is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedArtistTextBox.ReadOnly == !editable); AddAssert($"romanised artist is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedArtistTextBox.ReadOnly == !editable);
} }
private void assertTitle(string expected) private void assertTitle(string expected)
=> AddAssert($"title is {expected}", () => metadataSection.TitleTextBox.Current.Value == expected); => AddAssert($"title is {expected}", () => metadataSection.TitleTextBox.Current.Value, () => Is.EqualTo(expected));
private void assertRomanisedTitle(string expected, bool editable) private void assertRomanisedTitle(string expected, bool editable)
{ {
AddAssert($"romanised title is {expected}", () => metadataSection.RomanisedTitleTextBox.Current.Value == expected); AddAssert($"romanised title is {expected}", () => metadataSection.RomanisedTitleTextBox.Current.Value, () => Is.EqualTo(expected));
AddAssert($"romanised title is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedTitleTextBox.ReadOnly == !editable); AddAssert($"romanised title is {(editable ? "" : "not ")}editable", () => metadataSection.RomanisedTitleTextBox.ReadOnly == !editable);
} }

View File

@ -16,6 +16,7 @@ using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Cursor;
@ -317,13 +318,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
p.RequestResults = _ => resultsRequested = true; p.RequestResults = _ => resultsRequested = true;
}); });
AddUntilStep("wait for load", () => playlist.ChildrenOfType<DrawableLinkCompiler>().Any() && playlist.ChildrenOfType<BeatmapCardThumbnail>().First().DrawWidth > 0);
AddStep("move mouse to first item title", () => AddStep("move mouse to first item title", () =>
{ {
var drawQuad = playlist.ChildrenOfType<LinkFlowContainer>().First().ScreenSpaceDrawQuad; var drawQuad = playlist.ChildrenOfType<LinkFlowContainer>().First().ScreenSpaceDrawQuad;
var location = (drawQuad.TopLeft + drawQuad.BottomLeft) / 2 + new Vector2(drawQuad.Width * 0.2f, 0); var location = (drawQuad.TopLeft + drawQuad.BottomLeft) / 2 + new Vector2(drawQuad.Width * 0.2f, 0);
InputManager.MoveMouseTo(location); InputManager.MoveMouseTo(location);
}); });
AddUntilStep("wait for text load", () => playlist.ChildrenOfType<DrawableLinkCompiler>().Any());
AddAssert("first item title not hovered", () => playlist.ChildrenOfType<DrawableLinkCompiler>().First().IsHovered, () => Is.False); AddAssert("first item title not hovered", () => playlist.ChildrenOfType<DrawableLinkCompiler>().First().IsHovered, () => Is.False);
AddStep("click left mouse", () => InputManager.Click(MouseButton.Left)); AddStep("click left mouse", () => InputManager.Click(MouseButton.Left));
AddUntilStep("first item selected", () => playlist.ChildrenOfType<DrawableRoomPlaylistItem>().First().IsSelectedItem, () => Is.True); AddUntilStep("first item selected", () => playlist.ChildrenOfType<DrawableRoomPlaylistItem>().First().IsSelectedItem, () => Is.True);

View File

@ -7,6 +7,7 @@ using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -17,6 +18,7 @@ using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.GameplayTest; using osu.Game.Screens.Edit.GameplayTest;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter; using osu.Game.Screens.Select.Filter;
@ -27,6 +29,59 @@ namespace osu.Game.Tests.Visual.Navigation
{ {
public partial class TestSceneBeatmapEditorNavigation : OsuGameTestScene public partial class TestSceneBeatmapEditorNavigation : OsuGameTestScene
{ {
[Test]
public void TestChangeMetadataExitWhileTextboxFocusedPromptsSave()
{
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);
AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo);
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);
AddStep("change to song setup", () => InputManager.Key(Key.F4));
TextBox textbox = null!;
AddUntilStep("wait for metadata section", () =>
{
var t = Game.ChildrenOfType<MetadataSection>().SingleOrDefault().ChildrenOfType<TextBox>().FirstOrDefault();
if (t == null)
return false;
textbox = t;
return true;
});
AddStep("focus textbox", () =>
{
InputManager.MoveMouseTo(textbox);
InputManager.Click(MouseButton.Left);
});
AddStep("simulate changing textbox", () =>
{
// Can't simulate text input but this should work.
InputManager.Keys(PlatformAction.SelectAll);
InputManager.Keys(PlatformAction.Copy);
InputManager.Keys(PlatformAction.Paste);
InputManager.Keys(PlatformAction.Paste);
});
AddStep("exit", () => Game.ChildrenOfType<Editor>().Single().Exit());
AddUntilStep("save dialog displayed", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault()?.CurrentDialog is PromptForSaveDialog);
}
[Test] [Test]
public void TestEditorGameplayTestAlwaysUsesOriginalRuleset() public void TestEditorGameplayTestAlwaysUsesOriginalRuleset()
{ {

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
@ -26,7 +24,7 @@ namespace osu.Game.Tests.Visual.Navigation
{ {
public partial class TestScenePresentScore : OsuGameTestScene public partial class TestScenePresentScore : OsuGameTestScene
{ {
private BeatmapSetInfo beatmap; private BeatmapSetInfo beatmap = null!;
[SetUpSteps] [SetUpSteps]
public new void SetUpSteps() public new void SetUpSteps()
@ -64,7 +62,7 @@ namespace osu.Game.Tests.Visual.Navigation
Ruleset = new OsuRuleset().RulesetInfo Ruleset = new OsuRuleset().RulesetInfo
}, },
} }
})?.Value; })!.Value;
}); });
} }
@ -158,6 +156,27 @@ namespace osu.Game.Tests.Visual.Navigation
presentAndConfirm(secondImport, type); presentAndConfirm(secondImport, type);
} }
[Test]
public void TestScoreRefetchIgnoresEmptyHash()
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
importScore(-1, hash: string.Empty);
importScore(3, hash: @"deadbeef");
// oftentimes a `PresentScore()` call will be given a `ScoreInfo` which is converted from an online score,
// in which cases the hash will generally not be available.
AddStep("present score", () => Game.PresentScore(new ScoreInfo { OnlineID = 3, Hash = string.Empty }));
AddUntilStep("wait for results", () => lastWaitedScreen != Game.ScreenStack.CurrentScreen && Game.ScreenStack.CurrentScreen is ResultsScreen);
AddUntilStep("correct score displayed", () =>
{
var score = ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score!;
return score.OnlineID == 3 && score.Hash == "deadbeef";
});
}
private void returnToMenu() private void returnToMenu()
{ {
// if we don't pause, there's a chance the track may change at the main menu out of our control (due to reaching the end of the track). // if we don't pause, there's a chance the track may change at the main menu out of our control (due to reaching the end of the track).
@ -171,14 +190,14 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu); AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
} }
private Func<ScoreInfo> importScore(int i, RulesetInfo ruleset = null) private Func<ScoreInfo> importScore(int i, RulesetInfo? ruleset = null, string? hash = null)
{ {
ScoreInfo imported = null; ScoreInfo? imported = null;
AddStep($"import score {i}", () => AddStep($"import score {i}", () =>
{ {
imported = Game.ScoreManager.Import(new ScoreInfo imported = Game.ScoreManager.Import(new ScoreInfo
{ {
Hash = Guid.NewGuid().ToString(), Hash = hash ?? Guid.NewGuid().ToString(),
OnlineID = i, OnlineID = i,
BeatmapInfo = beatmap.Beatmaps.First(), BeatmapInfo = beatmap.Beatmaps.First(),
Ruleset = ruleset ?? new OsuRuleset().RulesetInfo, Ruleset = ruleset ?? new OsuRuleset().RulesetInfo,
@ -188,14 +207,14 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert($"import {i} succeeded", () => imported != null); AddAssert($"import {i} succeeded", () => imported != null);
return () => imported; return () => imported!;
} }
/// <summary> /// <summary>
/// Some tests test waiting for a particular screen twice in a row, but expect a new instance each time. /// Some tests test waiting for a particular screen twice in a row, but expect a new instance each time.
/// There's a case where they may succeed incorrectly if we don't compare against the previous instance. /// There's a case where they may succeed incorrectly if we don't compare against the previous instance.
/// </summary> /// </summary>
private IScreen lastWaitedScreen; private IScreen lastWaitedScreen = null!;
private void presentAndConfirm(Func<ScoreInfo> getImport, ScorePresentType type) private void presentAndConfirm(Func<ScoreInfo> getImport, ScorePresentType type)
{ {

View File

@ -87,6 +87,105 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("delete all beatmaps", () => manager.Delete()); AddStep("delete all beatmaps", () => manager.Delete());
} }
[Test]
public void TestSpeedChange()
{
createSongSelect();
changeMods();
decreaseModSpeed();
AddAssert("half time activated at 0.95x", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005));
decreaseModSpeed();
AddAssert("half time speed changed to 0.9x", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().SpeedChange.Value, () => Is.EqualTo(0.9).Within(0.005));
increaseModSpeed();
AddAssert("half time speed changed to 0.95x", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005));
increaseModSpeed();
AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0);
increaseModSpeed();
AddAssert("double time activated at 1.05x", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005));
increaseModSpeed();
AddAssert("double time speed changed to 1.1x", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().SpeedChange.Value, () => Is.EqualTo(1.1).Within(0.005));
decreaseModSpeed();
AddAssert("double time speed changed to 1.05x", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005));
OsuModNightcore nc = new OsuModNightcore
{
SpeedChange = { Value = 1.05 }
};
changeMods(nc);
increaseModSpeed();
AddAssert("nightcore speed changed to 1.1x", () => songSelect!.Mods.Value.OfType<ModNightcore>().Single().SpeedChange.Value, () => Is.EqualTo(1.1).Within(0.005));
decreaseModSpeed();
AddAssert("nightcore speed changed to 1.05x", () => songSelect!.Mods.Value.OfType<ModNightcore>().Single().SpeedChange.Value, () => Is.EqualTo(1.05).Within(0.005));
decreaseModSpeed();
AddAssert("no mods selected", () => songSelect!.Mods.Value.Count == 0);
decreaseModSpeed();
AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType<ModDaycore>().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005));
decreaseModSpeed();
AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType<ModDaycore>().Single().SpeedChange.Value, () => Is.EqualTo(0.9).Within(0.005));
increaseModSpeed();
AddAssert("daycore activated at 0.95x", () => songSelect!.Mods.Value.OfType<ModDaycore>().Single().SpeedChange.Value, () => Is.EqualTo(0.95).Within(0.005));
OsuModDoubleTime dt = new OsuModDoubleTime
{
SpeedChange = { Value = 1.02 },
AdjustPitch = { Value = true },
};
changeMods(dt);
decreaseModSpeed();
AddAssert("half time activated at 0.97x", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().SpeedChange.Value, () => Is.EqualTo(0.97).Within(0.005));
AddAssert("adjust pitch preserved", () => songSelect!.Mods.Value.OfType<ModHalfTime>().Single().AdjustPitch.Value, () => Is.True);
OsuModHalfTime ht = new OsuModHalfTime
{
SpeedChange = { Value = 0.97 },
AdjustPitch = { Value = true },
};
Mod[] modlist = { ht, new OsuModHardRock(), new OsuModHidden() };
changeMods(modlist);
increaseModSpeed();
AddAssert("double time activated at 1.02x", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().SpeedChange.Value, () => Is.EqualTo(1.02).Within(0.005));
AddAssert("double time activated at 1.02x", () => songSelect!.Mods.Value.OfType<ModDoubleTime>().Single().AdjustPitch.Value, () => Is.True);
AddAssert("HD still enabled", () => songSelect!.Mods.Value.OfType<ModHidden>().SingleOrDefault(), () => Is.Not.Null);
AddAssert("HR still enabled", () => songSelect!.Mods.Value.OfType<ModHardRock>().SingleOrDefault(), () => Is.Not.Null);
changeMods(new ModWindUp());
increaseModSpeed();
AddAssert("windup still active", () => songSelect!.Mods.Value.First() is ModWindUp);
changeMods(new ModAdaptiveSpeed());
increaseModSpeed();
AddAssert("adaptive speed still active", () => songSelect!.Mods.Value.First() is ModAdaptiveSpeed);
void increaseModSpeed() => AddStep("increase mod speed", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.Up);
InputManager.ReleaseKey(Key.ControlLeft);
});
void decreaseModSpeed() => AddStep("decrease mod speed", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.Down);
InputManager.ReleaseKey(Key.ControlLeft);
});
}
[Test] [Test]
public void TestPlaceholderBeatmapPresence() public void TestPlaceholderBeatmapPresence()
{ {

View File

@ -0,0 +1,83 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.Metadata;
using osu.Game.Online.Rooms;
using osu.Game.Screens.Menu;
using osuTK.Input;
using Color4 = osuTK.Graphics.Color4;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public partial class TestSceneMainMenuButton : OsuTestScene
{
[Resolved]
private MetadataClient metadataClient { get; set; } = null!;
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
[Test]
public void TestStandardButton()
{
AddStep("add button", () => Child = new MainMenuButton(
ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), _ => { }, 0, Key.P)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
ButtonSystemState = ButtonSystemState.TopLevel,
});
}
[Test]
public void TestDailyChallengeButton()
{
AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null));
AddStep("set up API", () => dummyAPI.HandleRequest = req =>
{
switch (req)
{
case GetRoomRequest getRoomRequest:
if (getRoomRequest.RoomId != 1234)
return false;
var beatmap = CreateAPIBeatmap();
beatmap.OnlineID = 1001;
getRoomRequest.TriggerSuccess(new Room
{
RoomID = { Value = 1234 },
Playlist =
{
new PlaylistItem(beatmap)
},
EndDate = { Value = DateTimeOffset.Now.AddSeconds(30) }
});
return true;
default:
return false;
}
});
AddStep("add button", () => Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
ButtonSystemState = ButtonSystemState.TopLevel,
});
AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo
{
RoomID = 1234,
}));
}
}
}

View File

@ -10,6 +10,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
@ -623,7 +624,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("press tab", () => InputManager.Key(Key.Tab)); AddStep("press tab", () => InputManager.Key(Key.Tab));
AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus); AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus);
AddStep("unfocus search text box externally", () => InputManager.ChangeFocus(null)); AddStep("unfocus search text box externally", () => ((IFocusManager)InputManager).ChangeFocus(null));
AddStep("press tab", () => InputManager.Key(Key.Tab)); AddStep("press tab", () => InputManager.Key(Key.Tab));
AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus); AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus);

View File

@ -5,6 +5,7 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
@ -42,7 +43,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
AddStep("set instantaneous to false", () => sliderWithTextBoxInput.Instantaneous = false); AddStep("set instantaneous to false", () => sliderWithTextBoxInput.Instantaneous = false);
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox)); AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("change text", () => textBox.Text = "3"); AddStep("change text", () => textBox.Text = "3");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.Zero); AddAssert("slider not moved", () => slider.Current.Value, () => Is.Zero);
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.Zero); AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.Zero);
@ -61,7 +62,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("textbox changed", () => textBox.Current.Value, () => Is.EqualTo("-5")); AddAssert("textbox changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox)); AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage"); AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
@ -71,12 +72,12 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox)); AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage"); AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("lose focus", () => InputManager.ChangeFocus(null)); AddStep("lose focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5")); AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
@ -87,7 +88,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
AddStep("set instantaneous to true", () => sliderWithTextBoxInput.Instantaneous = true); AddStep("set instantaneous to true", () => sliderWithTextBoxInput.Instantaneous = true);
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox)); AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("change text", () => textBox.Text = "3"); AddStep("change text", () => textBox.Text = "3");
AddAssert("slider moved", () => slider.Current.Value, () => Is.EqualTo(3)); AddAssert("slider moved", () => slider.Current.Value, () => Is.EqualTo(3));
AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3)); AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3));
@ -106,7 +107,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("textbox not changed", () => textBox.Current.Value, () => Is.EqualTo("-5")); AddAssert("textbox not changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox)); AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage"); AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
@ -116,12 +117,12 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox)); AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage"); AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("lose focus", () => InputManager.ChangeFocus(null)); AddStep("lose focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5")); AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5)); AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5)); AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));

View File

@ -58,7 +58,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
editorInfo.Selected.ValueChanged += selection => editorInfo.Selected.ValueChanged += selection =>
{ {
// ensure any ongoing edits are committed out to the *current* selection before changing to a new one. // ensure any ongoing edits are committed out to the *current* selection before changing to a new one.
GetContainingInputManager().TriggerFocusContention(null); GetContainingFocusManager().TriggerFocusContention(null);
// Required to avoid cyclic failure in BindableWithCurrent (TriggerChange called during the Current_Set process). // Required to avoid cyclic failure in BindableWithCurrent (TriggerChange called during the Current_Set process).
// Arguable a framework issue but since we haven't hit it anywhere else a local workaround seems best. // Arguable a framework issue but since we haven't hit it anywhere else a local workaround seems best.

View File

@ -61,7 +61,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
thumbnail = new BeatmapCardThumbnail(BeatmapSet) thumbnail = new BeatmapCardThumbnail(BeatmapSet, BeatmapSet)
{ {
Name = @"Left (icon) area", Name = @"Left (icon) area",
Size = new Vector2(height), Size = new Vector2(height),

View File

@ -62,7 +62,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
thumbnail = new BeatmapCardThumbnail(BeatmapSet) thumbnail = new BeatmapCardThumbnail(BeatmapSet, BeatmapSet)
{ {
Name = @"Left (icon) area", Name = @"Left (icon) area",
Size = new Vector2(height), Size = new Vector2(height),

View File

@ -8,7 +8,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.Drawables.Cards.Buttons; using osu.Game.Beatmaps.Drawables.Cards.Buttons;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osuTK; using osuTK;
@ -36,14 +35,14 @@ namespace osu.Game.Beatmaps.Drawables.Cards
[Resolved] [Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!; private OverlayColourProvider colourProvider { get; set; } = null!;
public BeatmapCardThumbnail(APIBeatmapSet beatmapSetInfo) public BeatmapCardThumbnail(IBeatmapSetInfo beatmapSetInfo, IBeatmapSetOnlineInfo onlineInfo)
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List) new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
OnlineInfo = beatmapSetInfo OnlineInfo = onlineInfo
}, },
background = new Box background = new Box
{ {
@ -62,7 +61,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(50),
InnerRadius = 0.2f InnerRadius = 0.2f
}, },
content = new Container content = new Container
@ -93,6 +91,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
base.Update(); base.Update();
progress.Progress = playButton.Progress.Value; progress.Progress = playButton.Progress.Value;
playButton.Scale = new Vector2(DrawWidth / 100);
progress.Size = new Vector2(50 * DrawWidth / 100);
} }
private void updateState() private void updateState()

View File

@ -27,8 +27,17 @@ namespace osu.Game.Beatmaps.Drawables
set => base.Masking = value; set => base.Masking = value;
} }
public UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType coverType = BeatmapSetCoverType.Cover) protected override double LoadDelay { get; }
private readonly double timeBeforeUnload;
protected override double TransformDuration => 400;
public UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType coverType = BeatmapSetCoverType.Cover, double timeBeforeLoad = 500, double timeBeforeUnload = 1000)
{ {
LoadDelay = timeBeforeLoad;
this.timeBeforeUnload = timeBeforeUnload;
this.coverType = coverType; this.coverType = coverType;
InternalChild = new Box InternalChild = new Box
@ -38,12 +47,12 @@ namespace osu.Game.Beatmaps.Drawables
}; };
} }
protected override double LoadDelay => 500;
protected override double TransformDuration => 400;
protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad) protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad)
=> new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad); => new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad, timeBeforeUnload)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
protected override Drawable CreateDrawable(IBeatmapSetOnlineInfo model) protected override Drawable CreateDrawable(IBeatmapSetOnlineInfo model)
{ {

View File

@ -85,6 +85,8 @@ namespace osu.Game.Beatmaps.Formats
base.ParseStreamInto(stream, beatmap); base.ParseStreamInto(stream, beatmap);
applyDifficultyRestrictions(beatmap.Difficulty, beatmap);
flushPendingPoints(); flushPendingPoints();
// Objects may be out of order *only* if a user has manually edited an .osu file. // Objects may be out of order *only* if a user has manually edited an .osu file.
@ -102,10 +104,30 @@ namespace osu.Game.Beatmaps.Formats
} }
} }
/// <summary>
/// Ensures that all <see cref="BeatmapDifficulty"/> settings are within the allowed ranges.
/// See also: https://github.com/peppy/osu-stable-reference/blob/0e425c0d525ef21353c8293c235cc0621d28338b/osu!/GameplayElements/Beatmaps/Beatmap.cs#L567-L614
/// </summary>
private static void applyDifficultyRestrictions(BeatmapDifficulty difficulty, Beatmap beatmap)
{
difficulty.DrainRate = Math.Clamp(difficulty.DrainRate, 0, 10);
// mania uses "circle size" for key count, thus different allowable range
difficulty.CircleSize = beatmap.BeatmapInfo.Ruleset.OnlineID != 3
? Math.Clamp(difficulty.CircleSize, 0, 10)
: Math.Clamp(difficulty.CircleSize, 1, 18);
difficulty.OverallDifficulty = Math.Clamp(difficulty.OverallDifficulty, 0, 10);
difficulty.ApproachRate = Math.Clamp(difficulty.ApproachRate, 0, 10);
difficulty.SliderMultiplier = Math.Clamp(difficulty.SliderMultiplier, 0.4, 3.6);
difficulty.SliderTickRate = Math.Clamp(difficulty.SliderTickRate, 0.5, 8);
}
/// <summary> /// <summary>
/// Processes the beatmap such that a new combo is started the first hitobject following each break. /// Processes the beatmap such that a new combo is started the first hitobject following each break.
/// </summary> /// </summary>
private void postProcessBreaks(Beatmap beatmap) private static void postProcessBreaks(Beatmap beatmap)
{ {
int currentBreak = 0; int currentBreak = 0;
bool forceNewCombo = false; bool forceNewCombo = false;
@ -161,7 +183,7 @@ namespace osu.Game.Beatmaps.Formats
/// This method's intention is to restore those legacy defaults. /// This method's intention is to restore those legacy defaults.
/// See also: https://osu.ppy.sh/wiki/en/Client/File_formats/Osu_%28file_format%29 /// See also: https://osu.ppy.sh/wiki/en/Client/File_formats/Osu_%28file_format%29
/// </summary> /// </summary>
private void applyLegacyDefaults(BeatmapInfo beatmapInfo) private static void applyLegacyDefaults(BeatmapInfo beatmapInfo)
{ {
beatmapInfo.WidescreenStoryboard = false; beatmapInfo.WidescreenStoryboard = false;
beatmapInfo.SamplesMatchPlaybackRate = false; beatmapInfo.SamplesMatchPlaybackRate = false;
@ -402,11 +424,11 @@ namespace osu.Game.Beatmaps.Formats
break; break;
case @"SliderMultiplier": case @"SliderMultiplier":
difficulty.SliderMultiplier = Math.Clamp(Parsing.ParseDouble(pair.Value), 0.4, 3.6); difficulty.SliderMultiplier = Parsing.ParseDouble(pair.Value);
break; break;
case @"SliderTickRate": case @"SliderTickRate":
difficulty.SliderTickRate = Math.Clamp(Parsing.ParseDouble(pair.Value), 0.5, 8); difficulty.SliderTickRate = Parsing.ParseDouble(pair.Value);
break; break;
} }
} }

View File

@ -202,7 +202,7 @@ namespace osu.Game.Collections
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
AddInternal(addOrRemoveButton = new IconButton AddInternal(addOrRemoveButton = new NoFocusChangeIconButton
{ {
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
@ -271,6 +271,11 @@ namespace osu.Game.Collections
} }
protected override Drawable CreateContent() => (Content)base.CreateContent(); protected override Drawable CreateContent() => (Content)base.CreateContent();
private partial class NoFocusChangeIconButton : IconButton
{
public override bool ChangeFocusOnClick => false;
}
} }
} }
} }

View File

@ -137,7 +137,7 @@ namespace osu.Game.Collections
this.ScaleTo(0.9f, exit_duration); this.ScaleTo(0.9f, exit_duration);
// Ensure that textboxes commit // Ensure that textboxes commit
GetContainingInputManager()?.TriggerFocusContention(this); GetContainingFocusManager()?.TriggerFocusContention(this);
} }
} }
} }

View File

@ -1134,7 +1134,17 @@ namespace osu.Game.Database
case 41: case 41:
foreach (var score in migration.NewRealm.All<ScoreInfo>()) foreach (var score in migration.NewRealm.All<ScoreInfo>())
LegacyScoreDecoder.PopulateTotalScoreWithoutMods(score); {
try
{
// this can fail e.g. if a user has a score set on a ruleset that can no longer be loaded.
LegacyScoreDecoder.PopulateTotalScoreWithoutMods(score);
}
catch (Exception ex)
{
Logger.Log($@"Failed to populate total score without mods for score {score.ID}: {ex}", LoggingTarget.Database);
}
}
break; break;
} }

View File

@ -16,7 +16,6 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Scoring.Legacy; using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
namespace osu.Game.Database namespace osu.Game.Database
{ {
@ -248,8 +247,7 @@ namespace osu.Game.Database
// warning: ordering is important here - both total score and ranks are dependent on accuracy! // warning: ordering is important here - both total score and ranks are dependent on accuracy!
score.Accuracy = computeAccuracy(score, scoreProcessor); score.Accuracy = computeAccuracy(score, scoreProcessor);
score.Rank = computeRank(score, scoreProcessor); score.Rank = computeRank(score, scoreProcessor);
score.TotalScore = convertFromLegacyTotalScore(score, ruleset, beatmap); (score.TotalScoreWithoutMods, score.TotalScore) = convertFromLegacyTotalScore(score, ruleset, beatmap);
LegacyScoreDecoder.PopulateTotalScoreWithoutMods(score);
} }
/// <summary> /// <summary>
@ -273,7 +271,7 @@ namespace osu.Game.Database
// warning: ordering is important here - both total score and ranks are dependent on accuracy! // warning: ordering is important here - both total score and ranks are dependent on accuracy!
score.Accuracy = computeAccuracy(score, scoreProcessor); score.Accuracy = computeAccuracy(score, scoreProcessor);
score.Rank = computeRank(score, scoreProcessor); score.Rank = computeRank(score, scoreProcessor);
score.TotalScore = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes); (score.TotalScoreWithoutMods, score.TotalScore) = convertFromLegacyTotalScore(score, ruleset, difficulty, attributes);
} }
/// <summary> /// <summary>
@ -283,17 +281,13 @@ namespace osu.Game.Database
/// <param name="ruleset">The <see cref="Ruleset"/> in which the score was set.</param> /// <param name="ruleset">The <see cref="Ruleset"/> in which the score was set.</param>
/// <param name="beatmap">The <see cref="WorkingBeatmap"/> applicable for this score.</param> /// <param name="beatmap">The <see cref="WorkingBeatmap"/> applicable for this score.</param>
/// <returns>The standardised total score.</returns> /// <returns>The standardised total score.</returns>
private static long convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, WorkingBeatmap beatmap) private static (long withoutMods, long withMods) convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, WorkingBeatmap beatmap)
{ {
if (!score.IsLegacyScore) if (!score.IsLegacyScore)
return score.TotalScore; return (score.TotalScoreWithoutMods, score.TotalScore);
if (ruleset is not ILegacyRuleset legacyRuleset) if (ruleset is not ILegacyRuleset legacyRuleset)
return score.TotalScore; return (score.TotalScoreWithoutMods, score.TotalScore);
var mods = score.Mods;
if (mods.Any(mod => mod is ModScoreV2))
return score.TotalScore;
var playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods); var playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods);
@ -302,8 +296,13 @@ namespace osu.Game.Database
ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator(); ILegacyScoreSimulator sv1Simulator = legacyRuleset.CreateLegacyScoreSimulator();
LegacyScoreAttributes attributes = sv1Simulator.Simulate(beatmap, playableBeatmap); LegacyScoreAttributes attributes = sv1Simulator.Simulate(beatmap, playableBeatmap);
var legacyBeatmapConversionDifficultyInfo = LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap);
return convertFromLegacyTotalScore(score, ruleset, LegacyBeatmapConversionDifficultyInfo.FromBeatmap(beatmap.Beatmap), attributes); var mods = score.Mods;
if (mods.Any(mod => mod is ModScoreV2))
return ((long)Math.Round(score.TotalScore / sv1Simulator.GetLegacyScoreMultiplier(mods, legacyBeatmapConversionDifficultyInfo)), score.TotalScore);
return convertFromLegacyTotalScore(score, ruleset, legacyBeatmapConversionDifficultyInfo, attributes);
} }
/// <summary> /// <summary>
@ -314,15 +313,15 @@ namespace osu.Game.Database
/// <param name="difficulty">The beatmap difficulty.</param> /// <param name="difficulty">The beatmap difficulty.</param>
/// <param name="attributes">The legacy scoring attributes for the beatmap which the score was set on.</param> /// <param name="attributes">The legacy scoring attributes for the beatmap which the score was set on.</param>
/// <returns>The standardised total score.</returns> /// <returns>The standardised total score.</returns>
private static long convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes) private static (long withoutMods, long withMods) convertFromLegacyTotalScore(ScoreInfo score, Ruleset ruleset, LegacyBeatmapConversionDifficultyInfo difficulty, LegacyScoreAttributes attributes)
{ {
if (!score.IsLegacyScore) if (!score.IsLegacyScore)
return score.TotalScore; return (score.TotalScoreWithoutMods, score.TotalScore);
Debug.Assert(score.LegacyTotalScore != null); Debug.Assert(score.LegacyTotalScore != null);
if (ruleset is not ILegacyRuleset legacyRuleset) if (ruleset is not ILegacyRuleset legacyRuleset)
return score.TotalScore; return (score.TotalScoreWithoutMods, score.TotalScore);
double legacyModMultiplier = legacyRuleset.CreateLegacyScoreSimulator().GetLegacyScoreMultiplier(score.Mods, difficulty); double legacyModMultiplier = legacyRuleset.CreateLegacyScoreSimulator().GetLegacyScoreMultiplier(score.Mods, difficulty);
int maximumLegacyAccuracyScore = attributes.AccuracyScore; int maximumLegacyAccuracyScore = attributes.AccuracyScore;
@ -354,17 +353,18 @@ namespace osu.Game.Database
double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n); double modMultiplier = score.Mods.Select(m => m.ScoreMultiplier).Aggregate(1.0, (c, n) => c * n);
long convertedTotalScore; long convertedTotalScoreWithoutMods;
switch (score.Ruleset.OnlineID) switch (score.Ruleset.OnlineID)
{ {
case 0: case 0:
if (score.MaxCombo == 0 || score.Accuracy == 0) if (score.MaxCombo == 0 || score.Accuracy == 0)
{ {
return (long)Math.Round(( convertedTotalScoreWithoutMods = (long)Math.Round(
0 0
+ 500000 * Math.Pow(score.Accuracy, 5) + 500000 * Math.Pow(score.Accuracy, 5)
+ bonusProportion) * modMultiplier); + bonusProportion);
break;
} }
// see similar check above. // see similar check above.
@ -372,10 +372,11 @@ namespace osu.Game.Database
// are either pointless or wildly wrong. // are either pointless or wildly wrong.
if (maximumLegacyComboScore + maximumLegacyBonusScore == 0) if (maximumLegacyComboScore + maximumLegacyBonusScore == 0)
{ {
return (long)Math.Round(( convertedTotalScoreWithoutMods = (long)Math.Round(
500000 * comboProportion // as above, zero if mods result in zero multiplier, one otherwise 500000 * comboProportion // as above, zero if mods result in zero multiplier, one otherwise
+ 500000 * Math.Pow(score.Accuracy, 5) + 500000 * Math.Pow(score.Accuracy, 5)
+ bonusProportion) * modMultiplier); + bonusProportion);
break;
} }
// Assumptions: // Assumptions:
@ -472,17 +473,17 @@ namespace osu.Game.Database
double newComboScoreProportion = estimatedComboPortionInStandardisedScore / maximumAchievableComboPortionInStandardisedScore; double newComboScoreProportion = estimatedComboPortionInStandardisedScore / maximumAchievableComboPortionInStandardisedScore;
convertedTotalScore = (long)Math.Round(( convertedTotalScoreWithoutMods = (long)Math.Round(
500000 * newComboScoreProportion * score.Accuracy 500000 * newComboScoreProportion * score.Accuracy
+ 500000 * Math.Pow(score.Accuracy, 5) + 500000 * Math.Pow(score.Accuracy, 5)
+ bonusProportion) * modMultiplier); + bonusProportion);
break; break;
case 1: case 1:
convertedTotalScore = (long)Math.Round(( convertedTotalScoreWithoutMods = (long)Math.Round(
250000 * comboProportion 250000 * comboProportion
+ 750000 * Math.Pow(score.Accuracy, 3.6) + 750000 * Math.Pow(score.Accuracy, 3.6)
+ bonusProportion) * modMultiplier); + bonusProportion);
break; break;
case 2: case 2:
@ -507,28 +508,28 @@ namespace osu.Game.Database
? 0 ? 0
: (double)score.Statistics.GetValueOrDefault(HitResult.SmallTickHit) / score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit); : (double)score.Statistics.GetValueOrDefault(HitResult.SmallTickHit) / score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit);
convertedTotalScore = (long)Math.Round(( convertedTotalScoreWithoutMods = (long)Math.Round(
comboPortion * estimateComboProportionForCatch(attributes.MaxCombo, score.MaxCombo, score.Statistics.GetValueOrDefault(HitResult.Miss)) comboPortion * estimateComboProportionForCatch(attributes.MaxCombo, score.MaxCombo, score.Statistics.GetValueOrDefault(HitResult.Miss))
+ dropletsPortion * dropletsHit + dropletsPortion * dropletsHit
+ bonusProportion) * modMultiplier); + bonusProportion);
break; break;
case 3: case 3:
convertedTotalScore = (long)Math.Round(( convertedTotalScoreWithoutMods = (long)Math.Round(
850000 * comboProportion 850000 * comboProportion
+ 150000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy) + 150000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy)
+ bonusProportion) * modMultiplier); + bonusProportion);
break; break;
default: default:
convertedTotalScore = score.TotalScore; return (score.TotalScoreWithoutMods, score.TotalScore);
break;
} }
if (convertedTotalScore < 0) if (convertedTotalScoreWithoutMods < 0)
throw new InvalidOperationException($"Total score conversion operation returned invalid total of {convertedTotalScore}"); throw new InvalidOperationException($"Total score conversion operation returned invalid total of {convertedTotalScoreWithoutMods}");
return convertedTotalScore; long convertedTotalScore = (long)Math.Round(convertedTotalScoreWithoutMods * modMultiplier);
return (convertedTotalScoreWithoutMods, convertedTotalScore);
} }
/// <summary> /// <summary>

View File

@ -120,6 +120,7 @@ namespace osu.Game.Graphics
public static IconUsage Cross => get(OsuIconMapping.Cross); public static IconUsage Cross => get(OsuIconMapping.Cross);
public static IconUsage CrossCircle => get(OsuIconMapping.CrossCircle); public static IconUsage CrossCircle => get(OsuIconMapping.CrossCircle);
public static IconUsage Crown => get(OsuIconMapping.Crown); public static IconUsage Crown => get(OsuIconMapping.Crown);
public static IconUsage DailyChallenge => get(OsuIconMapping.DailyChallenge);
public static IconUsage Debug => get(OsuIconMapping.Debug); public static IconUsage Debug => get(OsuIconMapping.Debug);
public static IconUsage Delete => get(OsuIconMapping.Delete); public static IconUsage Delete => get(OsuIconMapping.Delete);
public static IconUsage Details => get(OsuIconMapping.Details); public static IconUsage Details => get(OsuIconMapping.Details);
@ -218,6 +219,9 @@ namespace osu.Game.Graphics
[Description(@"crown")] [Description(@"crown")]
Crown, Crown,
[Description(@"daily-challenge")]
DailyChallenge,
[Description(@"debug")] [Description(@"debug")]
Debug, Debug,

View File

@ -31,7 +31,7 @@ namespace osu.Game.Graphics.UserInterface
if (!allowImmediateFocus) if (!allowImmediateFocus)
return; return;
Scheduler.Add(() => GetContainingInputManager().ChangeFocus(this)); Scheduler.Add(() => GetContainingFocusManager().ChangeFocus(this));
} }
public new void KillFocus() => base.KillFocus(); public new void KillFocus() => base.KillFocus();

View File

@ -52,8 +52,6 @@ namespace osu.Game.Graphics.UserInterface
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
const float nub_padding = 5;
Children = new Drawable[] Children = new Drawable[]
{ {
LabelTextFlowContainer = new OsuTextFlowContainer(ApplyLabelParameters) LabelTextFlowContainer = new OsuTextFlowContainer(ApplyLabelParameters)
@ -69,15 +67,13 @@ namespace osu.Game.Graphics.UserInterface
{ {
Nub.Anchor = Anchor.CentreRight; Nub.Anchor = Anchor.CentreRight;
Nub.Origin = Anchor.CentreRight; Nub.Origin = Anchor.CentreRight;
Nub.Margin = new MarginPadding { Right = nub_padding }; LabelTextFlowContainer.Padding = new MarginPadding { Right = Nub.DEFAULT_EXPANDED_SIZE + 10f };
LabelTextFlowContainer.Padding = new MarginPadding { Right = Nub.DEFAULT_EXPANDED_SIZE + nub_padding * 2 };
} }
else else
{ {
Nub.Anchor = Anchor.CentreLeft; Nub.Anchor = Anchor.CentreLeft;
Nub.Origin = Anchor.CentreLeft; Nub.Origin = Anchor.CentreLeft;
Nub.Margin = new MarginPadding { Left = nub_padding }; LabelTextFlowContainer.Padding = new MarginPadding { Left = Nub.DEFAULT_EXPANDED_SIZE + 10f };
LabelTextFlowContainer.Padding = new MarginPadding { Left = Nub.DEFAULT_EXPANDED_SIZE + nub_padding * 2 };
} }
Nub.Current.BindTo(Current); Nub.Current.BindTo(Current);

View File

@ -57,7 +57,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
protected override void OnFocus(FocusEvent e) protected override void OnFocus(FocusEvent e)
{ {
base.OnFocus(e); base.OnFocus(e);
GetContainingInputManager().ChangeFocus(Component); GetContainingFocusManager().ChangeFocus(Component);
} }
protected override OsuTextBox CreateComponent() => CreateTextBox().With(t => protected override OsuTextBox CreateComponent() => CreateTextBox().With(t =>

View File

@ -85,7 +85,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
Current.BindValueChanged(updateTextBoxFromSlider, true); Current.BindValueChanged(updateTextBoxFromSlider, true);
} }
public bool TakeFocus() => GetContainingInputManager().ChangeFocus(textBox); public bool TakeFocus() => GetContainingFocusManager().ChangeFocus(textBox);
private bool updatingFromTextBox; private bool updatingFromTextBox;

View File

@ -182,6 +182,8 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.SelectPreviousRandom), new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.SelectPreviousRandom),
new KeyBinding(InputKey.F3, GlobalAction.ToggleBeatmapOptions), new KeyBinding(InputKey.F3, GlobalAction.ToggleBeatmapOptions),
new KeyBinding(InputKey.BackSpace, GlobalAction.DeselectAllMods), new KeyBinding(InputKey.BackSpace, GlobalAction.DeselectAllMods),
new KeyBinding(new[] { InputKey.Control, InputKey.Up }, GlobalAction.IncreaseModSpeed),
new KeyBinding(new[] { InputKey.Control, InputKey.Down }, GlobalAction.DecreaseModSpeed),
}; };
private static IEnumerable<KeyBinding> audioControlKeyBindings => new[] private static IEnumerable<KeyBinding> audioControlKeyBindings => new[]
@ -420,6 +422,12 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.StepReplayBackward))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.StepReplayBackward))]
StepReplayBackward, StepReplayBackward,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseModSpeed))]
IncreaseModSpeed,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DecreaseModSpeed))]
DecreaseModSpeed,
} }
public enum GlobalActionCategory public enum GlobalActionCategory

View File

@ -1,4 +1,4 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation; using osu.Framework.Localisation;
@ -54,6 +54,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString Exit => new TranslatableString(getKey(@"exit"), @"exit"); public static LocalisableString Exit => new TranslatableString(getKey(@"exit"), @"exit");
/// <summary>
/// "daily challenge"
/// </summary>
public static LocalisableString DailyChallenge => new TranslatableString(getKey(@"daily_challenge"), @"daily challenge");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -369,6 +369,16 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString EditorToggleRotateControl => new TranslatableString(getKey(@"editor_toggle_rotate_control"), @"Toggle rotate control"); public static LocalisableString EditorToggleRotateControl => new TranslatableString(getKey(@"editor_toggle_rotate_control"), @"Toggle rotate control");
/// <summary>
/// "Increase mod speed"
/// </summary>
public static LocalisableString IncreaseModSpeed => new TranslatableString(getKey(@"increase_mod_speed"), @"Increase mod speed");
/// <summary>
/// "Decrease mod speed"
/// </summary>
public static LocalisableString DecreaseModSpeed => new TranslatableString(getKey(@"decrease_mod_speed"), @"Decrease mod speed");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -49,6 +49,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString UrlCopied => new TranslatableString(getKey(@"url_copied"), @"URL copied"); public static LocalisableString UrlCopied => new TranslatableString(getKey(@"url_copied"), @"URL copied");
/// <summary>
/// "Speed changed to {0:N2}x"
/// </summary>
public static LocalisableString SpeedChangedTo(double speed) => new TranslatableString(getKey(@"speed_changed"), @"Speed changed to {0:N2}x", speed);
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -0,0 +1,16 @@
// 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 MessagePack;
namespace osu.Game.Online.Metadata
{
[MessagePackObject]
[Serializable]
public struct DailyChallengeInfo
{
[Key(0)]
public long RoomID { get; set; }
}
}

View File

@ -20,5 +20,11 @@ namespace osu.Game.Online.Metadata
/// Delivers an update of the <see cref="UserPresence"/> of the user with the supplied <paramref name="userId"/>. /// Delivers an update of the <see cref="UserPresence"/> of the user with the supplied <paramref name="userId"/>.
/// </summary> /// </summary>
Task UserPresenceUpdated(int userId, UserPresence? status); Task UserPresenceUpdated(int userId, UserPresence? status);
/// <summary>
/// Delivers an update of the current "daily challenge" status.
/// Null value means there is no "daily challenge" currently active.
/// </summary>
Task DailyChallengeUpdated(DailyChallengeInfo? info);
} }
} }

View File

@ -7,7 +7,12 @@ using osu.Game.Users;
namespace osu.Game.Online.Metadata namespace osu.Game.Online.Metadata
{ {
/// <summary> /// <summary>
/// Metadata server is responsible for keeping the osu! client up-to-date with any changes. /// Metadata server is responsible for keeping the osu! client up-to-date with various real-time happenings, such as:
/// <list type="bullet">
/// <item>beatmap updates via BSS,</item>
/// <item>online user activity/status updates,</item>
/// <item>other real-time happenings, such as current "daily challenge" status.</item>
/// </list>
/// </summary> /// </summary>
public interface IMetadataServer public interface IMetadataServer
{ {

View File

@ -59,6 +59,15 @@ namespace osu.Game.Online.Metadata
#endregion #endregion
#region Daily Challenge
public abstract IBindable<DailyChallengeInfo?> DailyChallengeInfo { get; }
/// <inheritdoc/>
public abstract Task DailyChallengeUpdated(DailyChallengeInfo? info);
#endregion
#region Disconnection handling #region Disconnection handling
public event Action? Disconnecting; public event Action? Disconnecting;

View File

@ -26,6 +26,9 @@ namespace osu.Game.Online.Metadata
public override IBindableDictionary<int, UserPresence> UserStates => userStates; public override IBindableDictionary<int, UserPresence> UserStates => userStates;
private readonly BindableDictionary<int, UserPresence> userStates = new BindableDictionary<int, UserPresence>(); private readonly BindableDictionary<int, UserPresence> userStates = new BindableDictionary<int, UserPresence>();
public override IBindable<DailyChallengeInfo?> DailyChallengeInfo => dailyChallengeInfo;
private readonly Bindable<DailyChallengeInfo?> dailyChallengeInfo = new Bindable<DailyChallengeInfo?>();
private readonly string endpoint; private readonly string endpoint;
private IHubClientConnector? connector; private IHubClientConnector? connector;
@ -58,6 +61,7 @@ namespace osu.Game.Online.Metadata
// https://github.com/dotnet/aspnetcore/issues/15198 // https://github.com/dotnet/aspnetcore/issues/15198
connection.On<BeatmapUpdates>(nameof(IMetadataClient.BeatmapSetsUpdated), ((IMetadataClient)this).BeatmapSetsUpdated); connection.On<BeatmapUpdates>(nameof(IMetadataClient.BeatmapSetsUpdated), ((IMetadataClient)this).BeatmapSetsUpdated);
connection.On<int, UserPresence?>(nameof(IMetadataClient.UserPresenceUpdated), ((IMetadataClient)this).UserPresenceUpdated); connection.On<int, UserPresence?>(nameof(IMetadataClient.UserPresenceUpdated), ((IMetadataClient)this).UserPresenceUpdated);
connection.On<DailyChallengeInfo?>(nameof(IMetadataClient.DailyChallengeUpdated), ((IMetadataClient)this).DailyChallengeUpdated);
connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMetadataClient)this).DisconnectRequested); connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMetadataClient)this).DisconnectRequested);
}; };
@ -101,6 +105,7 @@ namespace osu.Game.Online.Metadata
{ {
isWatchingUserPresence.Value = false; isWatchingUserPresence.Value = false;
userStates.Clear(); userStates.Clear();
dailyChallengeInfo.Value = null;
}); });
return; return;
} }
@ -229,6 +234,12 @@ namespace osu.Game.Online.Metadata
} }
} }
public override Task DailyChallengeUpdated(DailyChallengeInfo? info)
{
Schedule(() => dailyChallengeInfo.Value = info);
return Task.CompletedTask;
}
public override async Task DisconnectRequested() public override async Task DisconnectRequested()
{ {
await base.DisconnectRequested().ConfigureAwait(false); await base.DisconnectRequested().ConfigureAwait(false);

View File

@ -13,5 +13,8 @@ namespace osu.Game.Online.Rooms
[Description("Featured Artist")] [Description("Featured Artist")]
FeaturedArtist, FeaturedArtist,
[Description("Daily Challenge")]
DailyChallenge,
} }
} }

View File

@ -578,17 +578,17 @@ namespace osu.Game
{ {
case ITabletHandler th: case ITabletHandler th:
return new TabletSettings(th); return new TabletSettings(th);
case MouseHandler mh:
return new MouseSettings(mh);
case JoystickHandler jh:
return new JoystickSettings(jh);
} }
} }
switch (handler) switch (handler)
{ {
case MouseHandler mh:
return new MouseSettings(mh);
case JoystickHandler jh:
return new JoystickSettings(jh);
case TouchHandler th: case TouchHandler th:
return new TouchSettings(th); return new TouchSettings(th);

View File

@ -243,7 +243,7 @@ namespace osu.Game.Overlays.AccountCreation
if (nextTextBox != null) if (nextTextBox != null)
{ {
Schedule(() => GetContainingInputManager().ChangeFocus(nextTextBox)); Schedule(() => GetContainingFocusManager().ChangeFocus(nextTextBox));
return true; return true;
} }

View File

@ -16,7 +16,6 @@ using osu.Game.Beatmaps;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
using osuTK; using osuTK;
@ -26,9 +25,9 @@ namespace osu.Game.Overlays.BeatmapSet
{ {
private readonly Statistic length, bpm, circleCount, sliderCount; private readonly Statistic length, bpm, circleCount, sliderCount;
private APIBeatmapSet beatmapSet; private IBeatmapSetInfo beatmapSet;
public APIBeatmapSet BeatmapSet public IBeatmapSetInfo BeatmapSet
{ {
get => beatmapSet; get => beatmapSet;
set set

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
@ -28,9 +29,9 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
[CanBeNull] [CanBeNull]
public PreviewTrack Preview { get; private set; } public PreviewTrack Preview { get; private set; }
private APIBeatmapSet beatmapSet; private IBeatmapSetInfo beatmapSet;
public APIBeatmapSet BeatmapSet public IBeatmapSetInfo BeatmapSet
{ {
get => beatmapSet; get => beatmapSet;
set set

View File

@ -8,9 +8,9 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osuTK; using osuTK;
namespace osu.Game.Overlays.BeatmapSet.Buttons namespace osu.Game.Overlays.BeatmapSet.Buttons
@ -24,7 +24,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
public IBindable<bool> Playing => playButton.Playing; public IBindable<bool> Playing => playButton.Playing;
public APIBeatmapSet BeatmapSet public IBeatmapSetInfo BeatmapSet
{ {
get => playButton.BeatmapSet; get => playButton.BeatmapSet;
set => playButton.BeatmapSet = value; set => playButton.BeatmapSet = value;
@ -32,8 +32,6 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
public PreviewButton() public PreviewButton()
{ {
Height = 42;
Children = new Drawable[] Children = new Drawable[]
{ {
background = new Box background = new Box

View File

@ -68,6 +68,7 @@ namespace osu.Game.Overlays.BeatmapSet
preview = new PreviewButton preview = new PreviewButton
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = 42,
}, },
new DetailBox new DetailBox
{ {

View File

@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Comments
base.LoadComplete(); base.LoadComplete();
if (!TextBox.ReadOnly) if (!TextBox.ReadOnly)
GetContainingInputManager().ChangeFocus(TextBox); GetContainingFocusManager().ChangeFocus(TextBox);
} }
protected override void OnCommit(string text) protected override void OnCommit(string text)

View File

@ -150,7 +150,7 @@ namespace osu.Game.Overlays.Login
protected override void OnFocus(FocusEvent e) protected override void OnFocus(FocusEvent e)
{ {
Schedule(() => { GetContainingInputManager().ChangeFocus(string.IsNullOrEmpty(username.Text) ? username : password); }); Schedule(() => { GetContainingFocusManager().ChangeFocus(string.IsNullOrEmpty(username.Text) ? username : password); });
} }
} }
} }

View File

@ -186,7 +186,7 @@ namespace osu.Game.Overlays.Login
} }
if (form != null) if (form != null)
ScheduleAfterChildren(() => GetContainingInputManager()?.ChangeFocus(form)); ScheduleAfterChildren(() => GetContainingFocusManager()?.ChangeFocus(form));
}); });
private void updateDropdownCurrent(UserStatus? status) private void updateDropdownCurrent(UserStatus? status)
@ -216,7 +216,7 @@ namespace osu.Game.Overlays.Login
protected override void OnFocus(FocusEvent e) protected override void OnFocus(FocusEvent e)
{ {
if (form != null) GetContainingInputManager().ChangeFocus(form); if (form != null) GetContainingFocusManager().ChangeFocus(form);
base.OnFocus(e); base.OnFocus(e);
} }
} }

View File

@ -141,7 +141,7 @@ namespace osu.Game.Overlays.Login
protected override void OnFocus(FocusEvent e) protected override void OnFocus(FocusEvent e)
{ {
Schedule(() => { GetContainingInputManager().ChangeFocus(codeTextBox); }); Schedule(() => { GetContainingFocusManager().ChangeFocus(codeTextBox); });
} }
} }
} }

View File

@ -78,7 +78,7 @@ namespace osu.Game.Overlays
this.FadeIn(transition_time, Easing.OutQuint); this.FadeIn(transition_time, Easing.OutQuint);
FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out); FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out);
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(panel)); ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(panel));
} }
protected override void PopOut() protected override void PopOut()

View File

@ -89,7 +89,7 @@ namespace osu.Game.Overlays.Mods
{ {
base.LoadComplete(); base.LoadComplete();
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox)); ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(nameTextBox));
nameTextBox.Current.BindValueChanged(s => nameTextBox.Current.BindValueChanged(s =>
{ {

View File

@ -136,7 +136,7 @@ namespace osu.Game.Overlays.Mods
{ {
base.LoadComplete(); base.LoadComplete();
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox)); ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(nameTextBox));
} }
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)

View File

@ -186,7 +186,7 @@ namespace osu.Game.Overlays.Mods
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
Height = 0 Height = 0
} },
}); });
MainAreaContent.AddRange(new Drawable[] MainAreaContent.AddRange(new Drawable[]
@ -949,7 +949,7 @@ namespace osu.Game.Overlays.Mods
RequestScroll?.Invoke(this); RequestScroll?.Invoke(this);
// Killing focus is done here because it's the only feasible place on ModSelectOverlay you can click on without triggering any action. // Killing focus is done here because it's the only feasible place on ModSelectOverlay you can click on without triggering any action.
Scheduler.Add(() => GetContainingInputManager().ChangeFocus(null)); Scheduler.Add(() => GetContainingFocusManager().ChangeFocus(null));
return true; return true;
} }

View File

@ -3,18 +3,30 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Input.Events;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select;
using osu.Game.Utils; using osu.Game.Utils;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
public partial class UserModSelectOverlay : ModSelectOverlay public partial class UserModSelectOverlay : ModSelectOverlay
{ {
private ModSpeedHotkeyHandler modSpeedHotkeyHandler = null!;
public UserModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green) public UserModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green)
: base(colourScheme) : base(colourScheme)
{ {
} }
[BackgroundDependencyLoader]
private void load()
{
Add(modSpeedHotkeyHandler = new ModSpeedHotkeyHandler());
}
protected override ModColumn CreateModColumn(ModType modType) => new UserModColumn(modType, false); protected override ModColumn CreateModColumn(ModType modType) => new UserModColumn(modType, false);
protected override IReadOnlyList<Mod> ComputeNewModsFromSelection(IReadOnlyList<Mod> oldSelection, IReadOnlyList<Mod> newSelection) protected override IReadOnlyList<Mod> ComputeNewModsFromSelection(IReadOnlyList<Mod> oldSelection, IReadOnlyList<Mod> newSelection)
@ -38,6 +50,20 @@ namespace osu.Game.Overlays.Mods
return modsAfterRemoval.ToList(); return modsAfterRemoval.ToList();
} }
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
switch (e.Action)
{
case GlobalAction.IncreaseModSpeed:
return modSpeedHotkeyHandler.ChangeSpeed(0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod));
case GlobalAction.DecreaseModSpeed:
return modSpeedHotkeyHandler.ChangeSpeed(-0.05, AllAvailableMods.Where(state => state.ValidForSelection.Value).Select(state => state.Mod));
}
return base.OnPressed(e);
}
private partial class UserModColumn : ModColumn private partial class UserModColumn : ModColumn
{ {
public UserModColumn(ModType modType, bool allowIncompatibleSelection) public UserModColumn(ModType modType, bool allowIncompatibleSelection)

View File

@ -0,0 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Configuration;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
namespace osu.Game.Overlays.OSD
{
public partial class SpeedChangeToast : Toast
{
public SpeedChangeToast(OsuConfigManager config, double newSpeed)
: base(ModSelectOverlayStrings.ModCustomisation, ToastStrings.SpeedChangedTo(newSpeed), config.LookupKeyBindings(GlobalAction.IncreaseModSpeed) + " / " + config.LookupKeyBindings(GlobalAction.DecreaseModSpeed))
{
}
}
}

View File

@ -465,7 +465,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
} }
if (HasFocus) if (HasFocus)
GetContainingInputManager().ChangeFocus(null); GetContainingFocusManager().ChangeFocus(null);
cancelAndClearButtons.FadeOut(300, Easing.OutQuint); cancelAndClearButtons.FadeOut(300, Easing.OutQuint);
cancelAndClearButtons.BypassAutoSizeAxes |= Axes.Y; cancelAndClearButtons.BypassAutoSizeAxes |= Axes.Y;

View File

@ -106,7 +106,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{ {
var next = Children.SkipWhile(c => c != sender).Skip(1).FirstOrDefault(); var next = Children.SkipWhile(c => c != sender).Skip(1).FirstOrDefault();
if (next != null) if (next != null)
GetContainingInputManager().ChangeFocus(next); GetContainingFocusManager().ChangeFocus(next);
} }
} }
} }

View File

@ -105,12 +105,17 @@ namespace osu.Game.Overlays.Settings.Sections.Input
highPrecisionMouse.Current.BindValueChanged(highPrecision => highPrecisionMouse.Current.BindValueChanged(highPrecision =>
{ {
if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows) switch (RuntimeInfo.OS)
{ {
if (highPrecision.NewValue) case RuntimeInfo.Platform.Linux:
highPrecisionMouse.SetNoticeText(MouseSettingsStrings.HighPrecisionPlatformWarning, true); case RuntimeInfo.Platform.macOS:
else case RuntimeInfo.Platform.iOS:
highPrecisionMouse.ClearNoticeText(); if (highPrecision.NewValue)
highPrecisionMouse.SetNoticeText(MouseSettingsStrings.HighPrecisionPlatformWarning, true);
else
highPrecisionMouse.ClearNoticeText();
break;
} }
}, true); }, true);
} }

View File

@ -201,7 +201,7 @@ namespace osu.Game.Overlays
searchTextBox.HoldFocus = false; searchTextBox.HoldFocus = false;
if (searchTextBox.HasFocus) if (searchTextBox.HasFocus)
GetContainingInputManager().ChangeFocus(null); GetContainingFocusManager().ChangeFocus(null);
} }
public override bool AcceptsFocus => true; public override bool AcceptsFocus => true;

View File

@ -5,12 +5,11 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Utils;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
@ -33,148 +32,16 @@ namespace osu.Game.Overlays.SkinEditor
UpdatePosition = updateDrawablePosition UpdatePosition = updateDrawablePosition
}; };
private bool allSelectedSupportManualSizing(Axes axis) => SelectedItems.All(b => (b as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(axis) == false); public override SelectionScaleHandler CreateScaleHandler()
public override bool HandleScale(Vector2 scale, Anchor anchor)
{ {
Axes adjustAxis; var scaleHandler = new SkinSelectionScaleHandler
switch (anchor)
{ {
// for corners, adjust scale. UpdatePosition = updateDrawablePosition
case Anchor.TopLeft: };
case Anchor.TopRight:
case Anchor.BottomLeft:
case Anchor.BottomRight:
adjustAxis = Axes.Both;
break;
// for edges, adjust size. scaleHandler.PerformFlipFromScaleHandles += a => SelectionBox.PerformFlipFromScaleHandles(a);
// autosize elements can't be easily handled so just disable sizing for now.
case Anchor.TopCentre:
case Anchor.BottomCentre:
if (!allSelectedSupportManualSizing(Axes.Y))
return false;
adjustAxis = Axes.Y; return scaleHandler;
break;
case Anchor.CentreLeft:
case Anchor.CentreRight:
if (!allSelectedSupportManualSizing(Axes.X))
return false;
adjustAxis = Axes.X;
break;
default:
throw new ArgumentOutOfRangeException(nameof(anchor), anchor, null);
}
// convert scale to screen space
scale = ToScreenSpace(scale) - ToScreenSpace(Vector2.Zero);
adjustScaleFromAnchor(ref scale, anchor);
// the selection quad is always upright, so use an AABB rect to make mutating the values easier.
var selectionRect = getSelectionQuad().AABBFloat;
// If the selection has no area we cannot scale it
if (selectionRect.Area == 0)
return false;
// copy to mutate, as we will need to compare to the original later on.
var adjustedRect = selectionRect;
bool isRotated = false;
// for now aspect lock scale adjustments that occur at corners..
if (!anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1))
{
// project scale vector along diagonal
Vector2 diag = (selectionRect.TopLeft - selectionRect.BottomRight).Normalized();
scale = Vector2.Dot(scale, diag) * diag;
}
// ..or if any of the selection have been rotated.
// this is to avoid requiring skew logic (which would likely not be the user's expected transform anyway).
else if (SelectedBlueprints.Any(b => !Precision.AlmostEquals(((Drawable)b.Item).Rotation % 90, 0)))
{
isRotated = true;
if (anchor.HasFlagFast(Anchor.x1))
// if dragging from the horizontal centre, only a vertical component is available.
scale.X = scale.Y / selectionRect.Height * selectionRect.Width;
else
// in all other cases (arbitrarily) use the horizontal component for aspect lock.
scale.Y = scale.X / selectionRect.Width * selectionRect.Height;
}
if (anchor.HasFlagFast(Anchor.x0)) adjustedRect.X -= scale.X;
if (anchor.HasFlagFast(Anchor.y0)) adjustedRect.Y -= scale.Y;
// Maintain the selection's centre position if dragging from the centre anchors and selection is rotated.
if (isRotated && anchor.HasFlagFast(Anchor.x1)) adjustedRect.X -= scale.X / 2;
if (isRotated && anchor.HasFlagFast(Anchor.y1)) adjustedRect.Y -= scale.Y / 2;
adjustedRect.Width += scale.X;
adjustedRect.Height += scale.Y;
if (adjustedRect.Width <= 0 || adjustedRect.Height <= 0)
{
Axes toFlip = Axes.None;
if (adjustedRect.Width <= 0) toFlip |= Axes.X;
if (adjustedRect.Height <= 0) toFlip |= Axes.Y;
SelectionBox.PerformFlipFromScaleHandles(toFlip);
return true;
}
// scale adjust applied to each individual item should match that of the quad itself.
var scaledDelta = new Vector2(
adjustedRect.Width / selectionRect.Width,
adjustedRect.Height / selectionRect.Height
);
foreach (var b in SelectedBlueprints)
{
var drawableItem = (Drawable)b.Item;
// each drawable's relative position should be maintained in the scaled quad.
var screenPosition = drawableItem.ToScreenSpace(drawableItem.OriginPosition);
var relativePositionInOriginal =
new Vector2(
(screenPosition.X - selectionRect.TopLeft.X) / selectionRect.Width,
(screenPosition.Y - selectionRect.TopLeft.Y) / selectionRect.Height
);
var newPositionInAdjusted = new Vector2(
adjustedRect.TopLeft.X + adjustedRect.Width * relativePositionInOriginal.X,
adjustedRect.TopLeft.Y + adjustedRect.Height * relativePositionInOriginal.Y
);
updateDrawablePosition(drawableItem, newPositionInAdjusted);
var currentScaledDelta = scaledDelta;
if (Precision.AlmostEquals(MathF.Abs(drawableItem.Rotation) % 180, 90))
currentScaledDelta = new Vector2(scaledDelta.Y, scaledDelta.X);
switch (adjustAxis)
{
case Axes.X:
drawableItem.Width *= currentScaledDelta.X;
break;
case Axes.Y:
drawableItem.Height *= currentScaledDelta.Y;
break;
case Axes.Both:
drawableItem.Scale *= currentScaledDelta;
break;
}
}
return true;
} }
public override bool HandleFlip(Direction direction, bool flipOverOrigin) public override bool HandleFlip(Direction direction, bool flipOverOrigin)
@ -225,9 +92,6 @@ namespace osu.Game.Overlays.SkinEditor
{ {
base.OnSelectionChanged(); base.OnSelectionChanged();
SelectionBox.CanScaleX = allSelectedSupportManualSizing(Axes.X);
SelectionBox.CanScaleY = allSelectedSupportManualSizing(Axes.Y);
SelectionBox.CanScaleDiagonally = true;
SelectionBox.CanFlipX = true; SelectionBox.CanFlipX = true;
SelectionBox.CanFlipY = true; SelectionBox.CanFlipY = true;
SelectionBox.CanReverse = false; SelectionBox.CanReverse = false;
@ -421,24 +285,43 @@ namespace osu.Game.Overlays.SkinEditor
drawable.Position -= drawable.AnchorPosition - previousAnchor; drawable.Position -= drawable.AnchorPosition - previousAnchor;
} }
private static void applyOrigin(Drawable drawable, Anchor origin) private static void applyOrigin(Drawable drawable, Anchor screenSpaceOrigin)
{ {
if (origin == drawable.Origin) return; var boundingBox = drawable.ScreenSpaceDrawQuad.AABBFloat;
var previousOrigin = drawable.OriginPosition; var targetScreenSpacePosition = screenSpaceOrigin.PositionOnQuad(boundingBox);
drawable.Origin = origin;
drawable.Position += drawable.OriginPosition - previousOrigin;
}
private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference) Anchor localOrigin = Anchor.TopLeft;
{ float smallestDistanceFromTargetPosition = float.PositiveInfinity;
// cancel out scale in axes we don't care about (based on which drag handle was used).
if ((reference & Anchor.x1) > 0) scale.X = 0;
if ((reference & Anchor.y1) > 0) scale.Y = 0;
// reverse the scale direction if dragging from top or left. void checkOrigin(Anchor originToTest)
if ((reference & Anchor.x0) > 0) scale.X = -scale.X; {
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y; Vector2 positionToTest = drawable.ToScreenSpace(originToTest.PositionOnQuad(drawable.DrawRectangle));
float testedDistance = Vector2.Distance(targetScreenSpacePosition, positionToTest);
if (testedDistance < smallestDistanceFromTargetPosition)
{
localOrigin = originToTest;
smallestDistanceFromTargetPosition = testedDistance;
}
}
checkOrigin(Anchor.TopLeft);
checkOrigin(Anchor.TopCentre);
checkOrigin(Anchor.TopRight);
checkOrigin(Anchor.CentreLeft);
checkOrigin(Anchor.Centre);
checkOrigin(Anchor.CentreRight);
checkOrigin(Anchor.BottomLeft);
checkOrigin(Anchor.BottomCentre);
checkOrigin(Anchor.BottomRight);
Vector2 offset = drawable.ToParentSpace(localOrigin.PositionOnQuad(drawable.DrawRectangle)) - drawable.ToParentSpace(drawable.Origin.PositionOnQuad(drawable.DrawRectangle));
drawable.Origin = localOrigin;
drawable.Position += offset;
} }
} }
} }

View File

@ -0,0 +1,178 @@
// 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.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
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 SkinSelectionScaleHandler : SelectionScaleHandler
{
public Action<Drawable, Vector2> UpdatePosition { get; init; } = null!;
public event Action<Axes>? PerformFlipFromScaleHandles;
[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()
{
CanScaleX.Value = allSelectedSupportManualSizing(Axes.X);
CanScaleY.Value = allSelectedSupportManualSizing(Axes.Y);
CanScaleDiagonally.Value = true;
}
private bool allSelectedSupportManualSizing(Axes axis) => selectedItems.All(b => (b as CompositeDrawable)?.AutoSizeAxes.HasFlagFast(axis) == false);
private Dictionary<Drawable, OriginalDrawableState>? objectsInScale;
private Vector2? defaultOrigin;
private bool isFlippedX;
private bool isFlippedY;
public override void Begin()
{
if (objectsInScale != null)
throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!");
changeHandler?.BeginChange();
objectsInScale = selectedItems.Cast<Drawable>().ToDictionary(d => d, d => new OriginalDrawableState(d));
OriginalSurroundingQuad = ToLocalSpace(GeometryUtils.GetSurroundingQuad(objectsInScale.SelectMany(d => d.Key.ScreenSpaceDrawQuad.GetVertices().ToArray())));
defaultOrigin = OriginalSurroundingQuad.Value.Centre;
isFlippedX = false;
isFlippedY = false;
}
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
{
if (objectsInScale == null)
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null);
var actualOrigin = ToScreenSpace(origin ?? defaultOrigin.Value);
if ((adjustAxis == Axes.Y && !allSelectedSupportManualSizing(Axes.Y)) ||
(adjustAxis == Axes.X && !allSelectedSupportManualSizing(Axes.X)))
return;
// If the selection has no area we cannot scale it
if (OriginalSurroundingQuad.Value.Width == 0 || OriginalSurroundingQuad.Value.Height == 0)
return;
// for now aspect lock scale adjustments that occur at corners.
if (adjustAxis == Axes.Both)
{
// project scale vector along diagonal
scale = new Vector2((scale.X + scale.Y) * 0.5f);
}
// If any of the selection have been rotated and the adjust axis is not both,
// we would require skew logic to achieve a correct image editor-like scale.
// For now we just ignore, because it would likely not be the user's expected transform anyway.
bool flippedX = scale.X < 0;
bool flippedY = scale.Y < 0;
Axes toFlip = Axes.None;
if (flippedX != isFlippedX)
{
isFlippedX = flippedX;
toFlip |= Axes.X;
}
if (flippedY != isFlippedY)
{
isFlippedY = flippedY;
toFlip |= Axes.Y;
}
if (toFlip != Axes.None)
{
PerformFlipFromScaleHandles?.Invoke(toFlip);
return;
}
foreach (var (b, originalState) in objectsInScale)
{
UpdatePosition(b, GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.ScreenSpaceOriginPosition));
var currentScale = scale;
if (Precision.AlmostEquals(MathF.Abs(b.Rotation) % 180, 90))
currentScale = new Vector2(scale.Y, scale.X);
switch (adjustAxis)
{
case Axes.X:
b.Width = MathF.Abs(originalState.Width * currentScale.X);
break;
case Axes.Y:
b.Height = MathF.Abs(originalState.Height * currentScale.Y);
break;
case Axes.Both:
b.Scale = originalState.Scale * currentScale;
break;
}
}
}
public override void Commit()
{
if (objectsInScale == null)
throw new InvalidOperationException($"Cannot {nameof(Commit)} a scale operation without calling {nameof(Begin)} first!");
changeHandler?.EndChange();
objectsInScale = null;
defaultOrigin = null;
}
private struct OriginalDrawableState
{
public float Width { get; }
public float Height { get; }
public Vector2 Scale { get; }
public Vector2 ScreenSpaceOriginPosition { get; }
public OriginalDrawableState(Drawable drawable)
{
Width = drawable.Width;
Height = drawable.Height;
Scale = drawable.Scale;
ScreenSpaceOriginPosition = drawable.ToScreenSpace(drawable.OriginPosition);
}
}
}
}

View File

@ -36,6 +36,8 @@ namespace osu.Game.Rulesets.Mods
{ {
overlay.ShowHud.Value = false; overlay.ShowHud.Value = false;
overlay.ShowHud.Disabled = true; overlay.ShowHud.Disabled = true;
overlay.PlayfieldSkinLayer.Hide();
} }
public void ApplyToPlayer(Player player) public void ApplyToPlayer(Player player)

View File

@ -88,7 +88,7 @@ namespace osu.Game.Scoring
{ {
ScoreInfo? databasedScoreInfo = null; ScoreInfo? databasedScoreInfo = null;
if (originalScoreInfo is ScoreInfo scoreInfo) if (originalScoreInfo is ScoreInfo scoreInfo && !string.IsNullOrEmpty(scoreInfo.Hash))
databasedScoreInfo = Query(s => s.Hash == scoreInfo.Hash); databasedScoreInfo = Query(s => s.Hash == scoreInfo.Hash);
if (originalScoreInfo.OnlineID > 0) if (originalScoreInfo.OnlineID > 0)

View File

@ -580,7 +580,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
base.LoadComplete(); base.LoadComplete();
GetContainingInputManager().ChangeFocus(this); GetContainingFocusManager().ChangeFocus(this);
SelectAll(); SelectAll();
} }
} }

View File

@ -27,7 +27,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
[Resolved] [Resolved]
private SelectionRotationHandler? rotationHandler { get; set; } private SelectionRotationHandler? rotationHandler { get; set; }
public Func<Vector2, Anchor, bool>? OnScale; [Resolved]
private SelectionScaleHandler? scaleHandler { get; set; }
public Func<Direction, bool, bool>? OnFlip; public Func<Direction, bool, bool>? OnFlip;
public Func<bool>? OnReverse; public Func<bool>? OnReverse;
@ -57,60 +59,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
private readonly IBindable<bool> canRotate = new BindableBool(); private readonly IBindable<bool> canRotate = new BindableBool();
private bool canScaleX; private readonly IBindable<bool> canScaleX = new BindableBool();
/// <summary> private readonly IBindable<bool> canScaleY = new BindableBool();
/// Whether horizontal scaling (from the left or right edge) support should be enabled.
/// </summary>
public bool CanScaleX
{
get => canScaleX;
set
{
if (canScaleX == value) return;
canScaleX = value; private readonly IBindable<bool> canScaleDiagonally = new BindableBool();
recreate();
}
}
private bool canScaleY;
/// <summary>
/// Whether vertical scaling (from the top or bottom edge) support should be enabled.
/// </summary>
public bool CanScaleY
{
get => canScaleY;
set
{
if (canScaleY == value) return;
canScaleY = value;
recreate();
}
}
private bool canScaleDiagonally;
/// <summary>
/// Whether diagonal scaling (from a corner) support should be enabled.
/// </summary>
/// <remarks>
/// There are some cases where we only want to allow proportional resizing, and not allow
/// one or both explicit directions of scale.
/// </remarks>
public bool CanScaleDiagonally
{
get => canScaleDiagonally;
set
{
if (canScaleDiagonally == value) return;
canScaleDiagonally = value;
recreate();
}
}
private bool canFlipX; private bool canFlipX;
@ -176,7 +129,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (rotationHandler != null) if (rotationHandler != null)
canRotate.BindTo(rotationHandler.CanRotateSelectionOrigin); canRotate.BindTo(rotationHandler.CanRotateSelectionOrigin);
canRotate.BindValueChanged(_ => recreate(), true); if (scaleHandler != null)
{
canScaleX.BindTo(scaleHandler.CanScaleX);
canScaleY.BindTo(scaleHandler.CanScaleY);
canScaleDiagonally.BindTo(scaleHandler.CanScaleDiagonally);
}
canRotate.BindValueChanged(_ => recreate());
canScaleX.BindValueChanged(_ => recreate());
canScaleY.BindValueChanged(_ => recreate());
canScaleDiagonally.BindValueChanged(_ => recreate(), true);
} }
protected override bool OnKeyDown(KeyDownEvent e) protected override bool OnKeyDown(KeyDownEvent e)
@ -265,9 +228,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
}; };
if (CanScaleX) addXScaleComponents(); if (canScaleX.Value) addXScaleComponents();
if (CanScaleDiagonally) addFullScaleComponents(); if (canScaleDiagonally.Value) addFullScaleComponents();
if (CanScaleY) addYScaleComponents(); if (canScaleY.Value) addYScaleComponents();
if (CanFlipX) addXFlipComponents(); if (CanFlipX) addXFlipComponents();
if (CanFlipY) addYFlipComponents(); if (CanFlipY) addYFlipComponents();
if (canRotate.Value) addRotationComponents(); if (canRotate.Value) addRotationComponents();
@ -353,7 +316,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
var handle = new SelectionBoxScaleHandle var handle = new SelectionBoxScaleHandle
{ {
Anchor = anchor, Anchor = anchor,
HandleScale = (delta, a) => OnScale?.Invoke(delta, a)
}; };
handle.OperationStarted += operationStarted; handle.OperationStarted += operationStarted;

View File

@ -1,19 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Screens.Edit.Compose.Components namespace osu.Game.Screens.Edit.Compose.Components
{ {
public partial class SelectionBoxScaleHandle : SelectionBoxDragHandle public partial class SelectionBoxScaleHandle : SelectionBoxDragHandle
{ {
public Action<Vector2, Anchor> HandleScale { get; set; } [Resolved]
private SelectionScaleHandler? scaleHandler { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
@ -21,10 +23,108 @@ namespace osu.Game.Screens.Edit.Compose.Components
Size = new Vector2(10); Size = new Vector2(10);
} }
private Anchor originalAnchor;
protected override bool OnDragStart(DragStartEvent e)
{
if (e.Button != MouseButton.Left)
return false;
if (scaleHandler == null) return false;
originalAnchor = Anchor;
scaleHandler.Begin();
return true;
}
private Vector2 rawScale;
protected override void OnDrag(DragEvent e) protected override void OnDrag(DragEvent e)
{ {
HandleScale?.Invoke(e.Delta, Anchor);
base.OnDrag(e); base.OnDrag(e);
if (scaleHandler == null) return;
rawScale = convertDragEventToScaleMultiplier(e);
applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed);
} }
protected override bool OnKeyDown(KeyDownEvent e)
{
if (IsDragged)
{
applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed);
return true;
}
return base.OnKeyDown(e);
}
protected override void OnKeyUp(KeyUpEvent e)
{
base.OnKeyUp(e);
if (IsDragged)
applyScale(shouldLockAspectRatio: isCornerAnchor(originalAnchor) && e.ShiftPressed);
}
protected override void OnDragEnd(DragEndEvent e)
{
scaleHandler?.Commit();
}
private Vector2 convertDragEventToScaleMultiplier(DragEvent e)
{
Vector2 scale = e.MousePosition - e.MouseDownPosition;
adjustScaleFromAnchor(ref scale);
var surroundingQuad = scaleHandler!.OriginalSurroundingQuad!.Value;
scale.X = Precision.AlmostEquals(surroundingQuad.Width, 0) ? 0 : scale.X / surroundingQuad.Width;
scale.Y = Precision.AlmostEquals(surroundingQuad.Height, 0) ? 0 : scale.Y / surroundingQuad.Height;
return scale + Vector2.One;
}
private void adjustScaleFromAnchor(ref Vector2 scale)
{
// cancel out scale in axes we don't care about (based on which drag handle was used).
if ((originalAnchor & Anchor.x1) > 0) scale.X = 0;
if ((originalAnchor & Anchor.y1) > 0) scale.Y = 0;
// reverse the scale direction if dragging from top or left.
if ((originalAnchor & Anchor.x0) > 0) scale.X = -scale.X;
if ((originalAnchor & Anchor.y0) > 0) scale.Y = -scale.Y;
}
private void applyScale(bool shouldLockAspectRatio)
{
var newScale = shouldLockAspectRatio
? new Vector2((rawScale.X + rawScale.Y) * 0.5f)
: rawScale;
var scaleOrigin = originalAnchor.Opposite().PositionOnQuad(scaleHandler!.OriginalSurroundingQuad!.Value);
scaleHandler!.Update(newScale, scaleOrigin, getAdjustAxis());
}
private Axes getAdjustAxis()
{
switch (originalAnchor)
{
case Anchor.TopCentre:
case Anchor.BottomCentre:
return Axes.Y;
case Anchor.CentreLeft:
case Anchor.CentreRight:
return Axes.X;
default:
return Axes.Both;
}
}
private bool isCornerAnchor(Anchor anchor) => !anchor.HasFlagFast(Anchor.x1) && !anchor.HasFlagFast(Anchor.y1);
} }
} }

View File

@ -57,6 +57,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
public SelectionRotationHandler RotationHandler { get; private set; } public SelectionRotationHandler RotationHandler { get; private set; }
public SelectionScaleHandler ScaleHandler { get; private set; }
protected SelectionHandler() protected SelectionHandler()
{ {
selectedBlueprints = new List<SelectionBlueprint<T>>(); selectedBlueprints = new List<SelectionBlueprint<T>>();
@ -69,6 +71,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.CacheAs(RotationHandler = CreateRotationHandler()); dependencies.CacheAs(RotationHandler = CreateRotationHandler());
dependencies.CacheAs(ScaleHandler = CreateScaleHandler());
return dependencies; return dependencies;
} }
@ -78,6 +81,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
{ {
RotationHandler, RotationHandler,
ScaleHandler,
SelectionBox = CreateSelectionBox(), SelectionBox = CreateSelectionBox(),
}); });
@ -93,7 +97,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
OperationStarted = OnOperationBegan, OperationStarted = OnOperationBegan,
OperationEnded = OnOperationEnded, OperationEnded = OnOperationEnded,
OnScale = HandleScale,
OnFlip = HandleFlip, OnFlip = HandleFlip,
OnReverse = HandleReverse, OnReverse = HandleReverse,
}; };
@ -157,6 +160,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <returns>Whether any items could be scaled.</returns> /// <returns>Whether any items could be scaled.</returns>
public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false; public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false;
/// <summary>
/// Creates the handler to use for scale operations.
/// </summary>
public virtual SelectionScaleHandler CreateScaleHandler() => new SelectionScaleHandler();
/// <summary> /// <summary>
/// Handles the selected items being flipped. /// Handles the selected items being flipped.
/// </summary> /// </summary>

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 osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
{
/// <summary>
/// Base handler for editor scale operations.
/// </summary>
public partial class SelectionScaleHandler : Component
{
/// <summary>
/// Whether horizontal scaling (from the left or right edge) support should be enabled.
/// </summary>
public Bindable<bool> CanScaleX { get; private set; } = new BindableBool();
/// <summary>
/// Whether vertical scaling (from the top or bottom edge) support should be enabled.
/// </summary>
public Bindable<bool> CanScaleY { get; private set; } = new BindableBool();
/// <summary>
/// Whether diagonal scaling (from a corner) support should be enabled.
/// </summary>
/// <remarks>
/// There are some cases where we only want to allow proportional resizing, and not allow
/// one or both explicit directions of scale.
/// </remarks>
public Bindable<bool> CanScaleDiagonally { get; private set; } = new BindableBool();
public Quad? OriginalSurroundingQuad { get; protected set; }
/// <summary>
/// Performs a single, instant, atomic scale 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="scale">The scale to apply, as multiplier.</param>
/// <param name="origin">
/// The origin point to scale from.
/// If the default <see langword="null"/> value is supplied, a sane implementation-defined default will be used.
/// </param>
/// <param name="adjustAxis">The axes to adjust the scale in.</param>
public void ScaleSelection(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
{
Begin();
Update(scale, origin, adjustAxis);
Commit();
}
/// <summary>
/// Begins a continuous scale operation.
/// </summary>
/// <remarks>
/// This flow is intended to be used when a scale operation is made incrementally (such as when dragging a scale handle or slider).
/// For instantaneous, atomic operations, use the convenience <see cref="ScaleSelection"/> method.
/// </remarks>
public virtual void Begin()
{
}
/// <summary>
/// Updates a continuous scale operation.
/// Must be preceded by a <see cref="Begin"/> call.
/// </summary>
/// <remarks>
/// <para>
/// This flow is intended to be used when a scale operation is made incrementally (such as when dragging a scale handle or slider).
/// As such, the values of <paramref name="scale"/> and <paramref name="origin"/> supplied should be relative to the state of the objects being scaled
/// when <see cref="Begin"/> was called, rather than instantaneous deltas.
/// </para>
/// <para>
/// For instantaneous, atomic operations, use the convenience <see cref="ScaleSelection"/> method.
/// </para>
/// </remarks>
/// <param name="scale">The Scale to apply, as multiplier.</param>
/// <param name="origin">
/// The origin point to scale from.
/// If the default <see langword="null"/> value is supplied, a sane implementation-defined default will be used.
/// </param>
/// <param name="adjustAxis">The axes to adjust the scale in.</param>
public virtual void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
{
}
/// <summary>
/// Ends a continuous scale operation.
/// Must be preceded by a <see cref="Begin"/> call.
/// </summary>
/// <remarks>
/// This flow is intended to be used when a scale operation is made incrementally (such as when dragging a scale handle or slider).
/// For instantaneous, atomic operations, use the convenience <see cref="ScaleSelection"/> method.
/// </remarks>
public virtual void Commit()
{
}
}
}

View File

@ -138,7 +138,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(sliderVelocitySlider)); ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(sliderVelocitySlider));
} }
} }
} }

View File

@ -142,7 +142,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(volume)); ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(volume));
} }
private static string? getCommonBank(IList<HitSampleInfo>[] relevantSamples) => relevantSamples.Select(GetBankValue).Distinct().Count() == 1 ? GetBankValue(relevantSamples.First()) : null; private static string? getCommonBank(IList<HitSampleInfo>[] relevantSamples) => relevantSamples.Select(GetBankValue).Distinct().Count() == 1 ? GetBankValue(relevantSamples.First()) : null;

View File

@ -719,6 +719,8 @@ namespace osu.Game.Screens.Edit
public override bool OnExiting(ScreenExitEvent e) public override bool OnExiting(ScreenExitEvent e)
{ {
currentScreen?.OnExiting(e);
if (!ExitConfirmed) if (!ExitConfirmed)
{ {
// dialog overlay may not be available in visual tests. // dialog overlay may not be available in visual tests.

View File

@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Screens;
namespace osu.Game.Screens.Edit namespace osu.Game.Screens.Edit
{ {
@ -37,6 +38,10 @@ namespace osu.Game.Screens.Edit
protected override void PopOut() => this.FadeOut(); protected override void PopOut() => this.FadeOut();
public virtual void OnExiting(ScreenExitEvent e)
{
}
#region Clipboard operations #region Clipboard operations
public BindableBool CanCut { get; } = new BindableBool(); public BindableBool CanCut { get; } = new BindableBool();

View File

@ -45,7 +45,7 @@ namespace osu.Game.Screens.Edit.Setup
OnFocused?.Invoke(); OnFocused?.Invoke();
base.OnFocus(e); base.OnFocus(e);
GetContainingInputManager().TriggerFocusContention(this); GetContainingFocusManager().TriggerFocusContention(this);
} }
} }
} }

View File

@ -53,9 +53,6 @@ namespace osu.Game.Screens.Edit.Setup
sourceTextBox = createTextBox<LabelledTextBox>(BeatmapsetsStrings.ShowInfoSource, metadata.Source), sourceTextBox = createTextBox<LabelledTextBox>(BeatmapsetsStrings.ShowInfoSource, metadata.Source),
tagsTextBox = createTextBox<LabelledTextBox>(BeatmapsetsStrings.ShowInfoTags, metadata.Tags) tagsTextBox = createTextBox<LabelledTextBox>(BeatmapsetsStrings.ShowInfoTags, metadata.Tags)
}; };
foreach (var item in Children.OfType<LabelledTextBox>())
item.OnCommit += onCommit;
} }
private TTextBox createTextBox<TTextBox>(LocalisableString label, string initialValue) private TTextBox createTextBox<TTextBox>(LocalisableString label, string initialValue)
@ -73,10 +70,14 @@ namespace osu.Game.Screens.Edit.Setup
base.LoadComplete(); base.LoadComplete();
if (string.IsNullOrEmpty(ArtistTextBox.Current.Value)) if (string.IsNullOrEmpty(ArtistTextBox.Current.Value))
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(ArtistTextBox)); ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(ArtistTextBox));
ArtistTextBox.Current.BindValueChanged(artist => transferIfRomanised(artist.NewValue, RomanisedArtistTextBox)); ArtistTextBox.Current.BindValueChanged(artist => transferIfRomanised(artist.NewValue, RomanisedArtistTextBox));
TitleTextBox.Current.BindValueChanged(title => transferIfRomanised(title.NewValue, RomanisedTitleTextBox)); TitleTextBox.Current.BindValueChanged(title => transferIfRomanised(title.NewValue, RomanisedTitleTextBox));
foreach (var item in Children.OfType<LabelledTextBox>())
item.OnCommit += onCommit;
updateReadOnlyState(); updateReadOnlyState();
} }
@ -86,7 +87,6 @@ namespace osu.Game.Screens.Edit.Setup
target.Current.Value = value; target.Current.Value = value;
updateReadOnlyState(); updateReadOnlyState();
Scheduler.AddOnce(updateMetadata);
} }
private void updateReadOnlyState() private void updateReadOnlyState()
@ -101,7 +101,7 @@ namespace osu.Game.Screens.Edit.Setup
// for now, update on commit rather than making BeatmapMetadata bindables. // for now, update on commit rather than making BeatmapMetadata bindables.
// after switching database engines we can reconsider if switching to bindables is a good direction. // after switching database engines we can reconsider if switching to bindables is a good direction.
Scheduler.AddOnce(updateMetadata); updateMetadata();
} }
private void updateMetadata() private void updateMetadata()

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Screens;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -55,6 +56,17 @@ namespace osu.Game.Screens.Edit.Setup
})); }));
} }
public override void OnExiting(ScreenExitEvent e)
{
base.OnExiting(e);
// Before exiting, trigger a focus loss.
//
// This is important to ensure that if the user is still editing a textbox, it will commit
// (and potentially block the exit procedure for save).
GetContainingFocusManager()?.TriggerFocusContention(this);
}
private partial class SetupScreenSectionsContainer : SectionsContainer<SetupSection> private partial class SetupScreenSectionsContainer : SectionsContainer<SetupSection>
{ {
protected override UserTrackingScrollContainer CreateScrollContainer() protected override UserTrackingScrollContainer CreateScrollContainer()

View File

@ -126,7 +126,7 @@ namespace osu.Game.Screens.Edit.Timing
protected override void OnFocus(FocusEvent e) protected override void OnFocus(FocusEvent e)
{ {
base.OnFocus(e); base.OnFocus(e);
GetContainingInputManager().ChangeFocus(textBox); GetContainingFocusManager().ChangeFocus(textBox);
} }
private void updateState() private void updateState()

View File

@ -24,6 +24,7 @@ using osu.Game.Input;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Overlays; using osu.Game.Overlays;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -46,6 +47,7 @@ namespace osu.Game.Screens.Menu
public Action? OnSettings; public Action? OnSettings;
public Action? OnMultiplayer; public Action? OnMultiplayer;
public Action? OnPlaylists; public Action? OnPlaylists;
public Action<Room>? OnDailyChallenge;
private readonly IBindable<bool> isIdle = new BindableBool(); private readonly IBindable<bool> isIdle = new BindableBool();
@ -102,10 +104,13 @@ namespace osu.Game.Screens.Menu
buttonArea.AddRange(new Drawable[] buttonArea.AddRange(new Drawable[]
{ {
new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, OsuIcon.Settings, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O, Key.S), new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, OsuIcon.Settings, new Color4(85, 85, 85, 255), _ => OnSettings?.Invoke(), Key.O, Key.S)
backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel,
-WEDGE_WIDTH)
{ {
Padding = new MarginPadding { Right = WEDGE_WIDTH },
},
backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), _ => State = ButtonSystemState.TopLevel)
{
Padding = new MarginPadding { Right = WEDGE_WIDTH },
VisibleStateMin = ButtonSystemState.Play, VisibleStateMin = ButtonSystemState.Play,
VisibleStateMax = ButtonSystemState.Edit, VisibleStateMax = ButtonSystemState.Edit,
}, },
@ -127,21 +132,31 @@ namespace osu.Game.Screens.Menu
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio, IdleTracker? idleTracker, GameHost host) private void load(AudioManager audio, IdleTracker? idleTracker, GameHost host)
{ {
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P)); buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), _ => OnSolo?.Invoke(), Key.P)
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", OsuIcon.Online, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M)); {
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Tournament, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L)); Padding = new MarginPadding { Left = WEDGE_WIDTH },
});
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", OsuIcon.Online, new Color4(94, 63, 186, 255), onMultiplayer, Key.M));
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Tournament, new Color4(94, 63, 186, 255), onPlaylists, Key.L));
buttonsPlay.Add(new DailyChallengeButton(@"button-default-select", new Color4(94, 63, 186, 255), onDailyChallenge, Key.D));
buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play);
buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), () => OnEditBeatmap?.Invoke(), WEDGE_WIDTH, Key.B, Key.E)); buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), _ => OnEditBeatmap?.Invoke(), Key.B, Key.E)
buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), () => OnEditSkin?.Invoke(), 0, Key.S)); {
Padding = new MarginPadding { Left = WEDGE_WIDTH },
});
buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), _ => OnEditSkin?.Invoke(), Key.S));
buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit); buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit);
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P, Key.M, Key.L)); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), _ => State = ButtonSystemState.Play, Key.P, Key.M, Key.L)
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => State = ButtonSystemState.Edit, 0, Key.E)); {
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.B, Key.D)); Padding = new MarginPadding { Left = WEDGE_WIDTH },
});
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), _ => State = ButtonSystemState.Edit, Key.E));
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), _ => OnBeatmapListing?.Invoke(), Key.B, Key.D));
if (host.CanExit) if (host.CanExit)
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q)); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), _ => OnExit?.Invoke(), Key.Q));
buttonArea.AddRange(buttonsPlay); buttonArea.AddRange(buttonsPlay);
buttonArea.AddRange(buttonsEdit); buttonArea.AddRange(buttonsEdit);
@ -164,7 +179,7 @@ namespace osu.Game.Screens.Menu
sampleLogoSwoosh = audio.Samples.Get(@"Menu/osu-logo-swoosh"); sampleLogoSwoosh = audio.Samples.Get(@"Menu/osu-logo-swoosh");
} }
private void onMultiplayer() private void onMultiplayer(MainMenuButton _)
{ {
if (api.State.Value != APIState.Online) if (api.State.Value != APIState.Online)
{ {
@ -175,7 +190,7 @@ namespace osu.Game.Screens.Menu
OnMultiplayer?.Invoke(); OnMultiplayer?.Invoke();
} }
private void onPlaylists() private void onPlaylists(MainMenuButton _)
{ {
if (api.State.Value != APIState.Online) if (api.State.Value != APIState.Online)
{ {
@ -186,6 +201,20 @@ namespace osu.Game.Screens.Menu
OnPlaylists?.Invoke(); OnPlaylists?.Invoke();
} }
private void onDailyChallenge(MainMenuButton button)
{
if (api.State.Value != APIState.Online)
{
loginOverlay?.Show();
return;
}
var dailyChallengeButton = (DailyChallengeButton)button;
if (dailyChallengeButton.Room != null)
OnDailyChallenge?.Invoke(dailyChallengeButton.Room);
}
private void updateIdleState(bool isIdle) private void updateIdleState(bool isIdle)
{ {
if (!ReturnToTopOnIdle) if (!ReturnToTopOnIdle)

View File

@ -0,0 +1,221 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Metadata;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Screens.Menu
{
public partial class DailyChallengeButton : MainMenuButton, IHasCustomTooltip<APIBeatmapSet?>
{
public Room? Room { get; private set; }
private readonly OsuSpriteText countdown;
private ScheduledDelegate? scheduledCountdownUpdate;
private UpdateableOnlineBeatmapSetCover cover = null!;
private IBindable<DailyChallengeInfo?> info = null!;
private Box gradientLayer = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
public DailyChallengeButton(string sampleName, Color4 colour, Action<MainMenuButton>? clickAction = null, params Key[] triggerKeys)
: base(ButtonSystemStrings.DailyChallenge, sampleName, OsuIcon.DailyChallenge, colour, clickAction, triggerKeys)
{
BaseSize = new Vector2(ButtonSystem.BUTTON_WIDTH * 1.3f, ButtonArea.BUTTON_AREA_HEIGHT);
Content.Add(countdown = new OsuSpriteText
{
Shadow = true,
AllowMultiline = false,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Margin = new MarginPadding
{
Left = -3,
Bottom = 22,
},
Font = OsuFont.Default.With(size: 12),
Alpha = 0,
});
}
protected override Drawable CreateBackground(Colour4 accentColour) => new BufferedContainer
{
Children = new Drawable[]
{
cover = new UpdateableOnlineBeatmapSetCover(timeBeforeLoad: 0, timeBeforeUnload: 600_000)
{
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.Both,
},
gradientLayer = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(accentColour.Opacity(0.2f), accentColour),
Blending = BlendingParameters.Additive,
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = accentColour.Opacity(0.7f)
},
},
};
[BackgroundDependencyLoader]
private void load(MetadataClient metadataClient)
{
info = metadataClient.DailyChallengeInfo.GetBoundCopy();
}
protected override void LoadComplete()
{
base.LoadComplete();
info.BindValueChanged(updateDisplay, true);
}
protected override void Update()
{
base.Update();
if (cover.LatestTransformEndTime == Time.Current)
{
const double duration = 3000;
float scale = 1 + RNG.NextSingle();
cover.ScaleTo(scale, duration, Easing.InOutSine)
.RotateTo(RNG.NextSingle(-4, 4) * (scale - 1), duration, Easing.InOutSine)
.MoveTo(new Vector2(
RNG.NextSingle(-0.5f, 0.5f) * (scale - 1),
RNG.NextSingle(-0.5f, 0.5f) * (scale - 1)
), duration, Easing.InOutSine);
gradientLayer.FadeIn(duration / 2)
.Then()
.FadeOut(duration / 2);
}
}
private void updateDisplay(ValueChangedEvent<DailyChallengeInfo?> info)
{
UpdateState();
scheduledCountdownUpdate?.Cancel();
scheduledCountdownUpdate = null;
if (info.NewValue == null)
{
Room = null;
cover.OnlineInfo = TooltipContent = null;
}
else
{
var roomRequest = new GetRoomRequest(info.NewValue.Value.RoomID);
roomRequest.Success += room =>
{
Room = room;
cover.OnlineInfo = TooltipContent = room.Playlist.FirstOrDefault()?.Beatmap.BeatmapSet as APIBeatmapSet;
updateCountdown();
Scheduler.AddDelayed(updateCountdown, 1000, true);
};
api.Queue(roomRequest);
}
}
private void updateCountdown()
{
if (Room == null)
return;
var remaining = (Room.EndDate.Value - DateTimeOffset.Now) ?? TimeSpan.Zero;
if (remaining <= TimeSpan.Zero)
{
countdown.FadeOut(250, Easing.OutQuint);
}
else
{
if (countdown.Alpha == 0)
countdown.FadeIn(250, Easing.OutQuint);
countdown.Text = remaining.ToString(@"hh\:mm\:ss");
}
}
protected override void UpdateState()
{
if (info.IsNotNull() && info.Value == null)
{
ContractStyle = 0;
State = ButtonState.Contracted;
return;
}
base.UpdateState();
}
public ITooltip<APIBeatmapSet?> GetCustomTooltip() => new DailyChallengeTooltip();
public APIBeatmapSet? TooltipContent { get; private set; }
internal partial class DailyChallengeTooltip : CompositeDrawable, ITooltip<APIBeatmapSet?>
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
private APIBeatmapSet? lastContent;
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
}
public void Move(Vector2 pos) => Position = pos;
public void SetContent(APIBeatmapSet? content)
{
if (content == lastContent)
return;
lastContent = content;
ClearInternal();
if (content != null)
AddInternal(new BeatmapCardNano(content));
}
}
}
}

View File

@ -147,6 +147,12 @@ namespace osu.Game.Screens.Menu
OnSolo = loadSoloSongSelect, OnSolo = loadSoloSongSelect,
OnMultiplayer = () => this.Push(new Multiplayer()), OnMultiplayer = () => this.Push(new Multiplayer()),
OnPlaylists = () => this.Push(new Playlists()), OnPlaylists = () => this.Push(new Playlists()),
OnDailyChallenge = room =>
{
Playlists playlistsScreen;
this.Push(playlistsScreen = new Playlists());
playlistsScreen.Join(room);
},
OnExit = () => OnExit = () =>
{ {
exitConfirmedViaHoldOrClick = true; exitConfirmedViaHoldOrClick = true;

Some files were not shown because too many files have changed in this diff Show More