1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-14 21:13:21 +08:00

Merge branch 'master' into user-panel-status

This commit is contained in:
Dean Herbert 2025-02-03 18:31:28 +09:00
commit f24716563c
No known key found for this signature in database
179 changed files with 3995 additions and 1936 deletions

View File

@ -96,7 +96,7 @@ jobs:
build-only-android: build-only-android:
name: Build only (Android) name: Build only (Android)
runs-on: windows-latest runs-on: windows-2019
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
- name: Checkout - name: Checkout

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.115.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2025.129.1" />
</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

@ -1,34 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Android.Content.PM;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Screens.Play;
namespace osu.Android
{
public partial class GameplayScreenRotationLocker : Component
{
private IBindable<LocalUserPlayingState> localUserPlaying = null!;
[Resolved]
private OsuGameActivity gameActivity { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(ILocalUserPlayInfo localUserPlayInfo)
{
localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy();
localUserPlaying.BindValueChanged(updateLock, true);
}
private void updateLock(ValueChangedEvent<LocalUserPlayingState> userPlaying)
{
gameActivity.RunOnUiThread(() =>
{
gameActivity.RequestedOrientation = userPlaying.NewValue == LocalUserPlayingState.Playing ? ScreenOrientation.Locked : gameActivity.DefaultOrientation;
});
}
}
}

View File

@ -49,6 +49,8 @@ namespace osu.Android
/// <remarks>Adjusted on startup to match expected UX for the current device type (phone/tablet).</remarks> /// <remarks>Adjusted on startup to match expected UX for the current device type (phone/tablet).</remarks>
public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified; public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified;
public new bool IsTablet { get; private set; }
private readonly OsuGameAndroid game; private readonly OsuGameAndroid game;
private bool gameCreated; private bool gameCreated;
@ -89,9 +91,9 @@ namespace osu.Android
WindowManager.DefaultDisplay.GetSize(displaySize); WindowManager.DefaultDisplay.GetSize(displaySize);
#pragma warning restore CA1422 #pragma warning restore CA1422
float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density; float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density;
bool isTablet = smallestWidthDp >= 600f; IsTablet = smallestWidthDp >= 600f;
RequestedOrientation = DefaultOrientation = isTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape; RequestedOrientation = DefaultOrientation = IsTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape;
// Currently (SDK 6.0.200), BundleAssemblies is not runnable for net6-android. // Currently (SDK 6.0.200), BundleAssemblies is not runnable for net6-android.
// The assembly files are not available as files either after native AOT. // The assembly files are not available as files either after native AOT.

View File

@ -3,11 +3,13 @@
using System; using System;
using Android.App; using Android.App;
using Android.Content.PM;
using Microsoft.Maui.Devices; using Microsoft.Maui.Devices;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game; using osu.Game;
using osu.Game.Screens;
using osu.Game.Updater; using osu.Game.Updater;
using osu.Game.Utils; using osu.Game.Utils;
@ -71,7 +73,35 @@ namespace osu.Android
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
LoadComponentAsync(new GameplayScreenRotationLocker(), Add); UserPlayingState.BindValueChanged(_ => updateOrientation());
}
protected override void ScreenChanged(IOsuScreen? current, IOsuScreen? newScreen)
{
base.ScreenChanged(current, newScreen);
if (newScreen != null)
updateOrientation();
}
private void updateOrientation()
{
var orientation = MobileUtils.GetOrientation(this, (IOsuScreen)ScreenStack.CurrentScreen, gameActivity.IsTablet);
switch (orientation)
{
case MobileUtils.Orientation.Locked:
gameActivity.RequestedOrientation = ScreenOrientation.Locked;
break;
case MobileUtils.Orientation.Portrait:
gameActivity.RequestedOrientation = ScreenOrientation.Portrait;
break;
case MobileUtils.Orientation.Default:
gameActivity.RequestedOrientation = gameActivity.DefaultOrientation;
break;
}
} }
public override void SetHost(GameHost host) public override void SetHost(GameHost host)

View File

@ -82,7 +82,7 @@ namespace osu.Desktop
}; };
client.OnReady += onReady; client.OnReady += onReady;
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network, LogLevel.Error); client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Message} ({e.Code})", LoggingTarget.Network);
try try
{ {

View File

@ -30,8 +30,6 @@ namespace osu.Desktop.Security
private partial class ElevatedPrivilegesNotification : SimpleNotification private partial class ElevatedPrivilegesNotification : SimpleNotification
{ {
public override bool IsImportant => true;
public ElevatedPrivilegesNotification() public ElevatedPrivilegesNotification()
{ {
Text = $"Running osu! as {(RuntimeInfo.IsUnix ? "root" : "administrator")} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user."; Text = $"Running osu! as {(RuntimeInfo.IsUnix ? "root" : "administrator")} does not improve performance, may break integrations and poses a security risk. Please run the game as a normal user.";

View File

@ -12,7 +12,6 @@ using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -71,11 +70,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
contentContainer.Playfield.HitObjectContainer.Add(hitObject); contentContainer.Playfield.HitObjectContainer.Add(hitObject);
} }
protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint) protected override void UpdatePlacementTimeAndPosition()
{ {
var result = base.SnapForBlueprint(blueprint); var position = InputManager.CurrentState.Mouse.Position;
result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP; double time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(position) / TIME_SNAP) * TIME_SNAP;
return result; CurrentBlueprint.UpdateTimeAndPosition(position, time);
} }
} }
} }

View File

@ -14,6 +14,7 @@ using osu.Game.Graphics;
using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Difficulty; using osu.Game.Rulesets.Catch.Difficulty;
using osu.Game.Rulesets.Catch.Edit; using osu.Game.Rulesets.Catch.Edit;
using osu.Game.Rulesets.Catch.Edit.Setup;
using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Catch.Replays;
@ -228,7 +229,7 @@ namespace osu.Game.Rulesets.Catch
public override IEnumerable<Drawable> CreateEditorSetupSections() => public override IEnumerable<Drawable> CreateEditorSetupSections() =>
[ [
new MetadataSection(), new MetadataSection(),
new DifficultySection(), new CatchDifficultySection(),
new FillFlowContainer new FillFlowContainer
{ {
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,

View File

@ -7,6 +7,7 @@ using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints namespace osu.Game.Rulesets.Catch.Edit.Blueprints
@ -59,11 +60,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
return base.OnMouseDown(e); return base.OnMouseDown(e);
} }
public override void UpdateTimeAndPosition(SnapResult result) public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
{ {
base.UpdateTimeAndPosition(result); var result = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
if (!(result.Time is double time)) return; base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
if (!(result.Time is double time)) return result;
switch (PlacementActive) switch (PlacementActive)
{ {
@ -78,6 +81,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
HitObject.StartTime = Math.Min(placementStartTime, placementEndTime); HitObject.StartTime = Math.Min(placementStartTime, placementEndTime);
HitObject.EndTime = Math.Max(placementStartTime, placementEndTime); HitObject.EndTime = Math.Max(placementStartTime, placementEndTime);
return result;
} }
} }
} }

View File

@ -9,7 +9,7 @@ using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{ {
public partial class CatchPlacementBlueprint<THitObject> : HitObjectPlacementBlueprint public abstract partial class CatchPlacementBlueprint<THitObject> : HitObjectPlacementBlueprint
where THitObject : CatchHitObject, new() where THitObject : CatchHitObject, new()
{ {
protected new THitObject HitObject => (THitObject)base.HitObject; protected new THitObject HitObject => (THitObject)base.HitObject;
@ -19,7 +19,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
[Resolved] [Resolved]
private Playfield playfield { get; set; } = null!; private Playfield playfield { get; set; } = null!;
public CatchPlacementBlueprint() [Resolved]
protected CatchHitObjectComposer? Composer { get; private set; }
protected CatchPlacementBlueprint()
: base(new THitObject()) : base(new THitObject())
{ {
} }

View File

@ -5,6 +5,7 @@ using osu.Framework.Input.Events;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Edit.Blueprints namespace osu.Game.Rulesets.Catch.Edit.Blueprints
@ -41,11 +42,20 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
return true; return true;
} }
public override void UpdateTimeAndPosition(SnapResult result) public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
{ {
base.UpdateTimeAndPosition(result); var gridSnapResult = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
gridSnapResult.ScreenSpacePosition.X = screenSpacePosition.X;
var distanceSnapResult = Composer?.TryDistanceSnap(gridSnapResult.ScreenSpacePosition);
var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS
? distanceSnapResult
: gridSnapResult;
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
HitObject.X = ToLocalSpace(result.ScreenSpacePosition).X; HitObject.X = ToLocalSpace(result.ScreenSpacePosition).X;
return result;
} }
} }
} }

View File

@ -83,8 +83,16 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
return base.OnMouseDown(e); return base.OnMouseDown(e);
} }
public override void UpdateTimeAndPosition(SnapResult result) public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
{ {
var gridSnapResult = Composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
gridSnapResult.ScreenSpacePosition.X = screenSpacePosition.X;
var distanceSnapResult = Composer?.TryDistanceSnap(gridSnapResult.ScreenSpacePosition);
var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS
? distanceSnapResult
: gridSnapResult;
switch (PlacementActive) switch (PlacementActive)
{ {
case PlacementState.Waiting: case PlacementState.Waiting:
@ -99,7 +107,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
break; break;
default: default:
return; return result;
} }
// Make sure the up-to-date position is used for outlines. // Make sure the up-to-date position is used for outlines.
@ -113,6 +121,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
ApplyDefaultsToHitObject(); ApplyDefaultsToHitObject();
scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject); scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject);
nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject); nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject);
return result;
} }
private double positionToTime(float relativeYPosition) private double positionToTime(float relativeYPosition)

View File

@ -1,16 +1,22 @@
// 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 System.Collections.Generic;
using System.Linq;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Catch.Edit.Blueprints; using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit namespace osu.Game.Rulesets.Catch.Edit
{ {
public partial class CatchBlueprintContainer : ComposeBlueprintContainer public partial class CatchBlueprintContainer : ComposeBlueprintContainer
{ {
public new CatchHitObjectComposer Composer => (CatchHitObjectComposer)base.Composer;
public CatchBlueprintContainer(CatchHitObjectComposer composer) public CatchBlueprintContainer(CatchHitObjectComposer composer)
: base(composer) : base(composer)
{ {
@ -36,5 +42,28 @@ namespace osu.Game.Rulesets.Catch.Edit
} }
protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield); protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<HitObject> blueprint, Vector2[] originalSnapPositions)> blueprints)
{
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
// The final movement position, relative to movementBlueprintOriginalPosition.
Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled;
// Retrieve a snapped position.
var gridSnapResult = Composer.FindSnappedPositionAndTime(movePosition);
gridSnapResult.ScreenSpacePosition.X = movePosition.X;
var distanceSnapResult = Composer.TryDistanceSnap(gridSnapResult.ScreenSpacePosition);
var result = distanceSnapResult != null && Vector2.Distance(gridSnapResult.ScreenSpacePosition, distanceSnapResult.ScreenSpacePosition) < CatchHitObjectComposer.DISTANCE_SNAP_RADIUS
? distanceSnapResult
: gridSnapResult;
var referenceBlueprint = blueprints.First().blueprint;
bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint));
if (moved)
ApplySnapResultTime(result, referenceBlueprint.Item.StartTime);
return moved;
}
} }
} }

View File

@ -23,9 +23,10 @@ using osuTK;
namespace osu.Game.Rulesets.Catch.Edit namespace osu.Game.Rulesets.Catch.Edit
{ {
[Cached]
public partial class CatchHitObjectComposer : ScrollingHitObjectComposer<CatchHitObject>, IKeyBindingHandler<GlobalAction> public partial class CatchHitObjectComposer : ScrollingHitObjectComposer<CatchHitObject>, IKeyBindingHandler<GlobalAction>
{ {
private const float distance_snap_radius = 50; public const float DISTANCE_SNAP_RADIUS = 50;
private CatchDistanceSnapGrid distanceSnapGrid = null!; private CatchDistanceSnapGrid distanceSnapGrid = null!;
@ -135,22 +136,12 @@ namespace osu.Game.Rulesets.Catch.Edit
DistanceSnapProvider.HandleToggleViaKey(key); DistanceSnapProvider.HandleToggleViaKey(key);
} }
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) public SnapResult? TryDistanceSnap(Vector2 screenSpacePosition)
{ {
var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType); if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(screenSpacePosition) is SnapResult snapResult)
return snapResult;
result.ScreenSpacePosition.X = screenSpacePosition.X; return null;
if (snapType.HasFlag(SnapType.RelativeGrids))
{
if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult &&
Vector2.Distance(snapResult.ScreenSpacePosition, result.ScreenSpacePosition) < distance_snap_radius)
{
result = snapResult;
}
}
return result;
} }
private PalpableCatchHitObject? getLastSnappableHitObject(double time) private PalpableCatchHitObject? getLastSnappableHitObject(double time)

View File

@ -0,0 +1,125 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Rulesets.Catch.Edit.Setup
{
public partial class CatchDifficultySection : SetupSection
{
private FormSliderBar<float> circleSizeSlider { get; set; } = null!;
private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
private FormSliderBar<float> approachRateSlider { get; set; } = null!;
private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
private FormSliderBar<double> tickRateSlider { get; set; } = null!;
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
circleSizeSlider = new FormSliderBar<float>
{
Caption = BeatmapsetsStrings.ShowStatsCs,
HintText = EditorSetupStrings.CircleSizeDescription,
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
},
TransferValueOnCommit = true,
TabbableContentContainer = this,
},
healthDrainSlider = new FormSliderBar<float>
{
Caption = BeatmapsetsStrings.ShowStatsDrain,
HintText = EditorSetupStrings.DrainRateDescription,
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
},
TransferValueOnCommit = true,
TabbableContentContainer = this,
},
approachRateSlider = new FormSliderBar<float>
{
Caption = BeatmapsetsStrings.ShowStatsAr,
HintText = EditorSetupStrings.ApproachRateDescription,
Current = new BindableFloat(Beatmap.Difficulty.ApproachRate)
{
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
},
TransferValueOnCommit = true,
TabbableContentContainer = this,
},
baseVelocitySlider = new FormSliderBar<double>
{
Caption = EditorSetupStrings.BaseVelocity,
HintText = EditorSetupStrings.BaseVelocityDescription,
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
{
Default = 1.4,
MinValue = 0.4,
MaxValue = 3.6,
Precision = 0.01f,
},
TransferValueOnCommit = true,
TabbableContentContainer = this,
},
tickRateSlider = new FormSliderBar<double>
{
Caption = EditorSetupStrings.TickRate,
HintText = EditorSetupStrings.TickRateDescription,
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
{
Default = 1,
MinValue = 1,
MaxValue = 4,
Precision = 1,
},
TransferValueOnCommit = true,
TabbableContentContainer = this,
},
};
foreach (var item in Children.OfType<FormSliderBar<float>>())
item.Current.ValueChanged += _ => updateValues();
foreach (var item in Children.OfType<FormSliderBar<double>>())
item.Current.ValueChanged += _ => updateValues();
}
private void updateValues()
{
// for now, update these on commit rather than making BeatmapMetadata bindables.
// after switching database engines we can reconsider if switching to bindables is a good direction.
Beatmap.Difficulty.CircleSize = circleSizeSlider.Current.Value;
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
Beatmap.Difficulty.ApproachRate = approachRateSlider.Current.Value;
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;
Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value;
Beatmap.UpdateAllHitObjects();
Beatmap.SaveState();
}
}
}

View File

@ -4,6 +4,7 @@
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -47,6 +48,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
return new DefaultSkinComponentsContainer(container => return new DefaultSkinComponentsContainer(container =>
{ {
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault(); var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
if (keyCounter != null) if (keyCounter != null)
{ {
@ -55,11 +57,19 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
keyCounter.Origin = Anchor.TopRight; keyCounter.Origin = Anchor.TopRight;
keyCounter.Position = new Vector2(0, -40) * 1.6f; keyCounter.Position = new Vector2(0, -40) * 1.6f;
} }
if (spectatorList != null)
{
spectatorList.Anchor = Anchor.BottomLeft;
spectatorList.Origin = Anchor.BottomLeft;
spectatorList.Position = new Vector2(10, -10);
}
}) })
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
new LegacyKeyCounterDisplay(), new LegacyKeyCounterDisplay(),
new SpectatorList(),
} }
}; };
} }

View File

@ -9,7 +9,6 @@ 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.Timing; using osu.Framework.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
@ -47,12 +46,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
}); });
} }
protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint) protected override void UpdatePlacementTimeAndPosition()
{ {
double time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position); double time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
var pos = column.ScreenSpacePositionAtTime(time); var pos = column.ScreenSpacePositionAtTime(time);
CurrentBlueprint.UpdateTimeAndPosition(pos, time);
return new SnapResult(pos, time, column);
} }
protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both }; protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both };

View File

@ -20,7 +20,6 @@ using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Mania.Tests.Editor namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
@ -100,10 +99,5 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
{ {
set => InternalChild = value; set => InternalChild = value;
} }
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
{
throw new NotImplementedException();
}
} }
} }

View File

@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
{ {
CreateModTest(new ModTestData CreateModTest(new ModTestData
{ {
Mod = new ManiaModHidden(), Mod = new ManiaModFadeIn(),
PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE) PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE)
}); });
} }
@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
{ {
CreateModTest(new ModTestData CreateModTest(new ModTestData
{ {
Mod = new ManiaModHidden(), Mod = new ManiaModFadeIn(),
PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE) PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE)
}); });
@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
{ {
CreateModTest(new ModTestData CreateModTest(new ModTestData
{ {
Mod = new ManiaModHidden(), Mod = new ManiaModFadeIn(),
PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE) PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE)
}); });
@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
{ {
CreateModTest(new ModTestData CreateModTest(new ModTestData
{ {
Mod = new ManiaModHidden(), Mod = new ManiaModFadeIn(),
PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE) PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE)
}); });
@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
{ {
CreateModTest(new ModTestData CreateModTest(new ModTestData
{ {
Mod = new ManiaModHidden(), Mod = new ManiaModFadeIn(),
CreateBeatmap = () => new Beatmap CreateBeatmap = () => new Beatmap
{ {
HitObjects = Enumerable.Range(1, 100).Select(i => (HitObject)new Note { StartTime = 1000 + 200 * i }).ToList(), HitObjects = Enumerable.Range(1, 100).Select(i => (HitObject)new Note { StartTime = 1000 + 200 * i }).ToList(),

View File

@ -28,18 +28,20 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Width = 0.5f, Width = 0.5f,
Child = new ColumnHitObjectArea(new HitObjectContainer()) Child = new ColumnHitObjectArea
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both,
Child = new HitObjectContainer(),
} }
}, },
new ColumnTestContainer(1, ManiaAction.Key2) new ColumnTestContainer(1, ManiaAction.Key2)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Width = 0.5f, Width = 0.5f,
Child = new ColumnHitObjectArea(new HitObjectContainer()) Child = new ColumnHitObjectArea
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both,
Child = new HitObjectContainer(),
} }
} }
} }

View File

@ -0,0 +1,68 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
public partial class TestSceneManiaTouchInput : PlayerTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
[Test]
public void TestTouchInput()
{
for (int i = 0; i < 4; i++)
{
int index = i;
AddStep($"touch column {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getColumn(index).ScreenSpaceDrawQuad.Centre)));
AddAssert("action sent",
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
() => Does.Contain(getColumn(index).Action.Value));
AddStep($"release column {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(index).ScreenSpaceDrawQuad.Centre)));
AddAssert("action released",
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
() => Does.Not.Contain(getColumn(index).Action.Value));
}
}
[Test]
public void TestOneColumnMultipleTouches()
{
AddStep("touch column 0", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre)));
AddAssert("action sent",
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
() => Does.Contain(getColumn(0).Action.Value));
AddStep("touch another finger", () => InputManager.BeginTouch(new Touch(TouchSource.Touch2, getColumn(0).ScreenSpaceDrawQuad.Centre)));
AddAssert("action still pressed",
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
() => Does.Contain(getColumn(0).Action.Value));
AddStep("release first finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getColumn(0).ScreenSpaceDrawQuad.Centre)));
AddAssert("action still pressed",
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
() => Does.Contain(getColumn(0).Action.Value));
AddStep("release second finger", () => InputManager.EndTouch(new Touch(TouchSource.Touch2, getColumn(0).ScreenSpaceDrawQuad.Centre)));
AddAssert("action released",
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
() => Does.Not.Contain(getColumn(0).Action.Value));
}
private Column getColumn(int index) => this.ChildrenOfType<Column>().ElementAt(index);
}
}

View File

@ -1,49 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
public partial class TestSceneManiaTouchInputArea : PlayerTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
[Test]
public void TestTouchAreaNotInitiallyVisible()
{
AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
}
[Test]
public void TestPressReceptors()
{
AddAssert("touch area not visible", () => getTouchOverlay()?.State.Value == Visibility.Hidden);
for (int i = 0; i < 4; i++)
{
int index = i;
AddStep($"touch receptor {index}", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
AddAssert("action sent",
() => this.ChildrenOfType<ManiaInputManager>().SelectMany(m => m.KeyBindingContainer.PressedActions),
() => Does.Contain(getReceptor(index).Action.Value));
AddStep($"release receptor {index}", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, getReceptor(index).ScreenSpaceDrawQuad.Centre)));
AddAssert("touch area visible", () => getTouchOverlay()?.State.Value == Visibility.Visible);
}
}
private ManiaTouchInputArea? getTouchOverlay() => this.ChildrenOfType<ManiaTouchInputArea>().SingleOrDefault();
private ManiaTouchInputArea.ColumnInputReceptor getReceptor(int index) => this.ChildrenOfType<ManiaTouchInputArea.ColumnInputReceptor>().ElementAt(index);
}
}

View File

@ -98,9 +98,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
private double originalStartTime; private double originalStartTime;
public override void UpdateTimeAndPosition(SnapResult result) public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
{ {
base.UpdateTimeAndPosition(result); var result = base.UpdateTimeAndPosition(screenSpacePosition, fallbackTime);
if (PlacementActive == PlacementState.Active) if (PlacementActive == PlacementState.Active)
{ {
@ -121,6 +121,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
if (result.Time is double startTime) if (result.Time is double startTime)
originalStartTime = HitObject.StartTime = startTime; originalStartTime = HitObject.StartTime = startTime;
} }
return result;
} }
} }
} }

View File

@ -5,7 +5,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components; using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
@ -24,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
private EditorBeatmap? editorBeatmap { get; set; } private EditorBeatmap? editorBeatmap { get; set; }
[Resolved] [Resolved]
private IPositionSnapProvider? positionSnapProvider { get; set; } private ManiaHitObjectComposer? positionSnapProvider { get; set; }
private EditBodyPiece body = null!; private EditBodyPiece body = null!;
private EditHoldNoteEndPiece head = null!; private EditHoldNoteEndPiece head = null!;

View File

@ -1,8 +1,8 @@
// 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.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
@ -20,13 +20,18 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{ {
protected new T HitObject => (T)base.HitObject; protected new T HitObject => (T)base.HitObject;
private Column column; [Resolved]
private ManiaHitObjectComposer? composer { get; set; }
public Column Column private Column? column;
public Column? Column
{ {
get => column; get => column;
set set
{ {
ArgumentNullException.ThrowIfNull(value);
if (value == column) if (value == column)
return; return;
@ -53,9 +58,11 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
return true; return true;
} }
public override void UpdateTimeAndPosition(SnapResult result) public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
{ {
base.UpdateTimeAndPosition(result); var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
if (result.Playfield is Column col) if (result.Playfield is Column col)
{ {
@ -76,6 +83,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
if (PlacementActive == PlacementState.Waiting) if (PlacementActive == PlacementState.Waiting)
Column = col; Column = col;
} }
return result;
} }
private float getNoteHeight(Column resultPlayfield) => private float getNoteHeight(Column resultPlayfield) =>

View File

@ -8,6 +8,7 @@ using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints namespace osu.Game.Rulesets.Mania.Edit.Blueprints
@ -35,15 +36,17 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
}; };
} }
public override void UpdateTimeAndPosition(SnapResult result) public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double referenceTime)
{ {
base.UpdateTimeAndPosition(result); var result = base.UpdateTimeAndPosition(screenSpacePosition, referenceTime);
if (result.Playfield != null) if (result.Playfield != null)
{ {
piece.Width = result.Playfield.DrawWidth; piece.Width = result.Playfield.DrawWidth;
piece.Position = ToLocalSpace(result.ScreenSpacePosition); piece.Position = ToLocalSpace(result.ScreenSpacePosition);
} }
return result;
} }
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)

View File

@ -1,17 +1,23 @@
// 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 System.Collections.Generic;
using System.Linq;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Mania.Edit namespace osu.Game.Rulesets.Mania.Edit
{ {
public partial class ManiaBlueprintContainer : ComposeBlueprintContainer public partial class ManiaBlueprintContainer : ComposeBlueprintContainer
{ {
public ManiaBlueprintContainer(HitObjectComposer composer) public new ManiaHitObjectComposer Composer => (ManiaHitObjectComposer)base.Composer;
public ManiaBlueprintContainer(ManiaHitObjectComposer composer)
: base(composer) : base(composer)
{ {
} }
@ -33,5 +39,22 @@ namespace osu.Game.Rulesets.Mania.Edit
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new ManiaSelectionHandler(); protected override SelectionHandler<HitObject> CreateSelectionHandler() => new ManiaSelectionHandler();
protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield); protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<HitObject> blueprint, Vector2[] originalSnapPositions)> blueprints)
{
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
// The final movement position, relative to movementBlueprintOriginalPosition.
Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled;
// Retrieve a snapped position.
var result = Composer.FindSnappedPositionAndTime(movePosition);
var referenceBlueprint = blueprints.First().blueprint;
bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint));
if (moved)
ApplySnapResultTime(result, referenceBlueprint.Item.StartTime);
return moved;
}
} }
} }

View File

@ -19,6 +19,7 @@ using osuTK;
namespace osu.Game.Rulesets.Mania.Edit namespace osu.Game.Rulesets.Mania.Edit
{ {
[Cached]
public partial class ManiaHitObjectComposer : ScrollingHitObjectComposer<ManiaHitObject> public partial class ManiaHitObjectComposer : ScrollingHitObjectComposer<ManiaHitObject>
{ {
private DrawableManiaEditorRuleset drawableRuleset = null!; private DrawableManiaEditorRuleset drawableRuleset = null!;
@ -64,11 +65,11 @@ namespace osu.Game.Rulesets.Mania.Edit
return; return;
List<ManiaHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<ManiaHitObject>().Where(h => h.StartTime >= timestamp).ToList(); List<ManiaHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<ManiaHitObject>().Where(h => h.StartTime >= timestamp).ToList();
string[] objectDescriptions = objectDescription.Split(',').ToArray(); string[] objectDescriptions = objectDescription.Split(',');
for (int i = 0; i < objectDescriptions.Length; i++) for (int i = 0; i < objectDescriptions.Length; i++)
{ {
string[] split = objectDescriptions[i].Split('|').ToArray(); string[] split = objectDescriptions[i].Split('|');
if (split.Length != 2) if (split.Length != 2)
continue; continue;

View File

@ -8,7 +8,7 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania namespace osu.Game.Rulesets.Mania
{ {
[Cached] // Used for touch input, see ColumnTouchInputArea. [Cached] // Used for touch input, see Column.OnTouchDown/OnTouchUp.
public partial class ManiaInputManager : RulesetInputManager<ManiaAction> public partial class ManiaInputManager : RulesetInputManager<ManiaAction>
{ {
public ManiaInputManager(RulesetInfo ruleset, int variant) public ManiaInputManager(RulesetInfo ruleset, int variant)

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mania.Mods
foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns)) foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns))
{ {
HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer; HitObjectContainer hoc = column.HitObjectContainer;
Container hocParent = (Container)hoc.Parent!; Container hocParent = (Container)hoc.Parent!;
hocParent.Remove(hoc, false); hocParent.Remove(hoc, false);

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
{ {
public partial class ArgonJudgementPiece : TextJudgementPiece, IAnimatableJudgement public partial class ArgonJudgementPiece : TextJudgementPiece, IAnimatableJudgement
{ {
private const float judgement_y_position = 160; private const float judgement_y_position = -180f;
private RingExplosion? ringExplosion; private RingExplosion? ringExplosion;

View File

@ -9,7 +9,9 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning.Argon namespace osu.Game.Rulesets.Mania.Skinning.Argon
@ -39,6 +41,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
return new DefaultSkinComponentsContainer(container => return new DefaultSkinComponentsContainer(container =>
{ {
var combo = container.ChildrenOfType<ArgonManiaComboCounter>().FirstOrDefault(); var combo = container.ChildrenOfType<ArgonManiaComboCounter>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
if (combo != null) if (combo != null)
{ {
@ -47,9 +50,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
combo.Origin = Anchor.Centre; combo.Origin = Anchor.Centre;
combo.Y = 200; combo.Y = 200;
} }
if (spectatorList != null)
spectatorList.Position = new Vector2(36, -66);
}) })
{ {
new ArgonManiaComboCounter(), new ArgonManiaComboCounter(),
new SpectatorList
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
}
}; };
} }

View File

@ -7,7 +7,6 @@ using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -23,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
this.result = result; this.result = result;
this.animation = animation; this.animation = animation;
Anchor = Anchor.Centre; Anchor = Anchor.BottomCentre;
Origin = Anchor.Centre; Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
@ -32,12 +31,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISkinSource skin) private void load(ISkinSource skin)
{ {
float? scorePosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value; float hitPosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? 0;
float scorePosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value ?? 0;
if (scorePosition != null) float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition;
scorePosition -= Stage.HIT_TARGET_POSITION + 150; Y = scorePosition - absoluteHitPosition;
Y = scorePosition ?? 0;
InternalChild = animation.With(d => InternalChild = animation.With(d =>
{ {

View File

@ -15,7 +15,9 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Mania.Skinning.Legacy namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{ {
@ -95,6 +97,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
return new DefaultSkinComponentsContainer(container => return new DefaultSkinComponentsContainer(container =>
{ {
var combo = container.ChildrenOfType<LegacyManiaComboCounter>().FirstOrDefault(); var combo = container.ChildrenOfType<LegacyManiaComboCounter>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
if (combo != null) if (combo != null)
{ {
@ -102,9 +105,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
combo.Origin = Anchor.Centre; combo.Origin = Anchor.Centre;
combo.Y = this.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ComboPosition)?.Value ?? 0; combo.Y = this.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ComboPosition)?.Value ?? 0;
} }
if (spectatorList != null)
{
spectatorList.Anchor = Anchor.BottomLeft;
spectatorList.Origin = Anchor.BottomLeft;
spectatorList.Position = new Vector2(10, -10);
}
}) })
{ {
new LegacyManiaComboCounter(), new LegacyManiaComboCounter(),
new SpectatorList(),
}; };
} }

View File

@ -1,10 +1,9 @@
// 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 osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
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.Graphics.Pooling; using osu.Framework.Graphics.Pooling;
@ -45,11 +44,11 @@ namespace osu.Game.Rulesets.Mania.UI
internal readonly Container TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }; internal readonly Container TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both };
private DrawablePool<PoolableHitExplosion> hitExplosionPool; private DrawablePool<PoolableHitExplosion> hitExplosionPool = null!;
private readonly OrderedHitPolicy hitPolicy; private readonly OrderedHitPolicy hitPolicy;
public Container UnderlayElements => HitObjectArea.UnderlayElements; public Container UnderlayElements => HitObjectArea.UnderlayElements;
private GameplaySampleTriggerSource sampleTriggerSource; private GameplaySampleTriggerSource sampleTriggerSource = null!;
/// <summary> /// <summary>
/// Whether this is a special (ie. scratch) column. /// Whether this is a special (ie. scratch) column.
@ -67,11 +66,15 @@ namespace osu.Game.Rulesets.Mania.UI
Width = COLUMN_WIDTH; Width = COLUMN_WIDTH;
hitPolicy = new OrderedHitPolicy(HitObjectContainer); hitPolicy = new OrderedHitPolicy(HitObjectContainer);
HitObjectArea = new ColumnHitObjectArea(HitObjectContainer) { RelativeSizeAxes = Axes.Both }; HitObjectArea = new ColumnHitObjectArea
{
RelativeSizeAxes = Axes.Both,
Child = HitObjectContainer,
};
} }
[Resolved] [Resolved]
private ISkinSource skin { get; set; } private ISkinSource skin { get; set; } = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host) private void load(GameHost host)
@ -132,7 +135,7 @@ namespace osu.Game.Rulesets.Mania.UI
base.Dispose(isDisposing); base.Dispose(isDisposing);
if (skin != null) if (skin.IsNotNull())
skin.SourceChanged -= onSourceChanged; skin.SourceChanged -= onSourceChanged;
} }
@ -180,5 +183,29 @@ namespace osu.Game.Rulesets.Mania.UI
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
// This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border // This probably shouldn't exist as is, but the columns in the stage are separated by a 1px border
=> DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos)); => DrawRectangle.Inflate(new Vector2(Stage.COLUMN_SPACING / 2, 0)).Contains(ToLocalSpace(screenSpacePos));
#region Touch Input
[Resolved]
private ManiaInputManager? maniaInputManager { get; set; }
private int touchActivationCount;
protected override bool OnTouchDown(TouchDownEvent e)
{
maniaInputManager?.KeyBindingContainer.TriggerPressed(Action.Value);
touchActivationCount++;
return true;
}
protected override void OnTouchUp(TouchUpEvent e)
{
touchActivationCount--;
if (touchActivationCount == 0)
maniaInputManager?.KeyBindingContainer.TriggerReleased(Action.Value);
}
#endregion
} }
} }

View File

@ -3,13 +3,12 @@
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.UI.Components namespace osu.Game.Rulesets.Mania.UI.Components
{ {
public partial class ColumnHitObjectArea : HitObjectArea public partial class ColumnHitObjectArea : HitPositionPaddedContainer
{ {
public readonly Container Explosions; public readonly Container Explosions;
@ -17,25 +16,29 @@ namespace osu.Game.Rulesets.Mania.UI.Components
private readonly Drawable hitTarget; private readonly Drawable hitTarget;
public ColumnHitObjectArea(HitObjectContainer hitObjectContainer) protected override Container<Drawable> Content => content;
: base(hitObjectContainer)
private readonly Container content;
public ColumnHitObjectArea()
{ {
AddRangeInternal(new[] AddRangeInternal(new[]
{ {
UnderlayElements = new Container UnderlayElements = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Depth = 2,
}, },
hitTarget = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.HitTarget), _ => new DefaultHitTarget()) hitTarget = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.HitTarget), _ => new DefaultHitTarget())
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Depth = 1 },
content = new Container
{
RelativeSizeAxes = Axes.Both,
}, },
Explosions = new Container Explosions = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Depth = -1,
} }
}); });
} }

View File

@ -1,52 +1,38 @@
// 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.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
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.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.UI.Components namespace osu.Game.Rulesets.Mania.UI.Components
{ {
public partial class HitObjectArea : SkinReloadableDrawable public partial class HitPositionPaddedContainer : Container
{ {
protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>(); protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
public readonly HitObjectContainer HitObjectContainer;
public HitObjectArea(HitObjectContainer hitObjectContainer) [Resolved]
{ private ISkinSource skin { get; set; } = null!;
InternalChild = new Container
{
RelativeSizeAxes = Axes.Both,
Child = HitObjectContainer = hitObjectContainer
};
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo) private void load(IScrollingInfo scrollingInfo)
{ {
Direction.BindTo(scrollingInfo.Direction); Direction.BindTo(scrollingInfo.Direction);
Direction.BindValueChanged(onDirectionChanged, true); Direction.BindValueChanged(_ => UpdateHitPosition(), true);
skin.SourceChanged += onSkinChanged;
} }
protected override void SkinChanged(ISkinSource skin) private void onSkinChanged() => UpdateHitPosition();
{
base.SkinChanged(skin);
UpdateHitPosition();
}
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
{
UpdateHitPosition();
}
protected virtual void UpdateHitPosition() protected virtual void UpdateHitPosition()
{ {
float hitPosition = CurrentSkin.GetConfig<ManiaSkinConfigurationLookup, float>( float hitPosition = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value
?? Stage.HIT_TARGET_POSITION; ?? Stage.HIT_TARGET_POSITION;
@ -54,5 +40,13 @@ namespace osu.Game.Rulesets.Mania.UI.Components
? new MarginPadding { Top = hitPosition } ? new MarginPadding { Top = hitPosition }
: new MarginPadding { Bottom = hitPosition }; : new MarginPadding { Bottom = hitPosition };
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (skin.IsNotNull())
skin.SourceChanged -= onSkinChanged;
}
} }
} }

View File

@ -6,6 +6,7 @@
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Mania.UI namespace osu.Game.Rulesets.Mania.UI
{ {
@ -15,9 +16,12 @@ namespace osu.Game.Rulesets.Mania.UI
private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece
{ {
private const float judgement_y_position = -180f;
public DefaultManiaJudgementPiece(HitResult result) public DefaultManiaJudgementPiece(HitResult result)
: base(result) : base(result)
{ {
Y = judgement_y_position;
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -32,8 +36,20 @@ namespace osu.Game.Rulesets.Mania.UI
switch (Result) switch (Result)
{ {
case HitResult.None: case HitResult.None:
this.FadeOutFromOne(800);
break;
case HitResult.Miss: case HitResult.Miss:
base.PlayAnimation(); this.ScaleTo(1.6f);
this.ScaleTo(1, 100, Easing.In);
this.MoveToY(judgement_y_position);
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
this.RotateTo(0);
this.RotateTo(40, 800, Easing.InQuint);
this.FadeOutFromOne(800);
break; break;
default: default:
@ -43,8 +59,6 @@ namespace osu.Game.Rulesets.Mania.UI
this.Delay(50) this.Delay(50)
.ScaleTo(0.75f, 250) .ScaleTo(0.75f, 250)
.FadeOut(200); .FadeOut(200);
// osu!mania uses a custom fade length, so the base call is intentionally omitted.
break; break;
} }
} }

View File

@ -32,7 +32,6 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.UI namespace osu.Game.Rulesets.Mania.UI
{ {
[Cached]
public partial class DrawableManiaRuleset : DrawableScrollingRuleset<ManiaHitObject> public partial class DrawableManiaRuleset : DrawableScrollingRuleset<ManiaHitObject>
{ {
/// <summary> /// <summary>
@ -51,6 +50,8 @@ namespace osu.Game.Rulesets.Mania.UI
public IEnumerable<BarLine> BarLines; public IEnumerable<BarLine> BarLines;
public override bool RequiresPortraitOrientation => Beatmap.Stages.Count == 1;
protected override bool RelativeScaleBeatLengths => true; protected override bool RelativeScaleBeatLengths => true;
protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config; protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config;
@ -110,8 +111,6 @@ namespace osu.Game.Rulesets.Mania.UI
configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue)); configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue));
TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value); TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value);
KeyBindingInputManager.Add(new ManiaTouchInputArea());
} }
protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount; protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount;
@ -162,7 +161,7 @@ namespace osu.Game.Rulesets.Mania.UI
/// <returns>The scroll time.</returns> /// <returns>The scroll time.</returns>
public static double ComputeScrollTime(double scrollSpeed) => MAX_TIME_RANGE / scrollSpeed; public static double ComputeScrollTime(double scrollSpeed) => MAX_TIME_RANGE / scrollSpeed;
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer(); public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer(this);
protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages); protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages);

View File

@ -1,17 +1,63 @@
// 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 System;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Mania.UI namespace osu.Game.Rulesets.Mania.UI
{ {
public partial class ManiaPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer public partial class ManiaPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
{ {
public ManiaPlayfieldAdjustmentContainer() protected override Container<Drawable> Content { get; }
private readonly DrawSizePreservingFillContainer scalingContainer;
private readonly DrawableManiaRuleset drawableManiaRuleset;
public ManiaPlayfieldAdjustmentContainer(DrawableManiaRuleset drawableManiaRuleset)
{ {
Anchor = Anchor.Centre; this.drawableManiaRuleset = drawableManiaRuleset;
Origin = Anchor.Centre; InternalChild = scalingContainer = new DrawSizePreservingFillContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Child = Content = new Container
{
RelativeSizeAxes = Axes.Both,
}
};
}
protected override void Update()
{
base.Update();
float aspectRatio = DrawWidth / DrawHeight;
bool isPortrait = aspectRatio < 1f;
if (isPortrait && drawableManiaRuleset.Beatmap.Stages.Count == 1)
{
// Scale playfield up by 25% to become playable on mobile devices,
// and leave a 10% horizontal gap if the playfield is scaled down due to being too wide.
const float base_scale = 1.25f;
const float base_width = 768f / base_scale;
const float side_gap = 0.9f;
scalingContainer.Strategy = DrawSizePreservationStrategy.Maximum;
float stageWidth = drawableManiaRuleset.Playfield.Stages[0].DrawWidth;
scalingContainer.TargetDrawSize = new Vector2(1024, base_width * Math.Max(stageWidth / aspectRatio / (base_width * side_gap), 1f));
}
else
{
scalingContainer.Strategy = DrawSizePreservationStrategy.Minimum;
scalingContainer.Scale = new Vector2(1f);
scalingContainer.Size = new Vector2(1f);
scalingContainer.TargetDrawSize = new Vector2(1024, 768);
}
} }
} }
} }

View File

@ -1,199 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
/// <summary>
/// An overlay that captures and displays osu!mania mouse and touch input.
/// </summary>
public partial class ManiaTouchInputArea : VisibilityContainer
{
// visibility state affects our child. we always want to handle input.
public override bool PropagatePositionalInputSubTree => true;
public override bool PropagateNonPositionalInputSubTree => true;
[SettingSource("Spacing", "The spacing between receptors.")]
public BindableFloat Spacing { get; } = new BindableFloat(10)
{
Precision = 1,
MinValue = 0,
MaxValue = 100,
};
[SettingSource("Opacity", "The receptor opacity.")]
public BindableFloat Opacity { get; } = new BindableFloat(1)
{
Precision = 0.1f,
MinValue = 0,
MaxValue = 1
};
[Resolved]
private DrawableManiaRuleset drawableRuleset { get; set; } = null!;
private GridContainer gridContainer = null!;
public ManiaTouchInputArea()
{
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
RelativeSizeAxes = Axes.Both;
Height = 0.5f;
}
[BackgroundDependencyLoader]
private void load()
{
List<Drawable> receptorGridContent = new List<Drawable>();
List<Dimension> receptorGridDimensions = new List<Dimension>();
bool first = true;
foreach (var stage in drawableRuleset.Playfield.Stages)
{
foreach (var column in stage.Columns)
{
if (!first)
{
receptorGridContent.Add(new Gutter { Spacing = { BindTarget = Spacing } });
receptorGridDimensions.Add(new Dimension(GridSizeMode.AutoSize));
}
receptorGridContent.Add(new ColumnInputReceptor { Action = { BindTarget = column.Action } });
receptorGridDimensions.Add(new Dimension());
first = false;
}
}
InternalChild = gridContainer = new GridContainer
{
RelativeSizeAxes = Axes.Both,
AlwaysPresent = true,
Content = new[] { receptorGridContent.ToArray() },
ColumnDimensions = receptorGridDimensions.ToArray()
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Opacity.BindValueChanged(o => Alpha = o.NewValue, true);
}
protected override bool OnKeyDown(KeyDownEvent e)
{
// Hide whenever the keyboard is used.
Hide();
return false;
}
protected override bool OnTouchDown(TouchDownEvent e)
{
Show();
return true;
}
protected override void PopIn()
{
gridContainer.FadeIn(500, Easing.OutQuint);
}
protected override void PopOut()
{
gridContainer.FadeOut(300);
}
public partial class ColumnInputReceptor : CompositeDrawable
{
public readonly IBindable<ManiaAction> Action = new Bindable<ManiaAction>();
private readonly Box highlightOverlay;
[Resolved]
private ManiaInputManager? inputManager { get; set; }
private bool isPressed;
public ColumnInputReceptor()
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 10,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.15f,
},
highlightOverlay = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Blending = BlendingParameters.Additive,
}
}
}
};
}
protected override bool OnTouchDown(TouchDownEvent e)
{
updateButton(true);
return false; // handled by parent container to show overlay.
}
protected override void OnTouchUp(TouchUpEvent e)
{
updateButton(false);
}
private void updateButton(bool press)
{
if (press == isPressed)
return;
isPressed = press;
if (press)
{
inputManager?.KeyBindingContainer.TriggerPressed(Action.Value);
highlightOverlay.FadeTo(0.1f, 80, Easing.OutQuint);
}
else
{
inputManager?.KeyBindingContainer.TriggerReleased(Action.Value);
highlightOverlay.FadeTo(0, 400, Easing.OutQuint);
}
}
}
private partial class Gutter : Drawable
{
public readonly IBindable<float> Spacing = new Bindable<float>();
public Gutter()
{
Spacing.BindValueChanged(s => Size = new Vector2(s.NewValue));
}
}
}
}

View File

@ -103,12 +103,13 @@ namespace osu.Game.Rulesets.Mania.UI
Width = 1366, // Bar lines should only be masked on the vertical axis Width = 1366, // Bar lines should only be masked on the vertical axis
BypassAutoSizeAxes = Axes.Both, BypassAutoSizeAxes = Axes.Both,
Masking = true, Masking = true,
Child = barLineContainer = new HitObjectArea(HitObjectContainer) Child = barLineContainer = new HitPositionPaddedContainer
{ {
Name = "Bar lines", Name = "Bar lines",
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Child = HitObjectContainer,
} }
}, },
columnFlow = new ColumnFlow<Column>(definition) columnFlow = new ColumnFlow<Column>(definition)
@ -119,12 +120,13 @@ namespace osu.Game.Rulesets.Mania.UI
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}, },
judgements = new JudgementContainer<DrawableManiaJudgement> new HitPositionPaddedContainer
{ {
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Y = HIT_TARGET_POSITION + 150 Child = judgements = new JudgementContainer<DrawableManiaJudgement>
{
RelativeSizeAxes = Axes.Both,
},
}, },
topLevelContainer = new Container { RelativeSizeAxes = Axes.Both } topLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
} }
@ -218,7 +220,7 @@ namespace osu.Game.Rulesets.Mania.UI
{ {
j.Apply(result, judgedObject); j.Apply(result, judgedObject);
j.Anchor = Anchor.Centre; j.Anchor = Anchor.BottomCentre;
j.Origin = Anchor.Centre; j.Origin = Anchor.Centre;
})!); })!);
} }

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
public partial class GridPlacementBlueprint : PlacementBlueprint public partial class GridPlacementBlueprint : PlacementBlueprint
{ {
[Resolved] [Resolved]
private HitObjectComposer? hitObjectComposer { get; set; } private OsuHitObjectComposer? hitObjectComposer { get; set; }
private OsuGridToolboxGroup gridToolboxGroup = null!; private OsuGridToolboxGroup gridToolboxGroup = null!;
private Vector2 originalOrigin; private Vector2 originalOrigin;
@ -95,12 +95,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
base.OnDragEnd(e); base.OnDragEnd(e);
} }
public override SnapType SnapType => ~SnapType.GlobalGrids; public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
public override void UpdateTimeAndPosition(SnapResult result)
{ {
if (State.Value == Visibility.Hidden) if (State.Value == Visibility.Hidden)
return; return new SnapResult(screenSpacePosition, fallbackTime);
var result = hitObjectComposer?.TrySnapToNearbyObjects(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
var pos = ToLocalSpace(result.ScreenSpacePosition); var pos = ToLocalSpace(result.ScreenSpacePosition);
@ -120,6 +120,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
gridToolboxGroup.SetGridFromPoints(gridToolboxGroup.StartPosition.Value, pos); gridToolboxGroup.SetGridFromPoints(gridToolboxGroup.StartPosition.Value, pos);
} }
} }
return result;
} }
protected override void PopOut() protected override void PopOut()

View File

@ -1,10 +1,15 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
@ -15,12 +20,26 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
private readonly HitCirclePiece circlePiece; private readonly HitCirclePiece circlePiece;
[Resolved]
private OsuHitObjectComposer? composer { get; set; }
[Resolved]
private EditorClock? editorClock { get; set; }
private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
public HitCirclePlacementBlueprint() public HitCirclePlacementBlueprint()
: base(new HitCircle()) : base(new HitCircle())
{ {
InternalChild = circlePiece = new HitCirclePiece(); InternalChild = circlePiece = new HitCirclePiece();
} }
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
limitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
@ -45,10 +64,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
return base.OnMouseDown(e); return base.OnMouseDown(e);
} }
public override void UpdateTimeAndPosition(SnapResult result) public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
{ {
base.UpdateTimeAndPosition(result); var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime);
result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition, limitedDistanceSnap.Value && editorClock != null ? editorClock.CurrentTime : null);
if (composer?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? screenSpacePosition, result?.Time ?? fallbackTime) is SnapResult gridSnapResult)
result = gridSnapResult;
result ??= new SnapResult(screenSpacePosition, fallbackTime);
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
return result;
} }
} }
} }

View File

@ -9,6 +9,7 @@ using System.Collections.Specialized;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using Humanizer; using Humanizer;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -20,6 +21,7 @@ using osu.Framework.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -48,11 +50,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public Action<List<PathControlPoint>> SplitControlPointsRequested; public Action<List<PathControlPoint>> SplitControlPointsRequested;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IPositionSnapProvider positionSnapProvider { get; set; } [CanBeNull]
private OsuHitObjectComposer positionSnapProvider { get; set; }
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IDistanceSnapProvider distanceSnapProvider { get; set; } private IDistanceSnapProvider distanceSnapProvider { get; set; }
private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
public PathControlPointVisualiser(T hitObject, bool allowSelection) public PathControlPointVisualiser(T hitObject, bool allowSelection)
{ {
this.hitObject = hitObject; this.hitObject = hitObject;
@ -67,6 +72,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}; };
} }
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
limitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
@ -433,12 +444,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{ {
// Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account // Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account
Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex]));
SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(newHeadPosition);
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position; var result = positionSnapProvider?.TrySnapToNearbyObjects(newHeadPosition, oldStartTime);
result ??= positionSnapProvider?.TrySnapToDistanceGrid(newHeadPosition, limitedDistanceSnap.Value ? oldStartTime : null);
if (positionSnapProvider?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? newHeadPosition, result?.Time ?? oldStartTime) is SnapResult gridSnapResult)
result = gridSnapResult;
result ??= new SnapResult(newHeadPosition, oldStartTime);
Vector2 movementDelta = Parent!.ToLocalSpace(result.ScreenSpacePosition) - hitObject.Position;
hitObject.Position += movementDelta; hitObject.Position += movementDelta;
hitObject.StartTime = result?.Time ?? hitObject.StartTime; hitObject.StartTime = result.Time ?? hitObject.StartTime;
for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++) for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++)
{ {
@ -453,7 +469,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
} }
else else
{ {
SnapResult result = positionSnapProvider?.FindSnappedPositionAndTime(Parent!.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids); SnapResult result = positionSnapProvider?.TrySnapToPositionGrid(Parent!.ToScreenSpace(e.MousePosition));
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position; Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position;

View File

@ -5,10 +5,12 @@ 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.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
@ -25,6 +27,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{ {
public new Slider HitObject => (Slider)base.HitObject; public new Slider HitObject => (Slider)base.HitObject;
[Resolved]
private OsuHitObjectComposer? composer { get; set; }
private SliderBodyPiece bodyPiece = null!; private SliderBodyPiece bodyPiece = null!;
private HitCirclePiece headCirclePiece = null!; private HitCirclePiece headCirclePiece = null!;
private HitCirclePiece tailCirclePiece = null!; private HitCirclePiece tailCirclePiece = null!;
@ -40,15 +45,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private int currentSegmentLength; private int currentSegmentLength;
private bool usingCustomSegmentType; private bool usingCustomSegmentType;
[Resolved]
private IPositionSnapProvider? positionSnapProvider { get; set; }
[Resolved] [Resolved]
private IDistanceSnapProvider? distanceSnapProvider { get; set; } private IDistanceSnapProvider? distanceSnapProvider { get; set; }
[Resolved] [Resolved]
private FreehandSliderToolboxGroup? freehandToolboxGroup { get; set; } private FreehandSliderToolboxGroup? freehandToolboxGroup { get; set; }
[Resolved]
private EditorClock? editorClock { get; set; }
private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 }; private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
protected override bool IsValidForPlacement => HitObject.Path.HasValidLength; protected override bool IsValidForPlacement => HitObject.Path.HasValidLength;
@ -63,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(OsuConfigManager config)
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
@ -74,6 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}; };
state = SliderPlacementState.Initial; state = SliderPlacementState.Initial;
limitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -106,9 +114,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
[Resolved] [Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!; private EditorBeatmap editorBeatmap { get; set; } = null!;
public override void UpdateTimeAndPosition(SnapResult result) public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
{ {
base.UpdateTimeAndPosition(result); var result = composer?.TrySnapToNearbyObjects(screenSpacePosition, fallbackTime);
result ??= composer?.TrySnapToDistanceGrid(screenSpacePosition, limitedDistanceSnap.Value && editorClock != null ? editorClock.CurrentTime : null);
if (composer?.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? screenSpacePosition, result?.Time ?? fallbackTime) is SnapResult gridSnapResult)
result = gridSnapResult;
result ??= new SnapResult(screenSpacePosition, fallbackTime);
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
switch (state) switch (state)
{ {
@ -131,6 +145,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
updateCursor(); updateCursor();
break; break;
} }
return result;
} }
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)
@ -375,7 +391,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private Vector2 getCursorPosition() private Vector2 getCursorPosition()
{ {
var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.ControlPoints ? SnapType.GlobalGrids : SnapType.All); SnapResult? result = null;
var mousePosition = inputManager.CurrentState.Mouse.Position;
if (state != SliderPlacementState.ControlPoints)
{
result ??= composer?.TrySnapToNearbyObjects(mousePosition);
result ??= composer?.TrySnapToDistanceGrid(mousePosition);
}
result ??= composer?.TrySnapToPositionGrid(mousePosition);
return ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position; return ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position;
} }

View File

@ -1,6 +1,12 @@
// 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 System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
@ -8,16 +14,27 @@ using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners; using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit namespace osu.Game.Rulesets.Osu.Edit
{ {
public partial class OsuBlueprintContainer : ComposeBlueprintContainer public partial class OsuBlueprintContainer : ComposeBlueprintContainer
{ {
public OsuBlueprintContainer(HitObjectComposer composer) private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
public new OsuHitObjectComposer Composer => (OsuHitObjectComposer)base.Composer;
public OsuBlueprintContainer(OsuHitObjectComposer composer)
: base(composer) : base(composer)
{ {
} }
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
limitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
}
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new OsuSelectionHandler(); protected override SelectionHandler<HitObject> CreateSelectionHandler() => new OsuSelectionHandler();
public override HitObjectSelectionBlueprint? CreateHitObjectBlueprintFor(HitObject hitObject) public override HitObjectSelectionBlueprint? CreateHitObjectBlueprintFor(HitObject hitObject)
@ -36,5 +53,68 @@ namespace osu.Game.Rulesets.Osu.Edit
return base.CreateHitObjectBlueprintFor(hitObject); return base.CreateHitObjectBlueprintFor(hitObject);
} }
protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<HitObject> blueprint, Vector2[] originalSnapPositions)> blueprints)
{
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
for (int i = 0; i < blueprints.Count; i++)
{
if (checkSnappingBlueprintToNearbyObjects(blueprints[i].blueprint, distanceTravelled, blueprints[i].originalSnapPositions))
return true;
}
// if no positional snapping could be performed, try unrestricted snapping from the earliest
// item in the selection.
// The final movement position, relative to movementBlueprintOriginalPosition.
Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled;
var referenceBlueprint = blueprints.First().blueprint;
// Retrieve a snapped position.
var result = Composer.TrySnapToNearbyObjects(movePosition);
result ??= Composer.TrySnapToDistanceGrid(movePosition, limitedDistanceSnap.Value ? referenceBlueprint.Item.StartTime : null);
if (Composer.TrySnapToPositionGrid(result?.ScreenSpacePosition ?? movePosition, result?.Time) is SnapResult gridSnapResult)
result = gridSnapResult;
result ??= new SnapResult(movePosition, null);
bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint));
if (moved)
ApplySnapResultTime(result, referenceBlueprint.Item.StartTime);
return moved;
}
/// <summary>
/// Check for positional snap for given blueprint.
/// </summary>
/// <param name="blueprint">The blueprint to check for snapping.</param>
/// <param name="distanceTravelled">Distance travelled since start of dragging action.</param>
/// <param name="originalPositions">The snap positions of blueprint before start of dragging action.</param>
/// <returns>Whether an object to snap to was found.</returns>
private bool checkSnappingBlueprintToNearbyObjects(SelectionBlueprint<HitObject> blueprint, Vector2 distanceTravelled, Vector2[] originalPositions)
{
var currentPositions = blueprint.ScreenSpaceSnapPoints;
for (int i = 0; i < originalPositions.Length; i++)
{
Vector2 originalPosition = originalPositions[i];
var testPosition = originalPosition + distanceTravelled;
var positionalResult = Composer.TrySnapToNearbyObjects(testPosition);
if (positionalResult == null || positionalResult.ScreenSpacePosition == testPosition) continue;
var delta = positionalResult.ScreenSpacePosition - currentPositions[i];
// attempt to move the objects, and apply any time based snapping if we can.
if (SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(blueprint, delta)))
{
ApplySnapResultTime(positionalResult, blueprint.Item.StartTime);
return true;
}
}
return false;
}
} }
} }

View File

@ -7,6 +7,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Caching; using osu.Framework.Caching;
@ -31,6 +32,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Edit namespace osu.Game.Rulesets.Osu.Edit
{ {
[Cached]
public partial class OsuHitObjectComposer : HitObjectComposer<OsuHitObject> public partial class OsuHitObjectComposer : HitObjectComposer<OsuHitObject>
{ {
public OsuHitObjectComposer(Ruleset ruleset) public OsuHitObjectComposer(Ruleset ruleset)
@ -178,7 +180,7 @@ namespace osu.Game.Rulesets.Osu.Edit
return; return;
List<OsuHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<OsuHitObject>().Where(h => h.StartTime >= timestamp).ToList(); List<OsuHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<OsuHitObject>().Where(h => h.StartTime >= timestamp).ToList();
string[] splitDescription = objectDescription.Split(',').ToArray(); string[] splitDescription = objectDescription.Split(',');
for (int i = 0; i < splitDescription.Length; i++) for (int i = 0; i < splitDescription.Length; i++)
{ {
@ -222,56 +224,56 @@ namespace osu.Game.Rulesets.Osu.Edit
} }
} }
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All) [CanBeNull]
public SnapResult TrySnapToNearbyObjects(Vector2 screenSpacePosition, double? fallbackTime = null)
{ {
if (snapType.HasFlag(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) if (!snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
{ return null;
// In the case of snapping to nearby objects, a time value is not provided.
// This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap
// this could result in unexpected behaviour when distance snapping is turned on and a user attempts to place an object that is
// BOTH on a valid distance snap ring, and also at the same position as a previous object.
//
// We want to ensure that in this particular case, the time-snapping component of distance snap is still applied.
// The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over
// the time value if the proposed positions are roughly the same.
if (snapType.HasFlag(SnapType.RelativeGrids) && DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{
(Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition));
if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1))
snapResult.Time = distanceSnappedTime;
}
if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null)
return snapResult; return snapResult;
}
SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType); // In the case of snapping to nearby objects, a time value is not provided.
// This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap
// this could result in unexpected behaviour when distance snapping is turned on and a user attempts to place an object that is
// BOTH on a valid distance snap ring, and also at the same position as a previous object.
//
// We want to ensure that in this particular case, the time-snapping component of distance snap is still applied.
// The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over
// the time value if the proposed positions are roughly the same.
(Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition));
snapResult.Time = Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1)
? distanceSnappedTime
: fallbackTime;
if (snapType.HasFlag(SnapType.RelativeGrids)) return snapResult;
{ }
if (DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
result.ScreenSpacePosition = distanceSnapGrid.ToScreenSpace(pos); [CanBeNull]
result.Time = time; public SnapResult TrySnapToDistanceGrid(Vector2 screenSpacePosition, double? fixedTime = null)
} {
} if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null)
return null;
if (snapType.HasFlag(SnapType.GlobalGrids)) var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
{ (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition), fixedTime);
if (rectangularGridSnapToggle.Value == TernaryState.True) return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, playfield);
{ }
Vector2 pos = positionSnapGrid.GetSnappedPosition(positionSnapGrid.ToLocalSpace(result.ScreenSpacePosition));
// A grid which doesn't perfectly fit the playfield can produce a position that is outside of the playfield. [CanBeNull]
// We need to clamp the position to the playfield bounds to ensure that the snapped position is always in bounds. public SnapResult TrySnapToPositionGrid(Vector2 screenSpacePosition, double? fallbackTime = null)
pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE); {
if (rectangularGridSnapToggle.Value != TernaryState.True)
return null;
result.ScreenSpacePosition = positionSnapGrid.ToScreenSpace(pos); Vector2 pos = positionSnapGrid.GetSnappedPosition(positionSnapGrid.ToLocalSpace(screenSpacePosition));
}
}
return result; // A grid which doesn't perfectly fit the playfield can produce a position that is outside of the playfield.
// We need to clamp the position to the playfield bounds to ensure that the snapped position is always in bounds.
pos = Vector2.Clamp(pos, Vector2.Zero, OsuPlayfield.BASE_SIZE);
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
return new SnapResult(positionSnapGrid.ToScreenSpace(pos), fallbackTime, playfield);
} }
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult) private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)

View File

@ -0,0 +1,186 @@
// 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.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Input.Events;
using osu.Game.Extensions;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
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.Utils;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class PreciseMovementPopover : OsuPopover
{
[Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!;
private readonly Dictionary<HitObject, Vector2> initialPositions = new Dictionary<HitObject, Vector2>();
private RectangleF initialSurroundingQuad;
private BindableNumber<float> xBindable = null!;
private BindableNumber<float> yBindable = null!;
private SliderWithTextBoxInput<float> xInput = null!;
private OsuCheckbox relativeCheckbox = null!;
public PreciseMovementPopover()
{
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
}
[BackgroundDependencyLoader]
private void load()
{
Child = new FillFlowContainer
{
Width = 220,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(20),
Children = new Drawable[]
{
xInput = new SliderWithTextBoxInput<float>("X:")
{
Current = xBindable = new BindableNumber<float>
{
Precision = 1,
},
Instantaneous = true,
TabbableContentContainer = this,
},
new SliderWithTextBoxInput<float>("Y:")
{
Current = yBindable = new BindableNumber<float>
{
Precision = 1,
},
Instantaneous = true,
TabbableContentContainer = this,
},
relativeCheckbox = new OsuCheckbox(false)
{
RelativeSizeAxes = Axes.X,
LabelText = "Relative movement",
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
ScheduleAfterChildren(() => xInput.TakeFocus());
}
protected override void PopIn()
{
base.PopIn();
editorBeatmap.BeginChange();
initialPositions.AddRange(editorBeatmap.SelectedHitObjects.Where(ho => ho is not Spinner).Select(ho => new KeyValuePair<HitObject, Vector2>(ho, ((IHasPosition)ho).Position)));
initialSurroundingQuad = GeometryUtils.GetSurroundingQuad(initialPositions.Keys.Cast<IHasPosition>()).AABBFloat;
Debug.Assert(initialPositions.Count > 0);
if (initialPositions.Count > 1)
{
relativeCheckbox.Current.Value = true;
relativeCheckbox.Current.Disabled = true;
}
relativeCheckbox.Current.BindValueChanged(_ => relativeChanged(), true);
xBindable.BindValueChanged(_ => applyPosition());
yBindable.BindValueChanged(_ => applyPosition());
}
protected override void PopOut()
{
base.PopOut();
if (IsLoaded) editorBeatmap.EndChange();
}
private void relativeChanged()
{
// reset bindable bounds to something that is guaranteed to be larger than any previous value.
// this prevents crashes that can happen in the middle of changing the bounds, as updating both bound ends at the same is not atomic -
// if the old and new bounds are disjoint, assigning X first can produce a situation where MinValue > MaxValue.
(xBindable.MinValue, xBindable.MaxValue) = (float.MinValue, float.MaxValue);
(yBindable.MinValue, yBindable.MaxValue) = (float.MinValue, float.MaxValue);
float previousX = xBindable.Value;
float previousY = yBindable.Value;
if (relativeCheckbox.Current.Value)
{
(xBindable.MinValue, xBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.X, OsuPlayfield.BASE_SIZE.X - initialSurroundingQuad.BottomRight.X);
(yBindable.MinValue, yBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - initialSurroundingQuad.BottomRight.Y);
xBindable.Default = yBindable.Default = 0;
if (initialPositions.Count == 1)
{
var initialPosition = initialPositions.Single().Value;
xBindable.Value = previousX - initialPosition.X;
yBindable.Value = previousY - initialPosition.Y;
}
}
else
{
Debug.Assert(initialPositions.Count == 1);
var initialPosition = initialPositions.Single().Value;
var quadRelativeToPosition = new RectangleF(initialSurroundingQuad.Location - initialPosition, initialSurroundingQuad.Size);
(xBindable.MinValue, xBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.X, OsuPlayfield.BASE_SIZE.X - quadRelativeToPosition.BottomRight.X);
(yBindable.MinValue, yBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - quadRelativeToPosition.BottomRight.Y);
xBindable.Default = initialPosition.X;
yBindable.Default = initialPosition.Y;
xBindable.Value = xBindable.Default + previousX;
yBindable.Value = yBindable.Default + previousY;
}
}
private void applyPosition()
{
editorBeatmap.PerformOnSelection(ho =>
{
if (!initialPositions.TryGetValue(ho, out var initialPosition))
return;
var pos = new Vector2(xBindable.Value, yBindable.Value);
if (relativeCheckbox.Current.Value)
((IHasPosition)ho).Position = initialPosition + pos;
else
((IHasPosition)ho).Position = pos;
});
}
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Action == GlobalAction.Select && !e.Repeat)
{
this.HidePopover();
return true;
}
return base.OnPressed(e);
}
}
}

View File

@ -96,11 +96,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
base.LoadComplete(); base.LoadComplete();
ScheduleAfterChildren(() => ScheduleAfterChildren(() => angleInput.TakeFocus());
{
angleInput.TakeFocus();
angleInput.SelectAll();
});
angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue }); angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue });
rotationHandler.CanRotateAroundSelectionOrigin.BindValueChanged(e => rotationHandler.CanRotateAroundSelectionOrigin.BindValueChanged(e =>

View File

@ -139,11 +139,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
base.LoadComplete(); base.LoadComplete();
ScheduleAfterChildren(() => ScheduleAfterChildren(() => scaleInput.TakeFocus());
{
scaleInput.TakeFocus();
scaleInput.SelectAll();
});
scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue }); scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue });
xCheckBox.Current.BindValueChanged(_ => xCheckBox.Current.BindValueChanged(_ =>

View File

@ -1,6 +1,7 @@
// 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 System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -10,6 +11,9 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK; using osuTK;
@ -18,9 +22,12 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler<GlobalAction> public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler<GlobalAction>
{ {
private readonly BindableList<HitObject> selectedHitObjects = new BindableList<HitObject>();
private readonly BindableBool canMove = new BindableBool();
private readonly AggregateBindable<bool> canRotate = new AggregateBindable<bool>((x, y) => x || y); private readonly AggregateBindable<bool> canRotate = new AggregateBindable<bool>((x, y) => x || y);
private readonly AggregateBindable<bool> canScale = new AggregateBindable<bool>((x, y) => x || y); private readonly AggregateBindable<bool> canScale = new AggregateBindable<bool>((x, y) => x || y);
private EditorToolButton moveButton = null!;
private EditorToolButton rotateButton = null!; private EditorToolButton rotateButton = null!;
private EditorToolButton scaleButton = null!; private EditorToolButton scaleButton = null!;
@ -35,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Edit
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(EditorBeatmap editorBeatmap)
{ {
Child = new FillFlowContainer Child = new FillFlowContainer
{ {
@ -44,20 +51,27 @@ namespace osu.Game.Rulesets.Osu.Edit
Spacing = new Vector2(5), Spacing = new Vector2(5),
Children = new Drawable[] Children = new Drawable[]
{ {
moveButton = new EditorToolButton("Move",
() => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt },
() => new PreciseMovementPopover()),
rotateButton = new EditorToolButton("Rotate", rotateButton = new EditorToolButton("Rotate",
() => new SpriteIcon { Icon = FontAwesome.Solid.Undo }, () => new SpriteIcon { Icon = FontAwesome.Solid.Undo },
() => new PreciseRotationPopover(RotationHandler, GridToolbox)), () => new PreciseRotationPopover(RotationHandler, GridToolbox)),
scaleButton = new EditorToolButton("Scale", scaleButton = new EditorToolButton("Scale",
() => new SpriteIcon { Icon = FontAwesome.Solid.ArrowsAlt }, () => new SpriteIcon { Icon = FontAwesome.Solid.ExpandArrowsAlt },
() => new PreciseScalePopover(ScaleHandler, GridToolbox)) () => new PreciseScalePopover(ScaleHandler, GridToolbox))
} }
}; };
selectedHitObjects.BindTo(editorBeatmap.SelectedHitObjects);
} }
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
selectedHitObjects.BindCollectionChanged((_, _) => canMove.Value = selectedHitObjects.Any(ho => ho is not Spinner), true);
canRotate.AddSource(RotationHandler.CanRotateAroundPlayfieldOrigin); canRotate.AddSource(RotationHandler.CanRotateAroundPlayfieldOrigin);
canRotate.AddSource(RotationHandler.CanRotateAroundSelectionOrigin); canRotate.AddSource(RotationHandler.CanRotateAroundSelectionOrigin);
@ -67,6 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit
// bindings to `Enabled` on the buttons are decoupled on purpose // bindings to `Enabled` on the buttons are decoupled on purpose
// due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set. // due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set.
canMove.BindValueChanged(move => moveButton.Enabled.Value = move.NewValue, true);
canRotate.Result.BindValueChanged(rotate => rotateButton.Enabled.Value = rotate.NewValue, true); canRotate.Result.BindValueChanged(rotate => rotateButton.Enabled.Value = rotate.NewValue, true);
canScale.Result.BindValueChanged(scale => scaleButton.Enabled.Value = scale.NewValue, true); canScale.Result.BindValueChanged(scale => scaleButton.Enabled.Value = scale.NewValue, true);
} }
@ -77,6 +92,12 @@ namespace osu.Game.Rulesets.Osu.Edit
switch (e.Action) switch (e.Action)
{ {
case GlobalAction.EditorToggleMoveControl:
{
moveButton.TriggerClick();
return true;
}
case GlobalAction.EditorToggleRotateControl: case GlobalAction.EditorToggleRotateControl:
{ {
if (!RotationHandler.OperationInProgress.Value || rotateButton.Selected.Value) if (!RotationHandler.OperationInProgress.Value || rotateButton.Selected.Value)

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override LocalisableString Description => "Flip objects on the chosen axes."; public override LocalisableString Description => "Flip objects on the chosen axes.";
public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) }; public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) };
[SettingSource("Mirrored axes", "Choose which axes objects are mirrored over.")] [SettingSource("Flipped axes")]
public Bindable<MirrorType> Reflection { get; } = new Bindable<MirrorType>(); public Bindable<MirrorType> Reflection { get; } = new Bindable<MirrorType>();
public void ApplyToHitObject(HitObject hitObject) public void ApplyToHitObject(HitObject hitObject)

View File

@ -6,6 +6,7 @@ using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
@ -70,12 +71,24 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
} }
var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault(); var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
Vector2 pos = new Vector2();
if (combo != null) if (combo != null)
{ {
combo.Anchor = Anchor.BottomLeft; combo.Anchor = Anchor.BottomLeft;
combo.Origin = Anchor.BottomLeft; combo.Origin = Anchor.BottomLeft;
combo.Scale = new Vector2(1.28f); combo.Scale = new Vector2(1.28f);
pos += new Vector2(10, -(combo.DrawHeight * 1.56f + 20) * combo.Scale.X);
}
if (spectatorList != null)
{
spectatorList.Anchor = Anchor.BottomLeft;
spectatorList.Origin = Anchor.BottomLeft;
spectatorList.Position = pos;
} }
}) })
{ {
@ -83,6 +96,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
new LegacyDefaultComboCounter(), new LegacyDefaultComboCounter(),
new LegacyKeyCounterDisplay(), new LegacyKeyCounterDisplay(),
new SpectatorList(),
} }
}; };
} }

View File

@ -1,6 +1,7 @@
// 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.Allocation;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
@ -16,6 +17,9 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
public new Hit HitObject => (Hit)base.HitObject; public new Hit HitObject => (Hit)base.HitObject;
[Resolved]
private TaikoHitObjectComposer? composer { get; set; }
public HitPlacementBlueprint() public HitPlacementBlueprint()
: base(new Hit()) : base(new Hit())
{ {
@ -40,10 +44,12 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
return true; return true;
} }
public override void UpdateTimeAndPosition(SnapResult result) public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
{ {
var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
piece.Position = ToLocalSpace(result.ScreenSpacePosition); piece.Position = ToLocalSpace(result.ScreenSpacePosition);
base.UpdateTimeAndPosition(result); base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
return result;
} }
} }
} }

View File

@ -1,9 +1,8 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils; using osu.Framework.Utils;
@ -26,12 +25,15 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
private readonly IHasDuration spanPlacementObject; private readonly IHasDuration spanPlacementObject;
[Resolved]
private TaikoHitObjectComposer? composer { get; set; }
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(spanPlacementObject.Duration, 0); protected override bool IsValidForPlacement => Precision.DefinitelyBigger(spanPlacementObject.Duration, 0);
public TaikoSpanPlacementBlueprint(HitObject hitObject) public TaikoSpanPlacementBlueprint(HitObject hitObject)
: base(hitObject) : base(hitObject)
{ {
spanPlacementObject = hitObject as IHasDuration; spanPlacementObject = (hitObject as IHasDuration)!;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -79,9 +81,11 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
EndPlacement(true); EndPlacement(true);
} }
public override void UpdateTimeAndPosition(SnapResult result) public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
{ {
base.UpdateTimeAndPosition(result); var result = composer?.FindSnappedPositionAndTime(screenSpacePosition) ?? new SnapResult(screenSpacePosition, fallbackTime);
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
if (PlacementActive == PlacementState.Active) if (PlacementActive == PlacementState.Active)
{ {
@ -116,6 +120,8 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
originalPosition = ToLocalSpace(result.ScreenSpacePosition); originalPosition = ToLocalSpace(result.ScreenSpacePosition);
} }
} }
return result;
} }
} }
} }

View File

@ -1,16 +1,22 @@
// 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 System.Collections.Generic;
using System.Linq;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Taiko.Edit.Blueprints; using osu.Game.Rulesets.Taiko.Edit.Blueprints;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Taiko.Edit namespace osu.Game.Rulesets.Taiko.Edit
{ {
public partial class TaikoBlueprintContainer : ComposeBlueprintContainer public partial class TaikoBlueprintContainer : ComposeBlueprintContainer
{ {
public TaikoBlueprintContainer(HitObjectComposer composer) public new TaikoHitObjectComposer Composer => (TaikoHitObjectComposer)base.Composer;
public TaikoBlueprintContainer(TaikoHitObjectComposer composer)
: base(composer) : base(composer)
{ {
} }
@ -19,5 +25,22 @@ namespace osu.Game.Rulesets.Taiko.Edit
public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) => public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) =>
new TaikoSelectionBlueprint(hitObject); new TaikoSelectionBlueprint(hitObject);
protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<HitObject> blueprint, Vector2[] originalSnapPositions)> blueprints)
{
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
// The final movement position, relative to movementBlueprintOriginalPosition.
Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled;
// Retrieve a snapped position.
var result = Composer.FindSnappedPositionAndTime(movePosition);
var referenceBlueprint = blueprints.First().blueprint;
bool moved = SelectionHandler.HandleMovement(new MoveSelectionEvent<HitObject>(referenceBlueprint, result.ScreenSpacePosition - referenceBlueprint.ScreenSpaceSelectionPoint));
if (moved)
ApplySnapResultTime(result, referenceBlueprint.Item.StartTime);
return moved;
}
} }
} }

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.Allocation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;
@ -12,6 +13,7 @@ using osu.Game.Screens.Edit.Compose.Components;
namespace osu.Game.Rulesets.Taiko.Edit namespace osu.Game.Rulesets.Taiko.Edit
{ {
[Cached]
public partial class TaikoHitObjectComposer : ScrollingHitObjectComposer<TaikoHitObject> public partial class TaikoHitObjectComposer : ScrollingHitObjectComposer<TaikoHitObject>
{ {
protected override bool ApplyHorizontalCentering => false; protected override bool ApplyHorizontalCentering => false;

View File

@ -36,6 +36,10 @@ namespace osu.Game.Tests.NonVisual.Ranking
.Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null)) .Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null))
.ToList(); .ToList();
// Add some red herrings
events.Insert(4, new HitEvent(200, 1.0, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null));
events.Insert(8, new HitEvent(-100, 1.0, HitResult.Miss, new HitObject(), null, null));
HitEventExtensions.UnstableRateCalculationResult result = null; HitEventExtensions.UnstableRateCalculationResult result = null;
for (int i = 0; i < events.Count; i++) for (int i = 0; i < events.Count; i++)
@ -57,6 +61,10 @@ namespace osu.Game.Tests.NonVisual.Ranking
.Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null)) .Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null))
.ToList(); .ToList();
// Add some red herrings
events.Insert(4, new HitEvent(200, 1.0, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null));
events.Insert(8, new HitEvent(-100, 1.0, HitResult.Miss, new HitObject(), null, null));
HitEventExtensions.UnstableRateCalculationResult result = null; HitEventExtensions.UnstableRateCalculationResult result = null;
for (int i = 0; i < events.Count; i++) for (int i = 0; i < events.Count; i++)

View File

@ -71,6 +71,8 @@ namespace osu.Game.Tests.Skins
"Archives/modified-classic-20240724.osk", "Archives/modified-classic-20240724.osk",
// Covers skinnable mod display // Covers skinnable mod display
"Archives/modified-default-20241207.osk", "Archives/modified-default-20241207.osk",
// Covers skinnable spectator list
"Archives/modified-argon-20250116.osk",
}; };
/// <summary> /// <summary>

View File

@ -181,7 +181,7 @@ namespace osu.Game.Tests.Visual.Editing
} }
} }
public override (Vector2 position, double time) GetSnappedPosition(Vector2 screenSpacePosition) public override (Vector2 position, double time) GetSnappedPosition(Vector2 screenSpacePosition, double? fixedTime = null)
=> (Vector2.Zero, 0); => (Vector2.Zero, 0);
} }

View File

@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1); AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1);
AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == newTime); AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == EditorBeatmap.SnapTime(newTime, null));
} }
[Test] [Test]
@ -122,6 +122,8 @@ namespace osu.Game.Tests.Visual.Editing
[TestCase(true)] [TestCase(true)]
public void TestCopyPaste(bool deselectAfterCopy) public void TestCopyPaste(bool deselectAfterCopy)
{ {
const int paste_time = 2000;
var addedObject = new HitCircle { StartTime = 1000 }; var addedObject = new HitCircle { StartTime = 1000 };
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
@ -130,7 +132,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("copy hitobject", () => Editor.Copy()); AddStep("copy hitobject", () => Editor.Copy());
AddStep("move forward in time", () => EditorClock.Seek(2000)); AddStep("move forward in time", () => EditorClock.Seek(paste_time));
if (deselectAfterCopy) if (deselectAfterCopy)
{ {
@ -144,7 +146,7 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("are two objects", () => EditorBeatmap.HitObjects.Count == 2); AddAssert("are two objects", () => EditorBeatmap.HitObjects.Count == 2);
AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == 2000); AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == EditorBeatmap.SnapTime(paste_time, null));
AddUntilStep("timeline selection box is visible", () => Editor.ChildrenOfType<Timeline>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha > 0); AddUntilStep("timeline selection box is visible", () => Editor.ChildrenOfType<Timeline>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha > 0);
AddUntilStep("composer selection box is visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha > 0); AddUntilStep("composer selection box is visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha > 0);

View File

@ -0,0 +1,82 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Visual.Editing
{
public partial class TestSceneEditorClipboardSnapping : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
private const double beat_length = 60_000 / 180.0; // 180 bpm
private const double timing_point_time = 1500;
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(timing_point_time, new TimingControlPoint { BeatLength = beat_length });
return new TestBeatmap(ruleset, false)
{
ControlPointInfo = controlPointInfo
};
}
[TestCase(1)]
[TestCase(2)]
[TestCase(3)]
[TestCase(4)]
[TestCase(6)]
[TestCase(8)]
[TestCase(12)]
[TestCase(16)]
public void TestPasteSnapping(int divisor)
{
const double paste_time = timing_point_time + 1271; // arbitrary timestamp that doesn't snap to the timing point at any divisor
var addedObjects = new HitObject[]
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 1200 },
};
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
AddStep("select added objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects));
AddStep("copy hitobjects", () => Editor.Copy());
AddStep($"set beat divisor to 1/{divisor}", () =>
{
var beatDivisor = (BindableBeatDivisor)Editor.Dependencies.Get(typeof(BindableBeatDivisor));
beatDivisor.SetArbitraryDivisor(divisor);
});
AddStep("move forward in time", () => EditorClock.Seek(paste_time));
AddAssert("not at snapped time", () => EditorClock.CurrentTime != EditorBeatmap.SnapTime(EditorClock.CurrentTime, null));
AddStep("paste hitobjects", () => Editor.Paste());
AddAssert("first object is snapped", () => Precision.AlmostEquals(
EditorBeatmap.SelectedHitObjects.MinBy(h => h.StartTime)!.StartTime,
EditorBeatmap.ControlPointInfo.GetClosestSnappedTime(paste_time, divisor)
));
AddAssert("duration between pasted objects is same", () =>
{
var firstObject = EditorBeatmap.SelectedHitObjects.MinBy(h => h.StartTime)!;
var secondObject = EditorBeatmap.SelectedHitObjects.MaxBy(h => h.StartTime)!;
return Precision.AlmostEquals(secondObject.StartTime - firstObject.StartTime, addedObjects[1].StartTime - addedObjects[0].StartTime);
});
}
}
}

View File

@ -4,6 +4,7 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
@ -25,13 +26,16 @@ namespace osu.Game.Tests.Visual.Editing
[Test] [Test]
public void TestLocallyModifyingOnlineBeatmap() public void TestLocallyModifyingOnlineBeatmap()
{ {
string initialHash = string.Empty;
AddAssert("editor beatmap has online ID", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.GreaterThan(0)); AddAssert("editor beatmap has online ID", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.GreaterThan(0));
AddStep("store hash for later", () => initialHash = EditorBeatmap.BeatmapInfo.MD5Hash);
AddStep("delete first hitobject", () => EditorBeatmap.RemoveAt(0)); AddStep("delete first hitobject", () => EditorBeatmap.RemoveAt(0));
SaveEditor(); SaveEditor();
ReloadEditorToSameBeatmap(); ReloadEditorToSameBeatmap();
AddAssert("editor beatmap online ID reset", () => EditorBeatmap.BeatmapInfo.OnlineID, () => Is.EqualTo(-1)); AddAssert("beatmap marked as locally modified", () => EditorBeatmap.BeatmapInfo.Status, () => Is.EqualTo(BeatmapOnlineStatus.LocallyModified));
AddAssert("beatmap hash changed", () => EditorBeatmap.BeatmapInfo.MD5Hash, () => Is.Not.EqualTo(initialHash));
} }
} }
} }

View File

@ -120,6 +120,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public double FramesPerSecond => throw new NotImplementedException(); public double FramesPerSecond => throw new NotImplementedException();
public FrameTimeInfo TimeInfo => throw new NotImplementedException(); public FrameTimeInfo TimeInfo => throw new NotImplementedException();
public double StartTime => throw new NotImplementedException(); public double StartTime => throw new NotImplementedException();
public double GameplayStartTime => throw new NotImplementedException();
public IAdjustableAudioComponent AdjustmentsFromMods => adjustableAudioComponent; public IAdjustableAudioComponent AdjustmentsFromMods => adjustableAudioComponent;

View File

@ -6,48 +6,74 @@ using NUnit.Framework;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual.Spectator;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
[TestFixture] [TestFixture]
public partial class TestSceneSpectatorList : OsuTestScene public partial class TestSceneSpectatorList : OsuTestScene
{ {
private readonly BindableList<SpectatorList.Spectator> spectators = new BindableList<SpectatorList.Spectator>();
private readonly Bindable<LocalUserPlayingState> localUserPlayingState = new Bindable<LocalUserPlayingState>();
private int counter; private int counter;
[Test] [Test]
public void TestBasics() public void TestBasics()
{ {
SpectatorList list = null!; SpectatorList list = null!;
AddStep("create spectator list", () => Child = list = new SpectatorList Bindable<LocalUserPlayingState> playingState = new Bindable<LocalUserPlayingState>();
GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), healthProcessor: new OsuHealthProcessor(0), localUserPlayingState: playingState);
TestSpectatorClient client = new TestSpectatorClient();
AddStep("create spectator list", () =>
{ {
Anchor = Anchor.Centre, Children = new Drawable[]
Origin = Anchor.Centre, {
Spectators = { BindTarget = spectators }, client,
UserPlayingState = { BindTarget = localUserPlayingState } new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies =
[
(typeof(GameplayState), gameplayState),
(typeof(SpectatorClient), client)
],
Child = list = new SpectatorList
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}
};
}); });
AddStep("start playing", () => localUserPlayingState.Value = LocalUserPlayingState.Playing); AddStep("start playing", () => playingState.Value = LocalUserPlayingState.Playing);
AddRepeatStep("add a user", () => AddRepeatStep("add a user", () =>
{ {
int id = Interlocked.Increment(ref counter); int id = Interlocked.Increment(ref counter);
spectators.Add(new SpectatorList.Spectator(id, $"User {id}")); ((ISpectatorClient)client).UserStartedWatching([
new SpectatorUser
{
OnlineID = id,
Username = $"User {id}"
}
]);
}, 10); }, 10);
AddRepeatStep("remove random user", () => spectators.RemoveAt(RNG.Next(0, spectators.Count)), 5); AddRepeatStep("remove random user", () => ((ISpectatorClient)client).UserEndedWatching(client.WatchingUsers[RNG.Next(client.WatchingUsers.Count)].OnlineID), 5);
AddStep("change font to venera", () => list.Font.Value = Typeface.Venera); AddStep("change font to venera", () => list.Font.Value = Typeface.Venera);
AddStep("change font to torus", () => list.Font.Value = Typeface.Torus); AddStep("change font to torus", () => list.Font.Value = Typeface.Torus);
AddStep("change header colour", () => list.HeaderColour.Value = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1)); AddStep("change header colour", () => list.HeaderColour.Value = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1));
AddStep("enter break", () => localUserPlayingState.Value = LocalUserPlayingState.Break); AddStep("enter break", () => playingState.Value = LocalUserPlayingState.Break);
AddStep("stop playing", () => localUserPlayingState.Value = LocalUserPlayingState.NotPlaying); AddStep("stop playing", () => playingState.Value = LocalUserPlayingState.NotPlaying);
} }
} }
} }

View File

@ -48,6 +48,7 @@ using osu.Game.Screens.Select.Carousel;
using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Screens.Select.Options; using osu.Game.Screens.Select.Options;
using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Resources;
using osu.Game.Utils; using osu.Game.Utils;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -202,6 +203,38 @@ namespace osu.Game.Tests.Visual.Navigation
TextBox filterControlTextBox() => songSelect.ChildrenOfType<FilterControl.FilterControlTextBox>().Single(); TextBox filterControlTextBox() => songSelect.ChildrenOfType<FilterControl.FilterControlTextBox>().Single();
} }
[Test]
public void TestSongSelectRandomRewindButton()
{
Guid? originalSelection = null;
TestPlaySongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
AddStep("Add two beatmaps", () =>
{
Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo(8));
Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo(8));
});
AddUntilStep("wait for selected", () =>
{
originalSelection = Game.Beatmap.Value.BeatmapInfo.ID;
return !Game.Beatmap.IsDefault;
});
AddStep("hit random", () =>
{
InputManager.MoveMouseTo(Game.ChildrenOfType<FooterButtonRandom>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for selection changed", () => Game.Beatmap.Value.BeatmapInfo.ID, () => Is.Not.EqualTo(originalSelection));
AddStep("hit random rewind", () => InputManager.Click(MouseButton.Right));
AddUntilStep("wait for selection reverted", () => Game.Beatmap.Value.BeatmapInfo.ID, () => Is.EqualTo(originalSelection));
}
[Test] [Test]
public void TestSongSelectScrollHandling() public void TestSongSelectScrollHandling()
{ {

View File

@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Profile; using osu.Game.Overlays.Profile;
@ -20,24 +21,16 @@ namespace osu.Game.Tests.Visual.Online
public partial class TestSceneUserProfileDailyChallenge : OsuManualInputManagerTestScene public partial class TestSceneUserProfileDailyChallenge : OsuManualInputManagerTestScene
{ {
[Cached] [Cached]
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>(new UserProfileData(new APIUser(), new OsuRuleset().RulesetInfo)); private readonly Bindable<UserProfileData?> userProfileData = new Bindable<UserProfileData?>(new UserProfileData(new APIUser(), new OsuRuleset().RulesetInfo));
[Cached] [Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
protected override void LoadComplete() private DailyChallengeStatsDisplay display = null!;
[SetUpSteps]
public void SetUpSteps()
{ {
base.LoadComplete();
DailyChallengeStatsDisplay display = null!;
AddSliderStep("daily", 0, 999, 2, v => update(s => s.DailyStreakCurrent = v));
AddSliderStep("daily best", 0, 999, 2, v => update(s => s.DailyStreakBest = v));
AddSliderStep("weekly", 0, 250, 1, v => update(s => s.WeeklyStreakCurrent = v));
AddSliderStep("weekly best", 0, 250, 1, v => update(s => s.WeeklyStreakBest = v));
AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v));
AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v));
AddSliderStep("playcount", 0, 1500, 1, v => update(s => s.PlayCount = v));
AddStep("create", () => AddStep("create", () =>
{ {
Clear(); Clear();
@ -51,16 +44,40 @@ namespace osu.Game.Tests.Visual.Online
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Scale = new Vector2(1f), Scale = new Vector2(1f),
User = { BindTarget = User }, User = { BindTarget = userProfileData },
}); });
}); });
AddStep("set local user", () => update(s => s.UserID = API.LocalUser.Value.Id));
}
protected override void LoadComplete()
{
base.LoadComplete();
AddSliderStep("daily", 0, 999, 2, v => update(s => s.DailyStreakCurrent = v));
AddSliderStep("daily best", 0, 999, 2, v => update(s => s.DailyStreakBest = v));
AddSliderStep("weekly", 0, 250, 1, v => update(s => s.WeeklyStreakCurrent = v));
AddSliderStep("weekly best", 0, 250, 1, v => update(s => s.WeeklyStreakBest = v));
AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v));
AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v));
AddSliderStep("playcount", 0, 1500, 1, v => update(s => s.PlayCount = v));
}
[Test]
public void TestStates()
{
AddStep("played today", () => update(s => s.LastUpdate = DateTimeOffset.UtcNow.Date));
AddStep("played yesterday", () => update(s => s.LastUpdate = DateTimeOffset.UtcNow.Date.AddDays(-1)));
AddStep("change to non-local user", () => update(s => s.UserID = API.LocalUser.Value.Id + 1000));
AddStep("hover", () => InputManager.MoveMouseTo(display)); AddStep("hover", () => InputManager.MoveMouseTo(display));
} }
private void update(Action<APIUserDailyChallengeStatistics> change) private void update(Action<APIUserDailyChallengeStatistics> change)
{ {
change.Invoke(User.Value!.User.DailyChallengeStatistics); change.Invoke(userProfileData.Value!.User.DailyChallengeStatistics);
User.Value = new UserProfileData(User.Value.User, User.Value.Ruleset); userProfileData.Value = new UserProfileData(userProfileData.Value.User, userProfileData.Value.Ruleset);
} }
[Test] [Test]

View File

@ -0,0 +1,189 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using osuTK.Graphics;
using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel;
namespace osu.Game.Tests.Visual.SongSelect
{
public abstract partial class BeatmapCarouselV2TestScene : OsuManualInputManagerTestScene
{
protected readonly BindableList<BeatmapSetInfo> BeatmapSets = new BindableList<BeatmapSetInfo>();
protected BeatmapCarousel Carousel = null!;
protected OsuScrollContainer<Drawable> Scroll => Carousel.ChildrenOfType<OsuScrollContainer<Drawable>>().Single();
[Cached(typeof(BeatmapStore))]
private BeatmapStore store;
private OsuTextFlowContainer stats = null!;
private int beatmapCount;
protected BeatmapCarouselV2TestScene()
{
store = new TestBeatmapStore
{
BeatmapSets = { BindTarget = BeatmapSets }
};
BeatmapSets.BindCollectionChanged((_, _) => beatmapCount = BeatmapSets.Sum(s => s.Beatmaps.Count));
Scheduler.AddDelayed(updateStats, 100, true);
}
[SetUpSteps]
public void SetUpSteps()
{
RemoveAllBeatmaps();
CreateCarousel();
SortBy(new FilterCriteria { Sort = SortMode.Title });
}
protected void CreateCarousel()
{
AddStep("create components", () =>
{
Box topBox;
Children = new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Relative, 1),
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 200),
new Dimension(),
new Dimension(GridSizeMode.Absolute, 200),
},
Content = new[]
{
new Drawable[]
{
topBox = new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = Color4.Cyan,
RelativeSizeAxes = Axes.Both,
Alpha = 0.4f,
},
},
new Drawable[]
{
Carousel = new BeatmapCarousel
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 500,
RelativeSizeAxes = Axes.Y,
},
},
new[]
{
new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = Color4.Cyan,
RelativeSizeAxes = Axes.Both,
Alpha = 0.4f,
},
topBox.CreateProxy(),
}
}
},
stats = new OsuTextFlowContainer
{
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding(10),
TextAnchor = Anchor.CentreLeft,
},
};
});
}
protected void SortBy(FilterCriteria criteria) => AddStep($"sort by {criteria.Sort}", () => Carousel.Filter(criteria));
protected void WaitForDrawablePanels() => AddUntilStep("drawable panels loaded", () => Carousel.ChildrenOfType<ICarouselPanel>().Count(), () => Is.GreaterThan(0));
protected void WaitForSorting() => AddUntilStep("sorting finished", () => Carousel.IsFiltering, () => Is.False);
protected void WaitForScrolling() => AddUntilStep("scroll finished", () => Scroll.Current, () => Is.EqualTo(Scroll.Target));
/// <summary>
/// Add requested beatmap sets count to list.
/// </summary>
/// <param name="count">The count of beatmap sets to add.</param>
/// <param name="fixedDifficultiesPerSet">If not null, the number of difficulties per set. If null, randomised difficulty count will be used.</param>
protected void AddBeatmaps(int count, int? fixedDifficultiesPerSet = null) => AddStep($"add {count} beatmaps", () =>
{
for (int i = 0; i < count; i++)
BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(fixedDifficultiesPerSet ?? RNG.Next(1, 4)));
});
protected void RemoveAllBeatmaps() => AddStep("clear all beatmaps", () => BeatmapSets.Clear());
protected void RemoveFirstBeatmap() =>
AddStep("remove first beatmap", () =>
{
if (BeatmapSets.Count == 0) return;
BeatmapSets.Remove(BeatmapSets.First());
});
private void updateStats()
{
if (Carousel.IsNull())
return;
stats.Clear();
createHeader("beatmap store");
stats.AddParagraph($"""
sets: {BeatmapSets.Count}
beatmaps: {beatmapCount}
""");
createHeader("carousel");
stats.AddParagraph($"""
sorting: {Carousel.IsFiltering}
tracked: {Carousel.ItemsTracked}
displayable: {Carousel.DisplayableItems}
displayed: {Carousel.VisibleItems}
selected: {Carousel.CurrentSelection}
""");
void createHeader(string text)
{
stats.AddParagraph(string.Empty);
stats.AddParagraph(text, cp =>
{
cp.Font = cp.Font.With(size: 18, weight: FontWeight.Bold);
});
}
}
}
}

View File

@ -1,273 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.Containers;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using osuTK.Graphics;
using BeatmapCarousel = osu.Game.Screens.SelectV2.BeatmapCarousel;
namespace osu.Game.Tests.Visual.SongSelect
{
[TestFixture]
public partial class TestSceneBeatmapCarouselV2 : OsuManualInputManagerTestScene
{
private readonly BindableList<BeatmapSetInfo> beatmapSets = new BindableList<BeatmapSetInfo>();
[Cached(typeof(BeatmapStore))]
private BeatmapStore store;
private OsuTextFlowContainer stats = null!;
private BeatmapCarousel carousel = null!;
private OsuScrollContainer<Drawable> scroll => carousel.ChildrenOfType<OsuScrollContainer<Drawable>>().Single();
private int beatmapCount;
public TestSceneBeatmapCarouselV2()
{
store = new TestBeatmapStore
{
BeatmapSets = { BindTarget = beatmapSets }
};
beatmapSets.BindCollectionChanged((_, _) =>
{
beatmapCount = beatmapSets.Sum(s => s.Beatmaps.Count);
});
Scheduler.AddDelayed(updateStats, 100, true);
}
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create components", () =>
{
beatmapSets.Clear();
Box topBox;
Children = new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Relative, 1),
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 200),
new Dimension(),
new Dimension(GridSizeMode.Absolute, 200),
},
Content = new[]
{
new Drawable[]
{
topBox = new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = Color4.Cyan,
RelativeSizeAxes = Axes.Both,
Alpha = 0.4f,
},
},
new Drawable[]
{
carousel = new BeatmapCarousel
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 500,
RelativeSizeAxes = Axes.Y,
},
},
new[]
{
new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = Color4.Cyan,
RelativeSizeAxes = Axes.Both,
Alpha = 0.4f,
},
topBox.CreateProxy(),
}
}
},
stats = new OsuTextFlowContainer(cp => cp.Font = FrameworkFont.Regular.With())
{
Padding = new MarginPadding(10),
TextAnchor = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
};
});
AddStep("sort by title", () =>
{
carousel.Filter(new FilterCriteria { Sort = SortMode.Title });
});
}
[Test]
public void TestBasic()
{
AddStep("add 10 beatmaps", () =>
{
for (int i = 0; i < 10; i++)
beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4)));
});
AddStep("add 1 beatmap", () => beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))));
AddStep("remove all beatmaps", () => beatmapSets.Clear());
}
[Test]
public void TestSorting()
{
AddStep("add 10 beatmaps", () =>
{
for (int i = 0; i < 10; i++)
beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4)));
});
AddStep("sort by difficulty", () =>
{
carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty });
});
AddStep("sort by artist", () =>
{
carousel.Filter(new FilterCriteria { Sort = SortMode.Artist });
});
}
[Test]
public void TestScrollPositionMaintainedOnAddSecondSelected()
{
Quad positionBefore = default;
AddStep("add 10 beatmaps", () =>
{
for (int i = 0; i < 10; i++)
beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4)));
});
AddUntilStep("visual item added", () => carousel.ChildrenOfType<BeatmapCarouselPanel>().Count(), () => Is.GreaterThan(0));
AddStep("select middle beatmap", () => carousel.CurrentSelection = beatmapSets.ElementAt(beatmapSets.Count - 2));
AddStep("scroll to selected item", () => scroll.ScrollTo(scroll.ChildrenOfType<BeatmapCarouselPanel>().Single(p => p.Item!.Selected.Value)));
AddUntilStep("wait for scroll finished", () => scroll.Current, () => Is.EqualTo(scroll.Target));
AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType<BeatmapCarouselPanel>().FirstOrDefault(p => p.Item!.Selected.Value)!.ScreenSpaceDrawQuad);
AddStep("remove first beatmap", () => beatmapSets.Remove(beatmapSets.Last()));
AddUntilStep("sorting finished", () => carousel.IsFiltering, () => Is.False);
AddAssert("select screen position unchanged", () => carousel.ChildrenOfType<BeatmapCarouselPanel>().Single(p => p.Item!.Selected.Value).ScreenSpaceDrawQuad,
() => Is.EqualTo(positionBefore));
}
[Test]
public void TestScrollPositionMaintainedOnAddLastSelected()
{
Quad positionBefore = default;
AddStep("add 10 beatmaps", () =>
{
for (int i = 0; i < 10; i++)
beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4)));
});
AddUntilStep("visual item added", () => carousel.ChildrenOfType<BeatmapCarouselPanel>().Count(), () => Is.GreaterThan(0));
AddStep("scroll to last item", () => scroll.ScrollToEnd(false));
AddStep("select last beatmap", () => carousel.CurrentSelection = beatmapSets.First());
AddUntilStep("wait for scroll finished", () => scroll.Current, () => Is.EqualTo(scroll.Target));
AddStep("save selected screen position", () => positionBefore = carousel.ChildrenOfType<BeatmapCarouselPanel>().FirstOrDefault(p => p.Item!.Selected.Value)!.ScreenSpaceDrawQuad);
AddStep("remove first beatmap", () => beatmapSets.Remove(beatmapSets.Last()));
AddUntilStep("sorting finished", () => carousel.IsFiltering, () => Is.False);
AddAssert("select screen position unchanged", () => carousel.ChildrenOfType<BeatmapCarouselPanel>().Single(p => p.Item!.Selected.Value).ScreenSpaceDrawQuad,
() => Is.EqualTo(positionBefore));
}
[Test]
public void TestAddRemoveOneByOne()
{
AddRepeatStep("add beatmaps", () => beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))), 20);
AddRepeatStep("remove beatmaps", () => beatmapSets.RemoveAt(RNG.Next(0, beatmapSets.Count)), 20);
}
[Test]
[Explicit]
public void TestInsane()
{
const int count = 200000;
List<BeatmapSetInfo> generated = new List<BeatmapSetInfo>();
AddStep($"populate {count} test beatmaps", () =>
{
generated.Clear();
Task.Run(() =>
{
for (int j = 0; j < count; j++)
generated.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4)));
}).ConfigureAwait(true);
});
AddUntilStep("wait for beatmaps populated", () => generated.Count, () => Is.GreaterThan(count / 3));
AddUntilStep("this takes a while", () => generated.Count, () => Is.GreaterThan(count / 3 * 2));
AddUntilStep("maybe they are done now", () => generated.Count, () => Is.EqualTo(count));
AddStep("add all beatmaps", () => beatmapSets.AddRange(generated));
}
private void updateStats()
{
if (carousel.IsNull())
return;
stats.Text = $"""
store
sets: {beatmapSets.Count}
beatmaps: {beatmapCount}
carousel:
sorting: {carousel.IsFiltering}
tracked: {carousel.ItemsTracked}
displayable: {carousel.DisplayableItems}
displayed: {carousel.VisibleItems}
""";
}
}
}

View File

@ -0,0 +1,119 @@
// 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.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.SongSelect
{
/// <summary>
/// Currently covers adding and removing of items and scrolling.
/// If we add more tests here, these two categories can likely be split out into separate scenes.
/// </summary>
[TestFixture]
public partial class TestSceneBeatmapCarouselV2Basics : BeatmapCarouselV2TestScene
{
[Test]
public void TestBasics()
{
AddBeatmaps(1);
AddBeatmaps(10);
RemoveFirstBeatmap();
RemoveAllBeatmaps();
}
[Test]
public void TestAddRemoveOneByOne()
{
AddRepeatStep("add beatmaps", () => BeatmapSets.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4))), 20);
AddRepeatStep("remove beatmaps", () => BeatmapSets.RemoveAt(RNG.Next(0, BeatmapSets.Count)), 20);
}
[Test]
public void TestSorting()
{
AddBeatmaps(10);
SortBy(new FilterCriteria { Sort = SortMode.Difficulty });
SortBy(new FilterCriteria { Sort = SortMode.Artist });
}
[Test]
public void TestScrollPositionMaintainedOnAddSecondSelected()
{
Quad positionBefore = default;
AddBeatmaps(10);
WaitForDrawablePanels();
AddStep("select middle beatmap", () => Carousel.CurrentSelection = BeatmapSets.ElementAt(BeatmapSets.Count - 2));
AddStep("scroll to selected item", () => Scroll.ScrollTo(Scroll.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value)));
WaitForScrolling();
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<BeatmapPanel>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
RemoveFirstBeatmap();
WaitForSorting();
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
() => Is.EqualTo(positionBefore));
}
[Test]
public void TestScrollPositionMaintainedOnAddLastSelected()
{
Quad positionBefore = default;
AddBeatmaps(10);
WaitForDrawablePanels();
AddStep("scroll to last item", () => Scroll.ScrollToEnd(false));
AddStep("select last beatmap", () => Carousel.CurrentSelection = BeatmapSets.Last());
WaitForScrolling();
AddStep("save selected screen position", () => positionBefore = Carousel.ChildrenOfType<BeatmapPanel>().FirstOrDefault(p => p.Selected.Value)!.ScreenSpaceDrawQuad);
RemoveFirstBeatmap();
WaitForSorting();
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<BeatmapPanel>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
() => Is.EqualTo(positionBefore));
}
[Test]
[Explicit]
public void TestPerformanceWithManyBeatmaps()
{
const int count = 200000;
List<BeatmapSetInfo> generated = new List<BeatmapSetInfo>();
AddStep($"populate {count} test beatmaps", () =>
{
generated.Clear();
Task.Run(() =>
{
for (int j = 0; j < count; j++)
generated.Add(TestResources.CreateTestBeatmapSetInfo(RNG.Next(1, 4)));
}).ConfigureAwait(true);
});
AddUntilStep("wait for beatmaps populated", () => generated.Count, () => Is.GreaterThan(count / 3));
AddUntilStep("this takes a while", () => generated.Count, () => Is.GreaterThan(count / 3 * 2));
AddUntilStep("maybe they are done now", () => generated.Count, () => Is.EqualTo(count));
AddStep("add all beatmaps", () => BeatmapSets.AddRange(generated));
}
}
}

View File

@ -0,0 +1,216 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Screens.SelectV2;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect
{
[TestFixture]
public partial class TestSceneBeatmapCarouselV2Selection : BeatmapCarouselV2TestScene
{
/// <summary>
/// Keyboard selection via up and down arrows doesn't actually change the selection until
/// the select key is pressed.
/// </summary>
[Test]
public void TestKeyboardSelectionKeyRepeat()
{
AddBeatmaps(10);
WaitForDrawablePanels();
checkNoSelection();
select();
checkNoSelection();
AddStep("press down arrow", () => InputManager.PressKey(Key.Down));
checkSelectionIterating(false);
AddStep("press up arrow", () => InputManager.PressKey(Key.Up));
checkSelectionIterating(false);
AddStep("release down arrow", () => InputManager.ReleaseKey(Key.Down));
checkSelectionIterating(false);
AddStep("release up arrow", () => InputManager.ReleaseKey(Key.Up));
checkSelectionIterating(false);
select();
checkHasSelection();
}
/// <summary>
/// Keyboard selection via left and right arrows moves between groups, updating the selection
/// immediately.
/// </summary>
[Test]
public void TestGroupSelectionKeyRepeat()
{
AddBeatmaps(10);
WaitForDrawablePanels();
checkNoSelection();
AddStep("press right arrow", () => InputManager.PressKey(Key.Right));
checkSelectionIterating(true);
AddStep("press left arrow", () => InputManager.PressKey(Key.Left));
checkSelectionIterating(true);
AddStep("release right arrow", () => InputManager.ReleaseKey(Key.Right));
checkSelectionIterating(true);
AddStep("release left arrow", () => InputManager.ReleaseKey(Key.Left));
checkSelectionIterating(false);
}
[Test]
public void TestCarouselRemembersSelection()
{
AddBeatmaps(10);
WaitForDrawablePanels();
selectNextGroup();
object? selection = null;
AddStep("store drawable selection", () => selection = getSelectedPanel()?.Item?.Model);
checkHasSelection();
AddAssert("drawable selection non-null", () => selection, () => Is.Not.Null);
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
RemoveAllBeatmaps();
AddUntilStep("no drawable selection", getSelectedPanel, () => Is.Null);
AddBeatmaps(10);
WaitForDrawablePanels();
checkHasSelection();
AddAssert("no drawable selection", getSelectedPanel, () => Is.Null);
AddStep("add previous selection", () => BeatmapSets.Add(((BeatmapInfo)selection!).BeatmapSet!));
AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
AddAssert("drawable selection matches carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType<BeatmapPanel>().SingleOrDefault(p => p.Selected.Value);
}
[Test]
public void TestTraversalBeyondStart()
{
const int total_set_count = 200;
AddBeatmaps(total_set_count);
WaitForDrawablePanels();
selectNextGroup();
waitForSelection(0, 0);
selectPrevGroup();
waitForSelection(total_set_count - 1, 0);
}
[Test]
public void TestTraversalBeyondEnd()
{
const int total_set_count = 200;
AddBeatmaps(total_set_count);
WaitForDrawablePanels();
selectPrevGroup();
waitForSelection(total_set_count - 1, 0);
selectNextGroup();
waitForSelection(0, 0);
}
[Test]
public void TestKeyboardSelection()
{
AddBeatmaps(10, 3);
WaitForDrawablePanels();
selectNextPanel();
selectNextPanel();
selectNextPanel();
selectNextPanel();
checkNoSelection();
select();
waitForSelection(3, 0);
selectNextPanel();
waitForSelection(3, 0);
select();
waitForSelection(3, 1);
selectNextPanel();
waitForSelection(3, 1);
select();
waitForSelection(3, 2);
selectNextPanel();
waitForSelection(3, 2);
select();
waitForSelection(4, 0);
}
[Test]
public void TestEmptyTraversal()
{
selectNextPanel();
checkNoSelection();
selectNextGroup();
checkNoSelection();
selectPrevPanel();
checkNoSelection();
selectPrevGroup();
checkNoSelection();
}
private void waitForSelection(int set, int? diff = null)
{
AddUntilStep($"selected is set{set}{(diff.HasValue ? $" diff{diff.Value}" : "")}", () =>
{
if (diff != null)
return ReferenceEquals(Carousel.CurrentSelection, BeatmapSets[set].Beatmaps[diff.Value]);
return BeatmapSets[set].Beatmaps.Contains(Carousel.CurrentSelection);
});
}
private void selectNextPanel() => AddStep("select next panel", () => InputManager.Key(Key.Down));
private void selectPrevPanel() => AddStep("select prev panel", () => InputManager.Key(Key.Up));
private void selectNextGroup() => AddStep("select next group", () => InputManager.Key(Key.Right));
private void selectPrevGroup() => AddStep("select prev group", () => InputManager.Key(Key.Left));
private void select() => AddStep("select", () => InputManager.Key(Key.Enter));
private void checkNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null);
private void checkHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null);
private void checkSelectionIterating(bool isIterating)
{
object? selection = null;
for (int i = 0; i < 3; i++)
{
AddStep("store selection", () => selection = Carousel.CurrentSelection);
if (isIterating)
AddUntilStep("selection changed", () => Carousel.CurrentSelection != selection);
else
AddUntilStep("selection not changed", () => Carousel.CurrentSelection == selection);
}
}
}
}

View File

@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Current = { Disabled = true }, Current = { Disabled = true },
TabbableContentContainer = this, TabbableContentContainer = this,
}, },
new FormNumberBox new FormNumberBox(allowDecimals: true)
{ {
Caption = "Number", Caption = "Number",
HintText = "Insert your favourite number", HintText = "Insert your favourite number",

View File

@ -83,6 +83,40 @@ namespace osu.Game.Tests.Visual.UserInterface
waitForCompletion(); waitForCompletion();
} }
[Test]
public void TestNormalDoesForwardToOverlay()
{
SimpleNotification notification = null!;
AddStep(@"simple #1", () => notificationOverlay.Post(notification = new SimpleNotification
{
Text = @"This shouldn't annoy you too much",
Transient = false,
}));
AddAssert("notification in toast tray", () => notification.IsInToastTray, () => Is.True);
AddUntilStep("wait for dismissed", () => notification.IsInToastTray, () => Is.False);
checkDisplayedCount(1);
}
[Test]
public void TestTransientDoesNotForwardToOverlay()
{
SimpleNotification notification = null!;
AddStep(@"simple #1", () => notificationOverlay.Post(notification = new SimpleNotification
{
Text = @"This shouldn't annoy you too much",
Transient = true,
}));
AddAssert("notification in toast tray", () => notification.IsInToastTray, () => Is.True);
AddUntilStep("wait for dismissed", () => notification.IsInToastTray, () => Is.False);
checkDisplayedCount(0);
}
[Test] [Test]
public void TestForwardWithFlingRight() public void TestForwardWithFlingRight()
{ {
@ -634,12 +668,18 @@ namespace osu.Game.Tests.Visual.UserInterface
private partial class BackgroundNotification : SimpleNotification private partial class BackgroundNotification : SimpleNotification
{ {
public override bool IsImportant => false; public BackgroundNotification()
{
IsImportant = false;
}
} }
private partial class BackgroundProgressNotification : ProgressNotification private partial class BackgroundProgressNotification : ProgressNotification
{ {
public override bool IsImportant => false; public BackgroundProgressNotification()
{
IsImportant = false;
}
} }
} }
} }

View File

@ -71,6 +71,8 @@ namespace osu.Game.Tournament.Screens.Editors
[Resolved] [Resolved]
private LadderInfo ladderInfo { get; set; } = null!; private LadderInfo ladderInfo { get; set; } = null!;
private readonly SettingsTextBox acronymTextBox;
public TeamRow(TournamentTeam team, TournamentScreen parent) public TeamRow(TournamentTeam team, TournamentScreen parent)
{ {
Model = team; Model = team;
@ -112,7 +114,7 @@ namespace osu.Game.Tournament.Screens.Editors
Width = 0.2f, Width = 0.2f,
Current = Model.FullName Current = Model.FullName
}, },
new SettingsTextBox acronymTextBox = new SettingsTextBox
{ {
LabelText = "Acronym", LabelText = "Acronym",
Width = 0.2f, Width = 0.2f,
@ -177,6 +179,27 @@ namespace osu.Game.Tournament.Screens.Editors
}; };
} }
protected override void LoadComplete()
{
base.LoadComplete();
Model.Acronym.BindValueChanged(acronym =>
{
var teamsWithSameAcronym = ladderInfo.Teams
.Where(t => t.Acronym.Value == acronym.NewValue && t != Model)
.ToList();
if (teamsWithSameAcronym.Count > 0)
{
acronymTextBox.SetNoticeText(
$"Acronym '{acronym.NewValue}' is already in use by team{(teamsWithSameAcronym.Count > 1 ? "s" : "")}:\n"
+ $"{string.Join(",\n", teamsWithSameAcronym)}", true);
}
else
acronymTextBox.ClearNoticeText();
}, true);
}
private partial class LastYearPlacementSlider : RoundedSliderBar<int> private partial class LastYearPlacementSlider : RoundedSliderBar<int>
{ {
public override LocalisableString TooltipText => Current.Value == 0 ? "N/A" : base.TooltipText; public override LocalisableString TooltipText => Current.Value == 0 ? "N/A" : base.TooltipText;

View File

@ -475,11 +475,8 @@ namespace osu.Game.Beatmaps
beatmapContent.BeatmapInfo = beatmapInfo; beatmapContent.BeatmapInfo = beatmapInfo;
// Since now this is a locally-modified beatmap, we also set all relevant flags to indicate this. // Since now this is a locally-modified beatmap, we also set all relevant flags to indicate this.
// Importantly, the `ResetOnlineInfo()` call must happen before encoding, as online ID is encoded into the `.osu` file,
// which influences the beatmap checksums.
beatmapInfo.LastLocalUpdate = DateTimeOffset.Now; beatmapInfo.LastLocalUpdate = DateTimeOffset.Now;
beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified; beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;
beatmapInfo.ResetOnlineInfo();
Realm.Write(r => Realm.Write(r =>
{ {

View File

@ -292,7 +292,7 @@ namespace osu.Game.Beatmaps.Drawables
"1407228 II-L - VANGUARD-1.osz", "1407228 II-L - VANGUARD-1.osz",
"1422686 II-L - VANGUARD-2.osz", "1422686 II-L - VANGUARD-2.osz",
"1429217 Street - Phi.osz", "1429217 Street - Phi.osz",
"1442235 2ToneDisco x Cosmicosmo - Shoelaces (feat. Puniden).osz", "1442235 2ToneDisco x Cosmicosmo - Shoelaces (feat. Puniden).osz", // set is not marked as FA, but track is listed in https://osu.ppy.sh/beatmaps/artists/157
"1447478 Cres. - End Time.osz", "1447478 Cres. - End Time.osz",
"1449942 m108 - Crescent Sakura.osz", "1449942 m108 - Crescent Sakura.osz",
"1463778 MuryokuP - A tree without a branch.osz", "1463778 MuryokuP - A tree without a branch.osz",
@ -336,8 +336,8 @@ namespace osu.Game.Beatmaps.Drawables
"1854710 Blaster & Extra Terra - Spacecraft (Cut Ver.).osz", "1854710 Blaster & Extra Terra - Spacecraft (Cut Ver.).osz",
"1859322 Hino Isuka - Delightness Brightness.osz", "1859322 Hino Isuka - Delightness Brightness.osz",
"1884102 Maduk - Go (feat. Lachi) (Cut Ver.).osz", "1884102 Maduk - Go (feat. Lachi) (Cut Ver.).osz",
"1884578 Neko Hacker - People People feat. Nanahira.osz", "1884578 Neko Hacker - People People feat. Nanahira.osz", // set is not marked as FA, but track is listed in https://osu.ppy.sh/beatmaps/artists/266
"1897902 uma vs. Morimori Atsushi - Re: End of a Dream.osz", "1897902 uma vs. Morimori Atsushi - Re: End of a Dream.osz", // set is not marked as FA, but track is listed in https://osu.ppy.sh/beatmaps/artists/108
"1905582 KINEMA106 - Fly Away (Cut Ver.).osz", "1905582 KINEMA106 - Fly Away (Cut Ver.).osz",
"1934686 ARForest - Rainbow Magic!!.osz", "1934686 ARForest - Rainbow Magic!!.osz",
"1963076 METAROOM - S.N.U.F.F.Y.osz", "1963076 METAROOM - S.N.U.F.F.Y.osz",
@ -345,7 +345,6 @@ namespace osu.Game.Beatmaps.Drawables
"1971951 James Landino - Shiba Paradise.osz", "1971951 James Landino - Shiba Paradise.osz",
"1972518 Toromaru - Sleight of Hand.osz", "1972518 Toromaru - Sleight of Hand.osz",
"1982302 KINEMA106 - INVITE.osz", "1982302 KINEMA106 - INVITE.osz",
"1983475 KNOWER - The Government Knows.osz",
"2010165 Junk - Yellow Smile (bms edit).osz", "2010165 Junk - Yellow Smile (bms edit).osz",
"2022737 Andora - Euphoria (feat. WaMi).osz", "2022737 Andora - Euphoria (feat. WaMi).osz",
"2025023 tephe - Genjitsu Escape.osz", "2025023 tephe - Genjitsu Escape.osz",

View File

@ -113,6 +113,31 @@ namespace osu.Game.Beatmaps
return queryCacheVersion2(db, beatmapInfo, out onlineMetadata); return queryCacheVersion2(db, beatmapInfo, out onlineMetadata);
} }
} }
onlineMetadata = null;
return false;
}
catch (SqliteException sqliteException)
{
onlineMetadata = null;
// There have been cases where the user's local database is corrupt.
// Let's attempt to identify these cases and re-initialise the local cache.
switch (sqliteException.SqliteErrorCode)
{
case 26: // SQLITE_NOTADB
case 11: // SQLITE_CORRUPT
// only attempt purge & re-download if there is no other refetch in progress
if (cacheDownloadRequest != null)
return false;
tryPurgeCache();
prepareLocalCache();
return false;
}
logForModel(beatmapInfo.BeatmapSet, $@"Cached local retrieval for {beatmapInfo} failed with unhandled sqlite error {sqliteException}.");
return false;
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -120,9 +145,22 @@ namespace osu.Game.Beatmaps
onlineMetadata = null; onlineMetadata = null;
return false; return false;
} }
}
onlineMetadata = null; private void tryPurgeCache()
return false; {
log(@"Local metadata cache is corrupted; attempting purge.");
try
{
File.Delete(storage.GetFullPath(cache_database_name));
}
catch (Exception ex)
{
log($@"Failed to purge local metadata cache: {ex}");
}
log(@"Local metadata cache purged due to corruption.");
} }
private SqliteConnection getConnection() => private SqliteConnection getConnection() =>

View File

@ -131,8 +131,6 @@ namespace osu.Game.Database
private partial class DownloadNotification : ProgressNotification private partial class DownloadNotification : ProgressNotification
{ {
public override bool IsImportant => false;
protected override Notification CreateCompletionNotification() => new SilencedProgressCompletionNotification protected override Notification CreateCompletionNotification() => new SilencedProgressCompletionNotification
{ {
Activated = CompletionClickAction, Activated = CompletionClickAction,
@ -141,7 +139,10 @@ namespace osu.Game.Database
private partial class SilencedProgressCompletionNotification : ProgressCompletionNotification private partial class SilencedProgressCompletionNotification : ProgressCompletionNotification
{ {
public override bool IsImportant => false; public SilencedProgressCompletionNotification()
{
IsImportant = false;
}
} }
} }
} }

View File

@ -11,7 +11,6 @@ using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Development; using osu.Framework.Development;
using osu.Framework.Extensions; using osu.Framework.Extensions;
@ -97,8 +96,9 @@ namespace osu.Game.Database
/// 44 2024-11-22 Removed several properties from BeatmapInfo which did not need to be persisted to realm. /// 44 2024-11-22 Removed several properties from BeatmapInfo which did not need to be persisted to realm.
/// 45 2024-12-23 Change beat snap divisor adjust defaults to be Ctrl+Scroll instead of Ctrl+Shift+Scroll, if not already changed by user. /// 45 2024-12-23 Change beat snap divisor adjust defaults to be Ctrl+Scroll instead of Ctrl+Shift+Scroll, if not already changed by user.
/// 46 2024-12-26 Change beat snap divisor bindings to match stable directionality ¯\_(ツ)_/¯. /// 46 2024-12-26 Change beat snap divisor bindings to match stable directionality ¯\_(ツ)_/¯.
/// 47 2025-01-21 Remove right mouse button binding for absolute scroll. Never use mouse buttons (or scroll) for global actions.
/// </summary> /// </summary>
private const int schema_version = 46; private const int schema_version = 47;
/// <summary> /// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods. /// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
@ -413,18 +413,7 @@ namespace osu.Game.Database
/// Compact this realm. /// Compact this realm.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public bool Compact() public bool Compact() => Realm.Compact(getConfiguration());
{
try
{
return Realm.Compact(getConfiguration());
}
// Catch can be removed along with entity framework. Is specifically to allow a failure message to arrive to the user (see similar catches in EFToRealmMigrator).
catch (AggregateException ae) when (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && ae.Flatten().InnerException is TypeInitializationException)
{
return true;
}
}
/// <summary> /// <summary>
/// Run work on realm with a return value. /// Run work on realm with a return value.
@ -720,11 +709,6 @@ namespace osu.Game.Database
return Realm.GetInstance(getConfiguration()); return Realm.GetInstance(getConfiguration());
} }
// Catch can be removed along with entity framework. Is specifically to allow a failure message to arrive to the user (see similar catches in EFToRealmMigrator).
catch (AggregateException ae) when (RuntimeInfo.OS == RuntimeInfo.Platform.macOS && ae.Flatten().InnerException is TypeInitializationException)
{
return Realm.GetInstance();
}
finally finally
{ {
if (tookSemaphoreLock) if (tookSemaphoreLock)
@ -1239,6 +1223,17 @@ namespace osu.Game.Database
break; break;
} }
case 47:
{
var keyBindings = migration.NewRealm.All<RealmKeyBinding>();
var existingBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.AbsoluteScrollSongList);
if (existingBinding != null && existingBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.MouseRight }))
migration.NewRealm.Remove(existingBinding);
break;
}
} }
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms"); Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");

View File

@ -1,32 +1,30 @@
// 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 System.Globalization;
using osu.Framework.Input; using osu.Framework.Input;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
{ {
public partial class FormNumberBox : FormTextBox public partial class FormNumberBox : FormTextBox
{ {
public bool AllowDecimals { get; init; } private readonly bool allowDecimals;
internal override InnerTextBox CreateTextBox() => new InnerNumberBox public FormNumberBox(bool allowDecimals = false)
{
this.allowDecimals = allowDecimals;
}
internal override InnerTextBox CreateTextBox() => new InnerNumberBox(allowDecimals)
{ {
AllowDecimals = AllowDecimals,
SelectAllOnFocus = true, SelectAllOnFocus = true,
}; };
internal partial class InnerNumberBox : InnerTextBox internal partial class InnerNumberBox : InnerTextBox
{ {
public bool AllowDecimals { get; init; } public InnerNumberBox(bool allowDecimals)
public InnerNumberBox()
{ {
InputProperties = new TextInputProperties(TextInputType.Number, false); InputProperties = new TextInputProperties(allowDecimals ? TextInputType.Decimal : TextInputType.Number, false);
} }
protected override bool CanAddCharacter(char character)
=> char.IsAsciiDigit(character) || (AllowDecimals && CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator.Contains(character));
} }
} }
} }

View File

@ -119,7 +119,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
Caption = Caption, Caption = Caption,
TooltipText = HintText, TooltipText = HintText,
}, },
textBox = new FormNumberBox.InnerNumberBox textBox = new FormNumberBox.InnerNumberBox(allowDecimals: true)
{ {
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
@ -127,7 +127,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
Width = 0.5f, Width = 0.5f,
CommitOnFocusLost = true, CommitOnFocusLost = true,
SelectAllOnFocus = true, SelectAllOnFocus = true,
AllowDecimals = true,
OnInputError = () => OnInputError = () =>
{ {
flashLayer.Colour = ColourInfo.GradientVertical(colours.Red3.Opacity(0), colours.Red3); flashLayer.Colour = ColourInfo.GradientVertical(colours.Red3.Opacity(0), colours.Red3);

View File

@ -14,7 +14,6 @@ using osu.Framework.Input.Events;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Overlays; using osu.Game.Overlays;
using osuTK; using osuTK;
using osuTK.Input;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
{ {
@ -75,14 +74,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
samplePopOut?.Play(); samplePopOut?.Play();
} }
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Key == Key.Escape)
return false; // disable the framework-level handling of escape key for conformity (we use GlobalAction.Back).
return base.OnKeyDown(e);
}
public virtual bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public virtual bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{ {
if (e.Repeat) if (e.Repeat)

View File

@ -32,6 +32,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
set => slider.Current = value; set => slider.Current = value;
} }
public CompositeDrawable TabbableContentContainer
{
set => textBox.TabbableContentContainer = value;
}
private bool instantaneous; private bool instantaneous;
/// <summary> /// <summary>
@ -69,6 +74,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
textBox = new LabelledTextBox textBox = new LabelledTextBox
{ {
Label = labelText, Label = labelText,
SelectAllOnFocus = true,
}, },
slider = new SettingsSlider<T> slider = new SettingsSlider<T>
{ {
@ -87,8 +93,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
public bool TakeFocus() => GetContainingFocusManager()?.ChangeFocus(textBox) == true; public bool TakeFocus() => GetContainingFocusManager()?.ChangeFocus(textBox) == true;
public bool SelectAll() => textBox.SelectAll();
private bool updatingFromTextBox; private bool updatingFromTextBox;
private void textChanged(ValueChangedEvent<string> change) private void textChanged(ValueChangedEvent<string> change)

View File

@ -9,6 +9,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using Microsoft.Toolkit.HighPerformance; using Microsoft.Toolkit.HighPerformance;
using osu.Framework.Extensions;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using SharpCompress.Archives.Zip; using SharpCompress.Archives.Zip;
using SharpCompress.Common; using SharpCompress.Common;
@ -54,12 +55,22 @@ namespace osu.Game.IO.Archives
if (entry == null) if (entry == null)
return null; return null;
var owner = MemoryAllocator.Default.Allocate<byte>((int)entry.Size);
using (Stream s = entry.OpenEntryStream()) using (Stream s = entry.OpenEntryStream())
s.ReadExactly(owner.Memory.Span); {
if (entry.Size > 0)
{
var owner = MemoryAllocator.Default.Allocate<byte>((int)entry.Size);
s.ReadExactly(owner.Memory.Span);
return new MemoryOwnerMemoryStream(owner);
}
return new MemoryOwnerMemoryStream(owner); // due to a sharpcompress bug (https://github.com/adamhathcock/sharpcompress/issues/88),
// in rare instances the `ZipArchiveEntry` will not contain a correct `Size` but instead report 0.
// this would lead to the block above reading nothing, and the game basically seeing an archive full of empty files.
// since the bug is years old now, and this is a rather rare situation anyways (reported once in years),
// work around this locally by falling back to reading as many bytes as possible and using a standard non-pooled memory stream.
return new MemoryStream(s.ReadAllRemainingBytesToArray());
}
} }
public override void Dispose() public override void Dispose()

View File

@ -144,6 +144,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.EditorIncreaseDistanceSpacing), new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.EditorIncreaseDistanceSpacing),
new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelDown }, GlobalAction.EditorCyclePreviousBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelDown }, GlobalAction.EditorCyclePreviousBeatSnapDivisor),
new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelUp }, GlobalAction.EditorCycleNextBeatSnapDivisor), new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelUp }, GlobalAction.EditorCycleNextBeatSnapDivisor),
new KeyBinding(InputKey.None, GlobalAction.EditorToggleMoveControl),
new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl),
new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl), new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl),
new KeyBinding(new[] { InputKey.Control, InputKey.Left }, GlobalAction.EditorSeekToPreviousHitObject), new KeyBinding(new[] { InputKey.Control, InputKey.Left }, GlobalAction.EditorSeekToPreviousHitObject),
@ -204,7 +205,7 @@ namespace osu.Game.Input.Bindings
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.Up }, GlobalAction.IncreaseModSpeed),
new KeyBinding(new[] { InputKey.Control, InputKey.Down }, GlobalAction.DecreaseModSpeed), new KeyBinding(new[] { InputKey.Control, InputKey.Down }, GlobalAction.DecreaseModSpeed),
new KeyBinding(new[] { InputKey.MouseRight }, GlobalAction.AbsoluteScrollSongList), new KeyBinding(InputKey.None, GlobalAction.AbsoluteScrollSongList),
}; };
private static IEnumerable<KeyBinding> audioControlKeyBindings => new[] private static IEnumerable<KeyBinding> audioControlKeyBindings => new[]
@ -493,7 +494,10 @@ namespace osu.Game.Input.Bindings
EditorSeekToNextBookmark, EditorSeekToNextBookmark,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.AbsoluteScrollSongList))] [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.AbsoluteScrollSongList))]
AbsoluteScrollSongList AbsoluteScrollSongList,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleMoveControl))]
EditorToggleMoveControl,
} }
public enum GlobalActionCategory public enum GlobalActionCategory

View File

@ -454,6 +454,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString AbsoluteScrollSongList => new TranslatableString(getKey(@"absolute_scroll_song_list"), @"Absolute scroll song list"); public static LocalisableString AbsoluteScrollSongList => new TranslatableString(getKey(@"absolute_scroll_song_list"), @"Absolute scroll song list");
/// <summary>
/// "Toggle movement control"
/// </summary>
public static LocalisableString EditorToggleMoveControl => new TranslatableString(getKey(@"editor_toggle_move_control"), @"Toggle movement control");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -31,15 +31,6 @@ namespace osu.Game.Online
[Resolved] [Resolved]
private MetadataClient metadataClient { get; set; } = null!; private MetadataClient metadataClient { get; set; } = null!;
[Resolved]
private ChannelManager channelManager { get; set; } = null!;
[Resolved]
private ChatOverlay chatOverlay { get; set; } = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } = null!; private OsuConfigManager config { get; set; } = null!;
@ -165,24 +156,7 @@ namespace osu.Game.Online
return; return;
} }
APIUser? singleUser = onlineAlertQueue.Count == 1 ? onlineAlertQueue.Single() : null; notifications.Post(new FriendOnlineNotification(onlineAlertQueue.ToArray()));
notifications.Post(new SimpleNotification
{
Icon = FontAwesome.Solid.UserPlus,
Text = $"Online: {string.Join(@", ", onlineAlertQueue.Select(u => u.Username))}",
IconColour = colours.Green,
Activated = () =>
{
if (singleUser != null)
{
channelManager.OpenPrivateChannel(singleUser);
chatOverlay.Show();
}
return true;
}
});
onlineAlertQueue.Clear(); onlineAlertQueue.Clear();
lastOnlineAlertTime = null; lastOnlineAlertTime = null;
@ -202,15 +176,60 @@ namespace osu.Game.Online
return; return;
} }
notifications.Post(new SimpleNotification notifications.Post(new FriendOfflineNotification(offlineAlertQueue.ToArray()));
{
Icon = FontAwesome.Solid.UserMinus,
Text = $"Offline: {string.Join(@", ", offlineAlertQueue.Select(u => u.Username))}",
IconColour = colours.Red
});
offlineAlertQueue.Clear(); offlineAlertQueue.Clear();
lastOfflineAlertTime = null; lastOfflineAlertTime = null;
} }
public partial class FriendOnlineNotification : SimpleNotification
{
private readonly ICollection<APIUser> users;
public FriendOnlineNotification(ICollection<APIUser> users)
{
this.users = users;
Transient = true;
IsImportant = false;
Icon = FontAwesome.Solid.User;
Text = $"Online: {string.Join(@", ", users.Select(u => u.Username))}";
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, ChannelManager channelManager, ChatOverlay chatOverlay)
{
IconColour = colours.GrayD;
Activated = () =>
{
APIUser? singleUser = users.Count == 1 ? users.Single() : null;
if (singleUser != null)
{
channelManager.OpenPrivateChannel(singleUser);
chatOverlay.Show();
}
return true;
};
}
public override string PopInSampleName => "UI/notification-friend-online";
}
private partial class FriendOfflineNotification : SimpleNotification
{
public FriendOfflineNotification(ICollection<APIUser> users)
{
Transient = true;
IsImportant = false;
Icon = FontAwesome.Solid.UserSlash;
Text = $"Offline: {string.Join(@", ", users.Select(u => u.Username))}";
}
[BackgroundDependencyLoader]
private void load(OsuColour colours) => IconColour = colours.Gray3;
public override string PopInSampleName => "UI/notification-friend-offline";
}
} }
} }

View File

@ -54,6 +54,7 @@ namespace osu.Game.Online.Leaderboards
private readonly int? rank; private readonly int? rank;
private readonly bool isOnlineScope; private readonly bool isOnlineScope;
private readonly bool highlightFriend;
private Box background; private Box background;
private Container content; private Container content;
@ -86,12 +87,13 @@ namespace osu.Game.Online.Leaderboards
[Resolved] [Resolved]
private ScoreManager scoreManager { get; set; } = null!; private ScoreManager scoreManager { get; set; } = null!;
public LeaderboardScore(ScoreInfo score, int? rank, bool isOnlineScope = true) public LeaderboardScore(ScoreInfo score, int? rank, bool isOnlineScope = true, bool highlightFriend = true)
{ {
Score = score; Score = score;
this.rank = rank; this.rank = rank;
this.isOnlineScope = isOnlineScope; this.isOnlineScope = isOnlineScope;
this.highlightFriend = highlightFriend;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Height = HEIGHT; Height = HEIGHT;
@ -130,7 +132,7 @@ namespace osu.Game.Online.Leaderboards
background = new Box background = new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = isUserFriend ? colour.Yellow : (user.OnlineID == api.LocalUser.Value.Id && isOnlineScope ? colour.Green : Color4.Black), Colour = (highlightFriend && isUserFriend) ? colour.Yellow : (user.OnlineID == api.LocalUser.Value.Id && isOnlineScope ? colour.Green : Color4.Black),
Alpha = background_alpha, Alpha = background_alpha,
}, },
}, },

View File

@ -37,5 +37,17 @@ namespace osu.Game.Online.Spectator
/// <param name="userId">The ID of the user who achieved the score.</param> /// <param name="userId">The ID of the user who achieved the score.</param>
/// <param name="scoreId">The ID of the score.</param> /// <param name="scoreId">The ID of the score.</param>
Task UserScoreProcessed(int userId, long scoreId); Task UserScoreProcessed(int userId, long scoreId);
/// <summary>
/// Signals that another user has <see cref="ISpectatorServer.StartWatchingUser">started watching this client</see>.
/// </summary>
/// <param name="user">The information about the user who started watching.</param>
Task UserStartedWatching(SpectatorUser[] user);
/// <summary>
/// Signals that another user has <see cref="ISpectatorServer.EndWatchingUser">ended watching this client</see>
/// </summary>
/// <param name="userId">The ID of the user who ended watching.</param>
Task UserEndedWatching(int userId);
} }
} }

View File

@ -42,6 +42,8 @@ namespace osu.Game.Online.Spectator
connection.On<int, FrameDataBundle>(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); connection.On<int, FrameDataBundle>(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames);
connection.On<int, SpectatorState>(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); connection.On<int, SpectatorState>(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying);
connection.On<int, long>(nameof(ISpectatorClient.UserScoreProcessed), ((ISpectatorClient)this).UserScoreProcessed); connection.On<int, long>(nameof(ISpectatorClient.UserScoreProcessed), ((ISpectatorClient)this).UserScoreProcessed);
connection.On<SpectatorUser[]>(nameof(ISpectatorClient.UserStartedWatching), ((ISpectatorClient)this).UserStartedWatching);
connection.On<int>(nameof(ISpectatorClient.UserEndedWatching), ((ISpectatorClient)this).UserEndedWatching);
connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IStatefulUserHubClient)this).DisconnectRequested); connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IStatefulUserHubClient)this).DisconnectRequested);
}; };

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Development; using osu.Framework.Development;
@ -36,10 +37,16 @@ namespace osu.Game.Online.Spectator
public abstract IBindable<bool> IsConnected { get; } public abstract IBindable<bool> IsConnected { get; }
/// <summary> /// <summary>
/// The states of all users currently being watched. /// The states of all users currently being watched by the local user.
/// </summary> /// </summary>
[UsedImplicitly] // Marked virtual due to mock use in testing
public virtual IBindableDictionary<int, SpectatorState> WatchedUserStates => watchedUserStates; public virtual IBindableDictionary<int, SpectatorState> WatchedUserStates => watchedUserStates;
/// <summary>
/// All users who are currently watching the local user.
/// </summary>
public IBindableList<SpectatorUser> WatchingUsers => watchingUsers;
/// <summary> /// <summary>
/// A global list of all players currently playing. /// A global list of all players currently playing.
/// </summary> /// </summary>
@ -53,6 +60,7 @@ namespace osu.Game.Online.Spectator
/// <summary> /// <summary>
/// Called whenever new frames arrive from the server. /// Called whenever new frames arrive from the server.
/// </summary> /// </summary>
[UsedImplicitly] // Marked virtual due to mock use in testing
public virtual event Action<int, FrameDataBundle>? OnNewFrames; public virtual event Action<int, FrameDataBundle>? OnNewFrames;
/// <summary> /// <summary>
@ -82,6 +90,7 @@ namespace osu.Game.Online.Spectator
private readonly BindableDictionary<int, SpectatorState> watchedUserStates = new BindableDictionary<int, SpectatorState>(); private readonly BindableDictionary<int, SpectatorState> watchedUserStates = new BindableDictionary<int, SpectatorState>();
private readonly BindableList<SpectatorUser> watchingUsers = new BindableList<SpectatorUser>();
private readonly BindableList<int> playingUsers = new BindableList<int>(); private readonly BindableList<int> playingUsers = new BindableList<int>();
private readonly SpectatorState currentState = new SpectatorState(); private readonly SpectatorState currentState = new SpectatorState();
@ -127,6 +136,7 @@ namespace osu.Game.Online.Spectator
{ {
playingUsers.Clear(); playingUsers.Clear();
watchedUserStates.Clear(); watchedUserStates.Clear();
watchingUsers.Clear();
} }
}), true); }), true);
} }
@ -179,6 +189,30 @@ namespace osu.Game.Online.Spectator
return Task.CompletedTask; return Task.CompletedTask;
} }
Task ISpectatorClient.UserStartedWatching(SpectatorUser[] users)
{
Schedule(() =>
{
foreach (var user in users)
{
if (!watchingUsers.Contains(user))
watchingUsers.Add(user);
}
});
return Task.CompletedTask;
}
Task ISpectatorClient.UserEndedWatching(int userId)
{
Schedule(() =>
{
watchingUsers.RemoveAll(u => u.OnlineID == userId);
});
return Task.CompletedTask;
}
Task IStatefulUserHubClient.DisconnectRequested() Task IStatefulUserHubClient.DisconnectRequested()
{ {
Schedule(() => DisconnectInternal()); Schedule(() => DisconnectInternal());

View File

@ -0,0 +1,39 @@
// 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;
using osu.Game.Users;
namespace osu.Game.Online.Spectator
{
[Serializable]
[MessagePackObject]
public class SpectatorUser : IUser, IEquatable<SpectatorUser>
{
[Key(0)]
public int OnlineID { get; set; }
[Key(1)]
public string Username { get; set; } = string.Empty;
[IgnoreMember]
public CountryCode CountryCode => CountryCode.Unknown;
[IgnoreMember]
public bool IsBot => false;
public bool Equals(SpectatorUser? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return OnlineID == other.OnlineID;
}
public override bool Equals(object? obj) => Equals(obj as SpectatorUser);
// ReSharper disable once NonReadonlyMemberInGetHashCode
public override int GetHashCode() => OnlineID;
}
}

View File

@ -178,9 +178,9 @@ namespace osu.Game
/// </summary> /// </summary>
private readonly IBindable<bool> backButtonVisibility = new Bindable<bool>(); private readonly IBindable<bool> backButtonVisibility = new Bindable<bool>();
IBindable<LocalUserPlayingState> ILocalUserPlayInfo.PlayingState => playingState; IBindable<LocalUserPlayingState> ILocalUserPlayInfo.PlayingState => UserPlayingState;
private readonly Bindable<LocalUserPlayingState> playingState = new Bindable<LocalUserPlayingState>(); protected readonly Bindable<LocalUserPlayingState> UserPlayingState = new Bindable<LocalUserPlayingState>();
protected OsuScreenStack ScreenStack; protected OsuScreenStack ScreenStack;
@ -233,8 +233,6 @@ namespace osu.Game
forwardGeneralLogsToNotifications(); forwardGeneralLogsToNotifications();
forwardTabletLogsToNotifications(); forwardTabletLogsToNotifications();
SentryLogger = new SentryLogger(this);
} }
#region IOverlayManager #region IOverlayManager
@ -308,7 +306,7 @@ namespace osu.Game
protected override UserInputManager CreateUserInputManager() protected override UserInputManager CreateUserInputManager()
{ {
var userInputManager = base.CreateUserInputManager(); var userInputManager = base.CreateUserInputManager();
(userInputManager as OsuUserInputManager)?.PlayingState.BindTo(playingState); (userInputManager as OsuUserInputManager)?.PlayingState.BindTo(UserPlayingState);
return userInputManager; return userInputManager;
} }
@ -320,6 +318,12 @@ namespace osu.Game
private readonly List<string> dragDropFiles = new List<string>(); private readonly List<string> dragDropFiles = new List<string>();
private ScheduledDelegate dragDropImportSchedule; private ScheduledDelegate dragDropImportSchedule;
public override void SetupLogging(Storage gameStorage, Storage cacheStorage)
{
base.SetupLogging(gameStorage, cacheStorage);
SentryLogger = new SentryLogger(this, cacheStorage);
}
public override void SetHost(GameHost host) public override void SetHost(GameHost host)
{ {
base.SetHost(host); base.SetHost(host);
@ -403,7 +407,7 @@ namespace osu.Game
// Transfer any runtime changes back to configuration file. // Transfer any runtime changes back to configuration file.
SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID.ToString(); SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID.ToString();
playingState.BindValueChanged(p => UserPlayingState.BindValueChanged(p =>
{ {
BeatmapManager.PauseImports = p.NewValue != LocalUserPlayingState.NotPlaying; BeatmapManager.PauseImports = p.NewValue != LocalUserPlayingState.NotPlaying;
SkinManager.PauseImports = p.NewValue != LocalUserPlayingState.NotPlaying; SkinManager.PauseImports = p.NewValue != LocalUserPlayingState.NotPlaying;
@ -1544,7 +1548,7 @@ namespace osu.Game
GlobalCursorDisplay.ShowCursor = (ScreenStack.CurrentScreen as IOsuScreen)?.CursorVisible ?? false; GlobalCursorDisplay.ShowCursor = (ScreenStack.CurrentScreen as IOsuScreen)?.CursorVisible ?? false;
} }
private void screenChanged(IScreen current, IScreen newScreen) protected virtual void ScreenChanged([CanBeNull] IOsuScreen current, [CanBeNull] IOsuScreen newScreen)
{ {
SentrySdk.ConfigureScope(scope => SentrySdk.ConfigureScope(scope =>
{ {
@ -1560,10 +1564,10 @@ namespace osu.Game
switch (current) switch (current)
{ {
case Player player: case Player player:
player.PlayingState.UnbindFrom(playingState); player.PlayingState.UnbindFrom(UserPlayingState);
// reset for sanity. // reset for sanity.
playingState.Value = LocalUserPlayingState.NotPlaying; UserPlayingState.Value = LocalUserPlayingState.NotPlaying;
break; break;
} }
@ -1580,7 +1584,7 @@ namespace osu.Game
break; break;
case Player player: case Player player:
player.PlayingState.BindTo(playingState); player.PlayingState.BindTo(UserPlayingState);
break; break;
default: default:
@ -1588,30 +1592,32 @@ namespace osu.Game
break; break;
} }
if (current is IOsuScreen currentOsuScreen) if (current != null)
{ {
backButtonVisibility.UnbindFrom(currentOsuScreen.BackButtonVisibility); backButtonVisibility.UnbindFrom(current.BackButtonVisibility);
OverlayActivationMode.UnbindFrom(currentOsuScreen.OverlayActivationMode); OverlayActivationMode.UnbindFrom(current.OverlayActivationMode);
configUserActivity.UnbindFrom(currentOsuScreen.Activity); configUserActivity.UnbindFrom(current.Activity);
} }
if (newScreen is IOsuScreen newOsuScreen) // Bind to new screen.
if (newScreen != null)
{ {
backButtonVisibility.BindTo(newOsuScreen.BackButtonVisibility); backButtonVisibility.BindTo(newScreen.BackButtonVisibility);
OverlayActivationMode.BindTo(newOsuScreen.OverlayActivationMode); OverlayActivationMode.BindTo(newScreen.OverlayActivationMode);
configUserActivity.BindTo(newOsuScreen.Activity); configUserActivity.BindTo(newScreen.Activity);
GlobalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = newOsuScreen.HideMenuCursorOnNonMouseInput; // Handle various configuration updates based on new screen settings.
GlobalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = newScreen.HideMenuCursorOnNonMouseInput;
if (newOsuScreen.HideOverlaysOnEnter) if (newScreen.HideOverlaysOnEnter)
CloseAllOverlays(); CloseAllOverlays();
else else
Toolbar.Show(); Toolbar.Show();
if (newOsuScreen.ShowFooter) if (newScreen.ShowFooter)
{ {
BackButton.Hide(); BackButton.Hide();
ScreenFooter.SetButtons(newOsuScreen.CreateFooterButtons()); ScreenFooter.SetButtons(newScreen.CreateFooterButtons());
ScreenFooter.Show(); ScreenFooter.Show();
} }
else else
@ -1619,16 +1625,16 @@ namespace osu.Game
ScreenFooter.SetButtons(Array.Empty<ScreenFooterButton>()); ScreenFooter.SetButtons(Array.Empty<ScreenFooterButton>());
ScreenFooter.Hide(); ScreenFooter.Hide();
} }
}
skinEditor.SetTarget((OsuScreen)newScreen); skinEditor.SetTarget((OsuScreen)newScreen);
}
} }
private void screenPushed(IScreen lastScreen, IScreen newScreen) => screenChanged(lastScreen, newScreen); private void screenPushed(IScreen lastScreen, IScreen newScreen) => ScreenChanged((OsuScreen)lastScreen, (OsuScreen)newScreen);
private void screenExited(IScreen lastScreen, IScreen newScreen) private void screenExited(IScreen lastScreen, IScreen newScreen)
{ {
screenChanged(lastScreen, newScreen); ScreenChanged((OsuScreen)lastScreen, (OsuScreen)newScreen);
if (newScreen == null) if (newScreen == null)
Exit(); Exit();

View File

@ -21,7 +21,7 @@ using Realms;
namespace osu.Game.Overlays.FirstRunSetup namespace osu.Game.Overlays.FirstRunSetup
{ {
[LocalisableDescription(typeof(FirstRunSetupBeatmapScreenStrings), nameof(FirstRunSetupBeatmapScreenStrings.Header))] [LocalisableDescription(typeof(FirstRunSetupBeatmapScreenStrings), nameof(FirstRunSetupBeatmapScreenStrings.Header))]
public partial class ScreenBeatmaps : FirstRunSetupScreen public partial class ScreenBeatmaps : WizardScreen
{ {
private ProgressRoundedButton downloadBundledButton = null!; private ProgressRoundedButton downloadBundledButton = null!;
private ProgressRoundedButton downloadTutorialButton = null!; private ProgressRoundedButton downloadTutorialButton = null!;

View File

@ -20,7 +20,7 @@ using osu.Game.Overlays.Settings.Sections;
namespace osu.Game.Overlays.FirstRunSetup namespace osu.Game.Overlays.FirstRunSetup
{ {
[LocalisableDescription(typeof(FirstRunSetupOverlayStrings), nameof(FirstRunSetupOverlayStrings.Behaviour))] [LocalisableDescription(typeof(FirstRunSetupOverlayStrings), nameof(FirstRunSetupOverlayStrings.Behaviour))]
public partial class ScreenBehaviour : FirstRunSetupScreen public partial class ScreenBehaviour : WizardScreen
{ {
private SearchContainer<SettingsSection> searchContainer; private SearchContainer<SettingsSection> searchContainer;

View File

@ -31,7 +31,7 @@ using osuTK;
namespace osu.Game.Overlays.FirstRunSetup namespace osu.Game.Overlays.FirstRunSetup
{ {
[LocalisableDescription(typeof(FirstRunOverlayImportFromStableScreenStrings), nameof(FirstRunOverlayImportFromStableScreenStrings.Header))] [LocalisableDescription(typeof(FirstRunOverlayImportFromStableScreenStrings), nameof(FirstRunOverlayImportFromStableScreenStrings.Header))]
public partial class ScreenImportFromStable : FirstRunSetupScreen public partial class ScreenImportFromStable : WizardScreen
{ {
private static readonly Vector2 button_size = new Vector2(400, 50); private static readonly Vector2 button_size = new Vector2(400, 50);

View File

@ -32,7 +32,7 @@ using osuTK;
namespace osu.Game.Overlays.FirstRunSetup namespace osu.Game.Overlays.FirstRunSetup
{ {
[LocalisableDescription(typeof(GraphicsSettingsStrings), nameof(GraphicsSettingsStrings.UIScaling))] [LocalisableDescription(typeof(GraphicsSettingsStrings), nameof(GraphicsSettingsStrings.UIScaling))]
public partial class ScreenUIScale : FirstRunSetupScreen public partial class ScreenUIScale : WizardScreen
{ {
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config)

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