mirror of
https://github.com/ppy/osu.git
synced 2025-02-08 06:22:55 +08:00
Merge branch 'master' into multiplayer-free-style
This commit is contained in:
commit
a93dabd33f
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.115.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.129.1" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -49,6 +49,8 @@ namespace osu.Android
|
||||
/// <remarks>Adjusted on startup to match expected UX for the current device type (phone/tablet).</remarks>
|
||||
public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified;
|
||||
|
||||
public new bool IsTablet { get; private set; }
|
||||
|
||||
private readonly OsuGameAndroid game;
|
||||
|
||||
private bool gameCreated;
|
||||
@ -89,9 +91,9 @@ namespace osu.Android
|
||||
WindowManager.DefaultDisplay.GetSize(displaySize);
|
||||
#pragma warning restore CA1422
|
||||
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.
|
||||
// The assembly files are not available as files either after native AOT.
|
||||
|
@ -3,11 +3,13 @@
|
||||
|
||||
using System;
|
||||
using Android.App;
|
||||
using Android.Content.PM;
|
||||
using Microsoft.Maui.Devices;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Updater;
|
||||
using osu.Game.Utils;
|
||||
|
||||
@ -71,7 +73,35 @@ namespace osu.Android
|
||||
protected override void 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)
|
||||
|
@ -12,7 +12,6 @@ using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Tests.Visual;
|
||||
@ -71,11 +70,11 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
contentContainer.Playfield.HitObjectContainer.Add(hitObject);
|
||||
}
|
||||
|
||||
protected override SnapResult SnapForBlueprint(HitObjectPlacementBlueprint blueprint)
|
||||
protected override void UpdatePlacementTimeAndPosition()
|
||||
{
|
||||
var result = base.SnapForBlueprint(blueprint);
|
||||
result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP;
|
||||
return result;
|
||||
var position = InputManager.CurrentState.Mouse.Position;
|
||||
double time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(position) / TIME_SNAP) * TIME_SNAP;
|
||||
CurrentBlueprint.UpdateTimeAndPosition(position, time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Difficulty;
|
||||
using osu.Game.Rulesets.Catch.Edit;
|
||||
using osu.Game.Rulesets.Catch.Edit.Setup;
|
||||
using osu.Game.Rulesets.Catch.Mods;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Replays;
|
||||
@ -228,7 +229,7 @@ namespace osu.Game.Rulesets.Catch
|
||||
public override IEnumerable<Drawable> CreateEditorSetupSections() =>
|
||||
[
|
||||
new MetadataSection(),
|
||||
new DifficultySection(),
|
||||
new CatchDifficultySection(),
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Y,
|
||||
|
@ -7,6 +7,7 @@ using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
@ -59,11 +60,13 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
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)
|
||||
{
|
||||
@ -78,6 +81,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
|
||||
HitObject.StartTime = Math.Min(placementStartTime, placementEndTime);
|
||||
HitObject.EndTime = Math.Max(placementStartTime, placementEndTime);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ using osu.Game.Rulesets.UI.Scrolling;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
public partial class CatchPlacementBlueprint<THitObject> : HitObjectPlacementBlueprint
|
||||
public abstract partial class CatchPlacementBlueprint<THitObject> : HitObjectPlacementBlueprint
|
||||
where THitObject : CatchHitObject, new()
|
||||
{
|
||||
protected new THitObject HitObject => (THitObject)base.HitObject;
|
||||
@ -19,7 +19,10 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
[Resolved]
|
||||
private Playfield playfield { get; set; } = null!;
|
||||
|
||||
public CatchPlacementBlueprint()
|
||||
[Resolved]
|
||||
protected CatchHitObjectComposer? Composer { get; private set; }
|
||||
|
||||
protected CatchPlacementBlueprint()
|
||||
: base(new THitObject())
|
||||
{
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
@ -41,11 +42,20 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
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;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -83,8 +83,16 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
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)
|
||||
{
|
||||
case PlacementState.Waiting:
|
||||
@ -99,7 +107,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Make sure the up-to-date position is used for outlines.
|
||||
@ -113,6 +121,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
ApplyDefaultsToHitObject();
|
||||
scrollingPath.UpdatePathFrom(HitObjectContainer, HitObject);
|
||||
nestedOutlineContainer.UpdateNestedObjectsFrom(HitObjectContainer, HitObject);
|
||||
return result;
|
||||
}
|
||||
|
||||
private double positionToTime(float relativeYPosition)
|
||||
|
@ -1,16 +1,22 @@
|
||||
// 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 osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public partial class CatchBlueprintContainer : ComposeBlueprintContainer
|
||||
{
|
||||
public new CatchHitObjectComposer Composer => (CatchHitObjectComposer)base.Composer;
|
||||
|
||||
public CatchBlueprintContainer(CatchHitObjectComposer composer)
|
||||
: base(composer)
|
||||
{
|
||||
@ -36,5 +42,28 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,9 +23,10 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
[Cached]
|
||||
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!;
|
||||
|
||||
@ -135,22 +136,12 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
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;
|
||||
|
||||
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;
|
||||
return null;
|
||||
}
|
||||
|
||||
private PalpableCatchHitObject? getLastSnappableHitObject(double time)
|
||||
|
125
osu.Game.Rulesets.Catch/Edit/Setup/CatchDifficultySection.cs
Normal file
125
osu.Game.Rulesets.Catch/Edit/Setup/CatchDifficultySection.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -9,7 +9,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
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);
|
||||
var pos = column.ScreenSpacePositionAtTime(time);
|
||||
|
||||
return new SnapResult(pos, time, column);
|
||||
CurrentBlueprint.UpdateTimeAndPosition(pos, time);
|
||||
}
|
||||
|
||||
protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both };
|
||||
|
@ -20,7 +20,6 @@ using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
@ -100,10 +99,5 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
set => InternalChild = value;
|
||||
}
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
Mod = new ManiaModFadeIn(),
|
||||
PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE)
|
||||
});
|
||||
}
|
||||
@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
Mod = new ManiaModFadeIn(),
|
||||
PassCondition = () => checkCoverage(ManiaModHidden.MIN_COVERAGE)
|
||||
});
|
||||
|
||||
@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
Mod = new ManiaModFadeIn(),
|
||||
PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE)
|
||||
});
|
||||
|
||||
@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
Mod = new ManiaModFadeIn(),
|
||||
PassCondition = () => checkCoverage(ManiaModHidden.MAX_COVERAGE)
|
||||
});
|
||||
|
||||
@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Mods
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ManiaModHidden(),
|
||||
Mod = new ManiaModFadeIn(),
|
||||
CreateBeatmap = () => new Beatmap
|
||||
{
|
||||
HitObjects = Enumerable.Range(1, 100).Select(i => (HitObject)new Note { StartTime = 1000 + 200 * i }).ToList(),
|
||||
|
@ -28,18 +28,20 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
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)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Width = 0.5f,
|
||||
Child = new ColumnHitObjectArea(new HitObjectContainer())
|
||||
Child = new ColumnHitObjectArea
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new HitObjectContainer(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
68
osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs
Normal file
68
osu.Game.Rulesets.Mania.Tests/TestSceneManiaTouchInput.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -98,9 +98,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
|
||||
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)
|
||||
{
|
||||
@ -121,6 +121,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
if (result.Time is double startTime)
|
||||
originalStartTime = HitObject.StartTime = startTime;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
@ -24,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
private EditorBeatmap? editorBeatmap { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IPositionSnapProvider? positionSnapProvider { get; set; }
|
||||
private ManiaHitObjectComposer? positionSnapProvider { get; set; }
|
||||
|
||||
private EditBodyPiece body = null!;
|
||||
private EditHoldNoteEndPiece head = null!;
|
||||
|
@ -1,8 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
@ -20,13 +20,18 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
{
|
||||
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;
|
||||
set
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
|
||||
if (value == column)
|
||||
return;
|
||||
|
||||
@ -53,9 +58,11 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
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)
|
||||
{
|
||||
@ -76,6 +83,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
|
||||
if (PlacementActive == PlacementState.Waiting)
|
||||
Column = col;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private float getNoteHeight(Column resultPlayfield) =>
|
||||
|
@ -8,6 +8,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
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)
|
||||
{
|
||||
piece.Width = result.Playfield.DrawWidth;
|
||||
piece.Position = ToLocalSpace(result.ScreenSpacePosition);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
|
@ -1,17 +1,23 @@
|
||||
// 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 osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit
|
||||
{
|
||||
public partial class ManiaBlueprintContainer : ComposeBlueprintContainer
|
||||
{
|
||||
public ManiaBlueprintContainer(HitObjectComposer composer)
|
||||
public new ManiaHitObjectComposer Composer => (ManiaHitObjectComposer)base.Composer;
|
||||
|
||||
public ManiaBlueprintContainer(ManiaHitObjectComposer composer)
|
||||
: base(composer)
|
||||
{
|
||||
}
|
||||
@ -33,5 +39,22 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new ManiaSelectionHandler();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit
|
||||
{
|
||||
[Cached]
|
||||
public partial class ManiaHitObjectComposer : ScrollingHitObjectComposer<ManiaHitObject>
|
||||
{
|
||||
private DrawableManiaEditorRuleset drawableRuleset = null!;
|
||||
|
@ -8,7 +8,7 @@ using osu.Game.Rulesets.UI;
|
||||
|
||||
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 ManiaInputManager(RulesetInfo ruleset, int variant)
|
||||
|
@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
|
||||
foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns))
|
||||
{
|
||||
HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer;
|
||||
HitObjectContainer hoc = column.HitObjectContainer;
|
||||
Container hocParent = (Container)hoc.Parent!;
|
||||
|
||||
hocParent.Remove(hoc, false);
|
||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
{
|
||||
public partial class ArgonJudgementPiece : TextJudgementPiece, IAnimatableJudgement
|
||||
{
|
||||
private const float judgement_y_position = 160;
|
||||
private const float judgement_y_position = -180f;
|
||||
|
||||
private RingExplosion? ringExplosion;
|
||||
|
||||
|
@ -7,7 +7,6 @@ using osu.Framework.Graphics.Animations;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
@ -23,7 +22,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
this.result = result;
|
||||
this.animation = animation;
|
||||
|
||||
Anchor = Anchor.Centre;
|
||||
Anchor = Anchor.BottomCentre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
@ -32,12 +31,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
[BackgroundDependencyLoader]
|
||||
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)
|
||||
scorePosition -= Stage.HIT_TARGET_POSITION + 150;
|
||||
|
||||
Y = scorePosition ?? 0;
|
||||
float absoluteHitPosition = 480f * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR - hitPosition;
|
||||
Y = scorePosition - absoluteHitPosition;
|
||||
|
||||
InternalChild = animation.With(d =>
|
||||
{
|
||||
|
@ -1,10 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using 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.Pooling;
|
||||
@ -45,11 +44,11 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
internal readonly Container TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
private DrawablePool<PoolableHitExplosion> hitExplosionPool;
|
||||
private DrawablePool<PoolableHitExplosion> hitExplosionPool = null!;
|
||||
private readonly OrderedHitPolicy hitPolicy;
|
||||
public Container UnderlayElements => HitObjectArea.UnderlayElements;
|
||||
|
||||
private GameplaySampleTriggerSource sampleTriggerSource;
|
||||
private GameplaySampleTriggerSource sampleTriggerSource = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this is a special (ie. scratch) column.
|
||||
@ -67,11 +66,15 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
Width = COLUMN_WIDTH;
|
||||
|
||||
hitPolicy = new OrderedHitPolicy(HitObjectContainer);
|
||||
HitObjectArea = new ColumnHitObjectArea(HitObjectContainer) { RelativeSizeAxes = Axes.Both };
|
||||
HitObjectArea = new ColumnHitObjectArea
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = HitObjectContainer,
|
||||
};
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private ISkinSource skin { get; set; }
|
||||
private ISkinSource skin { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host)
|
||||
@ -132,7 +135,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (skin != null)
|
||||
if (skin.IsNotNull())
|
||||
skin.SourceChanged -= onSourceChanged;
|
||||
}
|
||||
|
||||
@ -180,5 +183,29 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
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
|
||||
=> 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
|
||||
}
|
||||
}
|
||||
|
@ -3,13 +3,12 @@
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI.Components
|
||||
{
|
||||
public partial class ColumnHitObjectArea : HitObjectArea
|
||||
public partial class ColumnHitObjectArea : HitPositionPaddedContainer
|
||||
{
|
||||
public readonly Container Explosions;
|
||||
|
||||
@ -17,25 +16,29 @@ namespace osu.Game.Rulesets.Mania.UI.Components
|
||||
|
||||
private readonly Drawable hitTarget;
|
||||
|
||||
public ColumnHitObjectArea(HitObjectContainer hitObjectContainer)
|
||||
: base(hitObjectContainer)
|
||||
protected override Container<Drawable> Content => content;
|
||||
|
||||
private readonly Container content;
|
||||
|
||||
public ColumnHitObjectArea()
|
||||
{
|
||||
AddRangeInternal(new[]
|
||||
{
|
||||
UnderlayElements = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = 2,
|
||||
},
|
||||
hitTarget = new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.HitTarget), _ => new DefaultHitTarget())
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Depth = 1
|
||||
},
|
||||
content = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
Explosions = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = -1,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Mania.Skinning;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
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>();
|
||||
public readonly HitObjectContainer HitObjectContainer;
|
||||
|
||||
public HitObjectArea(HitObjectContainer hitObjectContainer)
|
||||
{
|
||||
InternalChild = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = HitObjectContainer = hitObjectContainer
|
||||
};
|
||||
}
|
||||
[Resolved]
|
||||
private ISkinSource skin { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IScrollingInfo scrollingInfo)
|
||||
{
|
||||
Direction.BindTo(scrollingInfo.Direction);
|
||||
Direction.BindValueChanged(onDirectionChanged, true);
|
||||
Direction.BindValueChanged(_ => UpdateHitPosition(), true);
|
||||
|
||||
skin.SourceChanged += onSkinChanged;
|
||||
}
|
||||
|
||||
protected override void SkinChanged(ISkinSource skin)
|
||||
{
|
||||
base.SkinChanged(skin);
|
||||
UpdateHitPosition();
|
||||
}
|
||||
|
||||
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
|
||||
{
|
||||
UpdateHitPosition();
|
||||
}
|
||||
private void onSkinChanged() => UpdateHitPosition();
|
||||
|
||||
protected virtual void UpdateHitPosition()
|
||||
{
|
||||
float hitPosition = CurrentSkin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
float hitPosition = skin.GetConfig<ManiaSkinConfigurationLookup, float>(
|
||||
new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.HitPosition))?.Value
|
||||
?? Stage.HIT_TARGET_POSITION;
|
||||
|
||||
@ -54,5 +40,13 @@ namespace osu.Game.Rulesets.Mania.UI.Components
|
||||
? new MarginPadding { Top = hitPosition }
|
||||
: new MarginPadding { Bottom = hitPosition };
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (skin.IsNotNull())
|
||||
skin.SourceChanged -= onSkinChanged;
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
@ -15,9 +16,12 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
private partial class DefaultManiaJudgementPiece : DefaultJudgementPiece
|
||||
{
|
||||
private const float judgement_y_position = -180f;
|
||||
|
||||
public DefaultManiaJudgementPiece(HitResult result)
|
||||
: base(result)
|
||||
{
|
||||
Y = judgement_y_position;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -32,8 +36,20 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
switch (Result)
|
||||
{
|
||||
case HitResult.None:
|
||||
this.FadeOutFromOne(800);
|
||||
break;
|
||||
|
||||
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;
|
||||
|
||||
default:
|
||||
@ -43,8 +59,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
this.Delay(50)
|
||||
.ScaleTo(0.75f, 250)
|
||||
.FadeOut(200);
|
||||
|
||||
// osu!mania uses a custom fade length, so the base call is intentionally omitted.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,6 @@ using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
[Cached]
|
||||
public partial class DrawableManiaRuleset : DrawableScrollingRuleset<ManiaHitObject>
|
||||
{
|
||||
/// <summary>
|
||||
@ -51,6 +50,8 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
public IEnumerable<BarLine> BarLines;
|
||||
|
||||
public override bool RequiresPortraitOrientation => Beatmap.Stages.Count == 1;
|
||||
|
||||
protected override bool RelativeScaleBeatLengths => true;
|
||||
|
||||
protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config;
|
||||
@ -110,8 +111,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue));
|
||||
|
||||
TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value);
|
||||
|
||||
KeyBindingInputManager.Add(new ManiaTouchInputArea());
|
||||
}
|
||||
|
||||
protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount;
|
||||
@ -162,7 +161,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
/// <returns>The scroll time.</returns>
|
||||
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);
|
||||
|
||||
|
@ -1,17 +1,63 @@
|
||||
// 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 osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
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;
|
||||
Origin = Anchor.Centre;
|
||||
this.drawableManiaRuleset = drawableManiaRuleset;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -103,12 +103,13 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
Width = 1366, // Bar lines should only be masked on the vertical axis
|
||||
BypassAutoSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Child = barLineContainer = new HitObjectArea(HitObjectContainer)
|
||||
Child = barLineContainer = new HitPositionPaddedContainer
|
||||
{
|
||||
Name = "Bar lines",
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Child = HitObjectContainer,
|
||||
}
|
||||
},
|
||||
columnFlow = new ColumnFlow<Column>(definition)
|
||||
@ -119,12 +120,13 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
judgements = new JudgementContainer<DrawableManiaJudgement>
|
||||
new HitPositionPaddedContainer
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Y = HIT_TARGET_POSITION + 150
|
||||
Child = judgements = new JudgementContainer<DrawableManiaJudgement>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
},
|
||||
topLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
|
||||
}
|
||||
@ -218,7 +220,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
j.Apply(result, judgedObject);
|
||||
|
||||
j.Anchor = Anchor.Centre;
|
||||
j.Anchor = Anchor.BottomCentre;
|
||||
j.Origin = Anchor.Centre;
|
||||
})!);
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
|
||||
public partial class GridPlacementBlueprint : PlacementBlueprint
|
||||
{
|
||||
[Resolved]
|
||||
private HitObjectComposer? hitObjectComposer { get; set; }
|
||||
private OsuHitObjectComposer? hitObjectComposer { get; set; }
|
||||
|
||||
private OsuGridToolboxGroup gridToolboxGroup = null!;
|
||||
private Vector2 originalOrigin;
|
||||
@ -95,12 +95,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
|
||||
base.OnDragEnd(e);
|
||||
}
|
||||
|
||||
public override SnapType SnapType => ~SnapType.GlobalGrids;
|
||||
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime)
|
||||
{
|
||||
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);
|
||||
|
||||
@ -120,6 +120,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
|
||||
gridToolboxGroup.SetGridFromPoints(gridToolboxGroup.StartPosition.Value, pos);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
|
@ -1,10 +1,15 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
||||
@ -15,12 +20,26 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
||||
|
||||
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()
|
||||
: base(new HitCircle())
|
||||
{
|
||||
InternalChild = circlePiece = new HitCirclePiece();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
limitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
@ -45,10 +64,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
|
||||
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);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Humanizer;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -20,6 +21,7 @@ using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -48,11 +50,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
public Action<List<PathControlPoint>> SplitControlPointsRequested;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IPositionSnapProvider positionSnapProvider { get; set; }
|
||||
[CanBeNull]
|
||||
private OsuHitObjectComposer positionSnapProvider { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IDistanceSnapProvider distanceSnapProvider { get; set; }
|
||||
|
||||
private Bindable<bool> limitedDistanceSnap { get; set; } = null!;
|
||||
|
||||
public PathControlPointVisualiser(T hitObject, bool allowSelection)
|
||||
{
|
||||
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()
|
||||
{
|
||||
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
|
||||
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.StartTime = result?.Time ?? hitObject.StartTime;
|
||||
hitObject.StartTime = result.Time ?? hitObject.StartTime;
|
||||
|
||||
for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++)
|
||||
{
|
||||
@ -453,7 +469,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
}
|
||||
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;
|
||||
|
||||
|
@ -5,10 +5,12 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
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;
|
||||
|
||||
[Resolved]
|
||||
private OsuHitObjectComposer? composer { get; set; }
|
||||
|
||||
private SliderBodyPiece bodyPiece = null!;
|
||||
private HitCirclePiece headCirclePiece = null!;
|
||||
private HitCirclePiece tailCirclePiece = null!;
|
||||
@ -40,15 +45,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
private int currentSegmentLength;
|
||||
private bool usingCustomSegmentType;
|
||||
|
||||
[Resolved]
|
||||
private IPositionSnapProvider? positionSnapProvider { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IDistanceSnapProvider? distanceSnapProvider { get; set; }
|
||||
|
||||
[Resolved]
|
||||
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 };
|
||||
|
||||
protected override bool IsValidForPlacement => HitObject.Path.HasValidLength;
|
||||
@ -63,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
@ -74,6 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
};
|
||||
|
||||
state = SliderPlacementState.Initial;
|
||||
limitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -106,9 +114,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
[Resolved]
|
||||
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)
|
||||
{
|
||||
@ -131,6 +145,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
updateCursor();
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
@ -375,7 +391,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,12 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
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.Objects;
|
||||
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.Objects;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
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)
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
limitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
|
||||
}
|
||||
|
||||
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new OsuSelectionHandler();
|
||||
|
||||
public override HitObjectSelectionBlueprint? CreateHitObjectBlueprintFor(HitObject hitObject)
|
||||
@ -36,5 +53,68 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
@ -31,6 +32,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
[Cached]
|
||||
public partial class OsuHitObjectComposer : HitObjectComposer<OsuHitObject>
|
||||
{
|
||||
public OsuHitObjectComposer(Ruleset ruleset)
|
||||
@ -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))
|
||||
{
|
||||
// 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 (!snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
|
||||
return null;
|
||||
|
||||
if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null)
|
||||
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))
|
||||
{
|
||||
if (DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
|
||||
{
|
||||
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
|
||||
return snapResult;
|
||||
}
|
||||
|
||||
result.ScreenSpacePosition = distanceSnapGrid.ToScreenSpace(pos);
|
||||
result.Time = time;
|
||||
}
|
||||
}
|
||||
[CanBeNull]
|
||||
public SnapResult TrySnapToDistanceGrid(Vector2 screenSpacePosition, double? fixedTime = null)
|
||||
{
|
||||
if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True || distanceSnapGrid == null)
|
||||
return null;
|
||||
|
||||
if (snapType.HasFlag(SnapType.GlobalGrids))
|
||||
{
|
||||
if (rectangularGridSnapToggle.Value == TernaryState.True)
|
||||
{
|
||||
Vector2 pos = positionSnapGrid.GetSnappedPosition(positionSnapGrid.ToLocalSpace(result.ScreenSpacePosition));
|
||||
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
|
||||
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition), fixedTime);
|
||||
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, playfield);
|
||||
}
|
||||
|
||||
// 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);
|
||||
[CanBeNull]
|
||||
public SnapResult TrySnapToPositionGrid(Vector2 screenSpacePosition, double? fallbackTime = null)
|
||||
{
|
||||
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)
|
||||
|
@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override LocalisableString Description => "Flip objects on the chosen axes.";
|
||||
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 void ApplyToHitObject(HitObject hitObject)
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
@ -16,6 +17,9 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
|
||||
|
||||
public new Hit HitObject => (Hit)base.HitObject;
|
||||
|
||||
[Resolved]
|
||||
private TaikoHitObjectComposer? composer { get; set; }
|
||||
|
||||
public HitPlacementBlueprint()
|
||||
: base(new Hit())
|
||||
{
|
||||
@ -40,10 +44,12 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
|
||||
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);
|
||||
base.UpdateTimeAndPosition(result);
|
||||
base.UpdateTimeAndPosition(result.ScreenSpacePosition, result.Time ?? fallbackTime);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
@ -26,12 +25,15 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
|
||||
|
||||
private readonly IHasDuration spanPlacementObject;
|
||||
|
||||
[Resolved]
|
||||
private TaikoHitObjectComposer? composer { get; set; }
|
||||
|
||||
protected override bool IsValidForPlacement => Precision.DefinitelyBigger(spanPlacementObject.Duration, 0);
|
||||
|
||||
public TaikoSpanPlacementBlueprint(HitObject hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
spanPlacementObject = hitObject as IHasDuration;
|
||||
spanPlacementObject = (hitObject as IHasDuration)!;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
@ -79,9 +81,11 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
|
||||
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)
|
||||
{
|
||||
@ -116,6 +120,8 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
|
||||
originalPosition = ToLocalSpace(result.ScreenSpacePosition);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,22 @@
|
||||
// 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 osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Edit.Blueprints;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Edit
|
||||
{
|
||||
public partial class TaikoBlueprintContainer : ComposeBlueprintContainer
|
||||
{
|
||||
public TaikoBlueprintContainer(HitObjectComposer composer)
|
||||
public new TaikoHitObjectComposer Composer => (TaikoHitObjectComposer)base.Composer;
|
||||
|
||||
public TaikoBlueprintContainer(TaikoHitObjectComposer composer)
|
||||
: base(composer)
|
||||
{
|
||||
}
|
||||
@ -19,5 +25,22 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
||||
|
||||
public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
@ -12,6 +13,7 @@ using osu.Game.Screens.Edit.Compose.Components;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Edit
|
||||
{
|
||||
[Cached]
|
||||
public partial class TaikoHitObjectComposer : ScrollingHitObjectComposer<TaikoHitObject>
|
||||
{
|
||||
protected override bool ApplyHorizontalCentering => false;
|
||||
|
@ -36,6 +36,10 @@ namespace osu.Game.Tests.NonVisual.Ranking
|
||||
.Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null))
|
||||
.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;
|
||||
|
||||
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))
|
||||
.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;
|
||||
|
||||
for (int i = 0; i < events.Count; i++)
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
@ -25,13 +26,16 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[Test]
|
||||
public void TestLocallyModifyingOnlineBeatmap()
|
||||
{
|
||||
string initialHash = string.Empty;
|
||||
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));
|
||||
SaveEditor();
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -120,6 +120,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
public double FramesPerSecond => throw new NotImplementedException();
|
||||
public FrameTimeInfo TimeInfo => throw new NotImplementedException();
|
||||
public double StartTime => throw new NotImplementedException();
|
||||
public double GameplayStartTime => throw new NotImplementedException();
|
||||
|
||||
public IAdjustableAudioComponent AdjustmentsFromMods => adjustableAudioComponent;
|
||||
|
||||
|
239
osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs
Normal file
239
osu.Game.Tests/Visual/SongSelect/BeatmapCarouselV2TestScene.cs
Normal file
@ -0,0 +1,239 @@
|
||||
// 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 osuTK.Input;
|
||||
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 virtual 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 {criteria.Sort} group {criteria.Group}", () => 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));
|
||||
|
||||
protected void SelectNextPanel() => AddStep("select next panel", () => InputManager.Key(Key.Down));
|
||||
protected void SelectPrevPanel() => AddStep("select prev panel", () => InputManager.Key(Key.Up));
|
||||
protected void SelectNextGroup() => AddStep("select next group", () => InputManager.Key(Key.Right));
|
||||
protected void SelectPrevGroup() => AddStep("select prev group", () => InputManager.Key(Key.Left));
|
||||
|
||||
protected void Select() => AddStep("select", () => InputManager.Key(Key.Enter));
|
||||
|
||||
protected void CheckNoSelection() => AddAssert("has no selection", () => Carousel.CurrentSelection, () => Is.Null);
|
||||
protected void CheckHasSelection() => AddAssert("has selection", () => Carousel.CurrentSelection, () => Is.Not.Null);
|
||||
|
||||
protected void WaitForGroupSelection(int group, int panel)
|
||||
{
|
||||
AddUntilStep($"selected is group{group} panel{panel}", () =>
|
||||
{
|
||||
var groupingFilter = Carousel.Filters.OfType<BeatmapCarouselFilterGrouping>().Single();
|
||||
|
||||
GroupDefinition g = groupingFilter.GroupItems.Keys.ElementAt(group);
|
||||
// offset by one because the group itself is included in the items list.
|
||||
CarouselItem item = groupingFilter.GroupItems[g].ElementAt(panel + 1);
|
||||
|
||||
return ReferenceEquals(Carousel.CurrentSelection, item.Model);
|
||||
});
|
||||
}
|
||||
|
||||
protected 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);
|
||||
});
|
||||
}
|
||||
|
||||
protected void ClickVisiblePanel<T>(int index)
|
||||
where T : Drawable
|
||||
{
|
||||
AddStep($"click panel at index {index}", () =>
|
||||
{
|
||||
Carousel.ChildrenOfType<UserTrackingScrollContainer>().Single()
|
||||
.ChildrenOfType<T>()
|
||||
.Where(p => ((ICarouselPanel)p).Item?.IsVisible == true)
|
||||
.OrderBy(p => p.Y)
|
||||
.ElementAt(index)
|
||||
.TriggerClick();
|
||||
});
|
||||
}
|
||||
|
||||
/// <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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,286 +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;
|
||||
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
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(10),
|
||||
TextAnchor = 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.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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.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 TestOffScreenLoading()
|
||||
{
|
||||
AddStep("disable masking", () => Scroll.Masking = false);
|
||||
AddStep("enable masking", () => Scroll.Masking = true);
|
||||
}
|
||||
|
||||
[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 { Group = GroupMode.Difficulty, 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));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,158 @@
|
||||
// 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.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneBeatmapCarouselV2GroupSelection : BeatmapCarouselV2TestScene
|
||||
{
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
RemoveAllBeatmaps();
|
||||
|
||||
CreateCarousel();
|
||||
|
||||
SortBy(new FilterCriteria { Group = GroupMode.Difficulty, Sort = SortMode.Difficulty });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOpenCloseGroupWithNoSelectionMouse()
|
||||
{
|
||||
AddBeatmaps(10, 5);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
CheckNoSelection();
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
CheckNoSelection();
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
CheckNoSelection();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOpenCloseGroupWithNoSelectionKeyboard()
|
||||
{
|
||||
AddBeatmaps(10, 5);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
AddAssert("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
CheckNoSelection();
|
||||
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
AddAssert("keyboard selected is expanded", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.True);
|
||||
CheckNoSelection();
|
||||
|
||||
Select();
|
||||
AddUntilStep("no beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.Zero);
|
||||
AddAssert("keyboard selected is collapsed", () => getKeyboardSelectedPanel()?.Expanded.Value, () => Is.False);
|
||||
CheckNoSelection();
|
||||
|
||||
GroupPanel? getKeyboardSelectedPanel() => Carousel.ChildrenOfType<GroupPanel>().SingleOrDefault(p => p.KeyboardSelected.Value);
|
||||
}
|
||||
|
||||
[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!));
|
||||
|
||||
AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
|
||||
AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
AddUntilStep("carousel item not visible", getSelectedPanel, () => Is.Null);
|
||||
|
||||
ClickVisiblePanel<GroupPanel>(0);
|
||||
AddUntilStep("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
|
||||
BeatmapPanel? getSelectedPanel() => Carousel.ChildrenOfType<BeatmapPanel>().SingleOrDefault(p => p.Selected.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGroupSelectionOnHeader()
|
||||
{
|
||||
AddBeatmaps(10, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
SelectNextGroup();
|
||||
WaitForGroupSelection(0, 0);
|
||||
|
||||
SelectPrevPanel();
|
||||
SelectPrevGroup();
|
||||
WaitForGroupSelection(2, 9);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestKeyboardSelection()
|
||||
{
|
||||
AddBeatmaps(10, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
SelectNextPanel();
|
||||
SelectNextPanel();
|
||||
SelectNextPanel();
|
||||
SelectNextPanel();
|
||||
CheckNoSelection();
|
||||
|
||||
// open first group
|
||||
Select();
|
||||
CheckNoSelection();
|
||||
AddUntilStep("some beatmaps visible", () => Carousel.ChildrenOfType<BeatmapPanel>().Count(p => p.Alpha > 0), () => Is.GreaterThan(0));
|
||||
|
||||
SelectNextPanel();
|
||||
Select();
|
||||
WaitForGroupSelection(0, 0);
|
||||
|
||||
SelectNextGroup();
|
||||
WaitForGroupSelection(0, 1);
|
||||
|
||||
SelectNextGroup();
|
||||
WaitForGroupSelection(0, 2);
|
||||
|
||||
SelectPrevGroup();
|
||||
WaitForGroupSelection(0, 1);
|
||||
|
||||
SelectPrevGroup();
|
||||
WaitForGroupSelection(0, 0);
|
||||
|
||||
SelectPrevGroup();
|
||||
WaitForGroupSelection(2, 9);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,211 @@
|
||||
// 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!));
|
||||
|
||||
AddAssert("selection matches original carousel selection", () => selection, () => Is.EqualTo(Carousel.CurrentSelection));
|
||||
AddUntilStep("drawable selection restored", () => getSelectedPanel()?.Item?.Model, () => Is.EqualTo(selection));
|
||||
AddAssert("carousel item is visible", () => getSelectedPanel()?.Item?.IsVisible, () => Is.True);
|
||||
|
||||
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 TestGroupSelectionOnHeader()
|
||||
{
|
||||
AddBeatmaps(10, 3);
|
||||
WaitForDrawablePanels();
|
||||
|
||||
SelectNextGroup();
|
||||
SelectNextGroup();
|
||||
WaitForSelection(1, 0);
|
||||
|
||||
SelectPrevPanel();
|
||||
SelectPrevGroup();
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
@ -27,87 +28,102 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new FillFlowContainer
|
||||
Child = new OsuScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 400,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(5),
|
||||
Padding = new MarginPadding(10),
|
||||
Children = new Drawable[]
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
new FormTextBox
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 400,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(5),
|
||||
Padding = new MarginPadding(10),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Caption = "Artist",
|
||||
HintText = "Poot artist here!",
|
||||
PlaceholderText = "Here is an artist",
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new FormTextBox
|
||||
{
|
||||
Caption = "Artist",
|
||||
HintText = "Poot artist here!",
|
||||
PlaceholderText = "Here is an artist",
|
||||
Current = { Disabled = true },
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new FormNumberBox
|
||||
{
|
||||
Caption = "Number",
|
||||
HintText = "Insert your favourite number",
|
||||
PlaceholderText = "Mine is 42!",
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new FormCheckBox
|
||||
{
|
||||
Caption = EditorSetupStrings.LetterboxDuringBreaks,
|
||||
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
|
||||
},
|
||||
new FormCheckBox
|
||||
{
|
||||
Caption = EditorSetupStrings.LetterboxDuringBreaks,
|
||||
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
|
||||
Current = { Disabled = true },
|
||||
},
|
||||
new FormSliderBar<float>
|
||||
{
|
||||
Caption = "Slider",
|
||||
Current = new BindableFloat
|
||||
new FormTextBox
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Value = 5,
|
||||
Precision = 0.1f,
|
||||
Caption = "Artist",
|
||||
HintText = "Poot artist here!",
|
||||
PlaceholderText = "Here is an artist",
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new FormEnumDropdown<CountdownType>
|
||||
{
|
||||
Caption = EditorSetupStrings.EnableCountdown,
|
||||
HintText = EditorSetupStrings.CountdownDescription,
|
||||
},
|
||||
new FormFileSelector
|
||||
{
|
||||
Caption = "File selector",
|
||||
PlaceholderText = "Select a file",
|
||||
},
|
||||
new FormBeatmapFileSelector(true)
|
||||
{
|
||||
Caption = "File selector with intermediate choice dialog",
|
||||
PlaceholderText = "Select a file",
|
||||
},
|
||||
new FormColourPalette
|
||||
{
|
||||
Caption = "Combo colours",
|
||||
Colours =
|
||||
new FormTextBox
|
||||
{
|
||||
Colour4.Red,
|
||||
Colour4.Green,
|
||||
Colour4.Blue,
|
||||
Colour4.Yellow,
|
||||
}
|
||||
Caption = "Artist",
|
||||
HintText = "Poot artist here!",
|
||||
PlaceholderText = "Here is an artist",
|
||||
Current = { Disabled = true },
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new FormNumberBox(allowDecimals: true)
|
||||
{
|
||||
Caption = "Number",
|
||||
HintText = "Insert your favourite number",
|
||||
PlaceholderText = "Mine is 42!",
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new FormCheckBox
|
||||
{
|
||||
Caption = EditorSetupStrings.LetterboxDuringBreaks,
|
||||
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
|
||||
},
|
||||
new FormCheckBox
|
||||
{
|
||||
Caption = EditorSetupStrings.LetterboxDuringBreaks,
|
||||
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
|
||||
Current = { Disabled = true },
|
||||
},
|
||||
new FormSliderBar<float>
|
||||
{
|
||||
Caption = "Slider",
|
||||
Current = new BindableFloat
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Value = 5,
|
||||
Precision = 0.1f,
|
||||
},
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
new FormEnumDropdown<CountdownType>
|
||||
{
|
||||
Caption = EditorSetupStrings.EnableCountdown,
|
||||
HintText = EditorSetupStrings.CountdownDescription,
|
||||
},
|
||||
new FormFileSelector
|
||||
{
|
||||
Caption = "File selector",
|
||||
PlaceholderText = "Select a file",
|
||||
},
|
||||
new FormBeatmapFileSelector(true)
|
||||
{
|
||||
Caption = "File selector with intermediate choice dialog",
|
||||
PlaceholderText = "Select a file",
|
||||
},
|
||||
new FormColourPalette
|
||||
{
|
||||
Caption = "Combo colours",
|
||||
Colours =
|
||||
{
|
||||
Colour4.Red,
|
||||
Colour4.Green,
|
||||
Colour4.Blue,
|
||||
Colour4.Yellow,
|
||||
}
|
||||
},
|
||||
new FormButton
|
||||
{
|
||||
Caption = "No text in button",
|
||||
Action = () => { },
|
||||
},
|
||||
new FormButton
|
||||
{
|
||||
Caption = "Text in button which is pretty long and is very likely to wrap",
|
||||
ButtonText = "Foo the bar",
|
||||
Action = () => { },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -475,11 +475,8 @@ namespace osu.Game.Beatmaps
|
||||
beatmapContent.BeatmapInfo = beatmapInfo;
|
||||
|
||||
// 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.Status = BeatmapOnlineStatus.LocallyModified;
|
||||
beatmapInfo.ResetOnlineInfo();
|
||||
|
||||
Realm.Write(r =>
|
||||
{
|
||||
|
@ -292,7 +292,7 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
"1407228 II-L - VANGUARD-1.osz",
|
||||
"1422686 II-L - VANGUARD-2.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",
|
||||
"1449942 m108 - Crescent Sakura.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",
|
||||
"1859322 Hino Isuka - Delightness Brightness.osz",
|
||||
"1884102 Maduk - Go (feat. Lachi) (Cut Ver.).osz",
|
||||
"1884578 Neko Hacker - People People feat. Nanahira.osz",
|
||||
"1897902 uma vs. Morimori Atsushi - Re: End of a Dream.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", // 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",
|
||||
"1934686 ARForest - Rainbow Magic!!.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",
|
||||
"1972518 Toromaru - Sleight of Hand.osz",
|
||||
"1982302 KINEMA106 - INVITE.osz",
|
||||
"1983475 KNOWER - The Government Knows.osz",
|
||||
"2010165 Junk - Yellow Smile (bms edit).osz",
|
||||
"2022737 Andora - Euphoria (feat. WaMi).osz",
|
||||
"2025023 tephe - Genjitsu Escape.osz",
|
||||
|
@ -447,60 +447,31 @@ namespace osu.Game.Beatmaps.Formats
|
||||
|
||||
private void addPathData(TextWriter writer, IHasPath pathData, Vector2 position)
|
||||
{
|
||||
PathType? lastType = null;
|
||||
|
||||
for (int i = 0; i < pathData.Path.ControlPoints.Count; i++)
|
||||
{
|
||||
PathControlPoint point = pathData.Path.ControlPoints[i];
|
||||
|
||||
// Note that lazer's encoding format supports specifying multiple curve types for a slider path, which is not supported by stable.
|
||||
// Backwards compatibility with stable is handled by `LegacyBeatmapExporter` and `BezierConverter.ConvertToModernBezier()`.
|
||||
if (point.Type != null)
|
||||
{
|
||||
// We've reached a new (explicit) segment!
|
||||
|
||||
// Explicit segments have a new format in which the type is injected into the middle of the control point string.
|
||||
// To preserve compatibility with osu-stable as much as possible, explicit segments with the same type are converted to use implicit segments by duplicating the control point.
|
||||
// One exception are consecutive perfect curves, which aren't supported in osu!stable and can lead to decoding issues if encoded as implicit segments
|
||||
bool needsExplicitSegment = point.Type != lastType || point.Type == PathType.PERFECT_CURVE || i == pathData.Path.ControlPoints.Count - 1;
|
||||
|
||||
// Another exception to this is when the last two control points of the last segment were duplicated. This is not a scenario supported by osu!stable.
|
||||
// Lazer does not add implicit segments for the last two control points of _any_ explicit segment, so an explicit segment is forced in order to maintain consistency with the decoder.
|
||||
if (i > 1)
|
||||
switch (point.Type?.Type)
|
||||
{
|
||||
// We need to use the absolute control point position to determine equality, otherwise floating point issues may arise.
|
||||
Vector2 p1 = position + pathData.Path.ControlPoints[i - 1].Position;
|
||||
Vector2 p2 = position + pathData.Path.ControlPoints[i - 2].Position;
|
||||
case SplineType.BSpline:
|
||||
writer.Write(point.Type.Value.Degree > 0 ? $"B{point.Type.Value.Degree}|" : "B|");
|
||||
break;
|
||||
|
||||
if ((int)p1.X == (int)p2.X && (int)p1.Y == (int)p2.Y)
|
||||
needsExplicitSegment = true;
|
||||
}
|
||||
case SplineType.Catmull:
|
||||
writer.Write("C|");
|
||||
break;
|
||||
|
||||
if (needsExplicitSegment)
|
||||
{
|
||||
switch (point.Type?.Type)
|
||||
{
|
||||
case SplineType.BSpline:
|
||||
writer.Write(point.Type.Value.Degree > 0 ? $"B{point.Type.Value.Degree}|" : "B|");
|
||||
break;
|
||||
case SplineType.PerfectCurve:
|
||||
writer.Write("P|");
|
||||
break;
|
||||
|
||||
case SplineType.Catmull:
|
||||
writer.Write("C|");
|
||||
break;
|
||||
|
||||
case SplineType.PerfectCurve:
|
||||
writer.Write("P|");
|
||||
break;
|
||||
|
||||
case SplineType.Linear:
|
||||
writer.Write("L|");
|
||||
break;
|
||||
}
|
||||
|
||||
lastType = point.Type;
|
||||
}
|
||||
else
|
||||
{
|
||||
// New segment with the same type - duplicate the control point
|
||||
writer.Write(FormattableString.Invariant($"{position.X + point.Position.X}:{position.Y + point.Position.Y}|"));
|
||||
case SplineType.Linear:
|
||||
writer.Write("L|");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -203,6 +203,8 @@ namespace osu.Game.Beatmaps
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO: This is a touch expensive and can become an issue if being accessed every Update call.
|
||||
// Optimally we would not involve the async flow if things are already loaded.
|
||||
return loadBeatmapAsync().GetResultSafely();
|
||||
}
|
||||
catch (AggregateException ae)
|
||||
|
@ -120,18 +120,30 @@ namespace osu.Game.Database
|
||||
if (BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1
|
||||
&& hasPath.Path.ControlPoints[0].Type!.Value.Degree == null) continue;
|
||||
|
||||
var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints);
|
||||
|
||||
// Truncate control points to integer positions
|
||||
foreach (var pathControlPoint in newControlPoints)
|
||||
{
|
||||
pathControlPoint.Position = new Vector2(
|
||||
(float)Math.Floor(pathControlPoint.Position.X),
|
||||
(float)Math.Floor(pathControlPoint.Position.Y));
|
||||
}
|
||||
var convertedToBezier = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints);
|
||||
|
||||
hasPath.Path.ControlPoints.Clear();
|
||||
hasPath.Path.ControlPoints.AddRange(newControlPoints);
|
||||
|
||||
for (int i = 0; i < convertedToBezier.Count; i++)
|
||||
{
|
||||
var convertedPoint = convertedToBezier[i];
|
||||
|
||||
// Truncate control points to integer positions
|
||||
var position = new Vector2(
|
||||
(float)Math.Floor(convertedPoint.Position.X),
|
||||
(float)Math.Floor(convertedPoint.Position.Y));
|
||||
|
||||
// stable only supports a single curve type specification per slider.
|
||||
// we exploit the fact that the converted-to-Bézier path only has Bézier segments,
|
||||
// and thus we specify the Bézier curve type once ever at the start of the slider.
|
||||
hasPath.Path.ControlPoints.Add(new PathControlPoint(position, i == 0 ? PathType.BEZIER : null));
|
||||
|
||||
// however, the Bézier path as output by the converter has multiple segments.
|
||||
// `LegacyBeatmapEncoder` will attempt to encode this by emitting per-control-point curve type specs which don't do anything for stable.
|
||||
// instead, stable expects control points that start a segment to be present in the path twice in succession.
|
||||
if (convertedPoint.Type == PathType.BEZIER && i > 0)
|
||||
hasPath.Path.ControlPoints.Add(new PathControlPoint(position));
|
||||
}
|
||||
}
|
||||
|
||||
// Encode to legacy format
|
||||
|
189
osu.Game/Graphics/UserInterfaceV2/FormButton.cs
Normal file
189
osu.Game/Graphics/UserInterfaceV2/FormButton.cs
Normal 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;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Backgrounds;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
public partial class FormButton : CompositeDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// Caption describing this button, displayed on the left of it.
|
||||
/// </summary>
|
||||
public LocalisableString Caption { get; init; }
|
||||
|
||||
public LocalisableString ButtonText { get; init; }
|
||||
|
||||
public Action? Action { get; init; }
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = 50;
|
||||
|
||||
Masking = true;
|
||||
CornerRadius = 5;
|
||||
CornerExponent = 2.5f;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background5,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Left = 9,
|
||||
Right = 5,
|
||||
Vertical = 5,
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuTextFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Width = 0.45f,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Text = Caption,
|
||||
},
|
||||
new Button
|
||||
{
|
||||
Action = Action,
|
||||
Text = ButtonText,
|
||||
RelativeSizeAxes = ButtonText == default ? Axes.None : Axes.X,
|
||||
Width = ButtonText == default ? 90 : 0.45f,
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateState();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
base.OnHoverLost(e);
|
||||
updateState();
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
BorderThickness = IsHovered ? 2 : 0;
|
||||
|
||||
if (IsHovered)
|
||||
BorderColour = colourProvider.Light4;
|
||||
}
|
||||
|
||||
public partial class Button : OsuButton
|
||||
{
|
||||
private TrianglesV2? triangles { get; set; }
|
||||
|
||||
protected override float HoverLayerFinalAlpha => 0;
|
||||
|
||||
private Color4? triangleGradientSecondColour;
|
||||
|
||||
public override Color4 BackgroundColour
|
||||
{
|
||||
get => base.BackgroundColour;
|
||||
set
|
||||
{
|
||||
base.BackgroundColour = value;
|
||||
triangleGradientSecondColour = BackgroundColour.Lighten(0.2f);
|
||||
updateColours();
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider overlayColourProvider)
|
||||
{
|
||||
DefaultBackgroundColour = overlayColourProvider.Colour3;
|
||||
triangleGradientSecondColour ??= overlayColourProvider.Colour1;
|
||||
|
||||
if (Text == default)
|
||||
{
|
||||
Add(new SpriteIcon
|
||||
{
|
||||
Icon = FontAwesome.Solid.ChevronRight,
|
||||
Size = new Vector2(16),
|
||||
Shadow = true,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Content.CornerRadius = 2;
|
||||
|
||||
Add(triangles = new TrianglesV2
|
||||
{
|
||||
Thickness = 0.02f,
|
||||
SpawnRatio = 0.6f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = float.MaxValue,
|
||||
});
|
||||
|
||||
updateColours();
|
||||
}
|
||||
|
||||
private void updateColours()
|
||||
{
|
||||
if (triangles == null)
|
||||
return;
|
||||
|
||||
Debug.Assert(triangleGradientSecondColour != null);
|
||||
|
||||
triangles.Colour = ColourInfo.GradientVertical(triangleGradientSecondColour.Value, BackgroundColour);
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
Debug.Assert(triangleGradientSecondColour != null);
|
||||
|
||||
Background.FadeColour(triangleGradientSecondColour.Value, 300, Easing.OutQuint);
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
Background.FadeColour(BackgroundColour, 300, Easing.OutQuint);
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,32 +1,30 @@
|
||||
// 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.Globalization;
|
||||
using osu.Framework.Input;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
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,
|
||||
};
|
||||
|
||||
internal partial class InnerNumberBox : InnerTextBox
|
||||
{
|
||||
public bool AllowDecimals { get; init; }
|
||||
|
||||
public InnerNumberBox()
|
||||
public InnerNumberBox(bool allowDecimals)
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -119,7 +119,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
Caption = Caption,
|
||||
TooltipText = HintText,
|
||||
},
|
||||
textBox = new FormNumberBox.InnerNumberBox
|
||||
textBox = new FormNumberBox.InnerNumberBox(allowDecimals: true)
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
@ -127,7 +127,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
Width = 0.5f,
|
||||
CommitOnFocusLost = true,
|
||||
SelectAllOnFocus = true,
|
||||
AllowDecimals = true,
|
||||
OnInputError = () =>
|
||||
{
|
||||
flashLayer.Colour = ColourInfo.GradientVertical(colours.Red3.Opacity(0), colours.Red3);
|
||||
|
@ -9,6 +9,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.Toolkit.HighPerformance;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.IO.Stores;
|
||||
using SharpCompress.Archives.Zip;
|
||||
using SharpCompress.Common;
|
||||
@ -54,12 +55,22 @@ namespace osu.Game.IO.Archives
|
||||
if (entry == null)
|
||||
return null;
|
||||
|
||||
var owner = MemoryAllocator.Default.Allocate<byte>((int)entry.Size);
|
||||
|
||||
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()
|
||||
|
@ -31,15 +31,6 @@ namespace osu.Game.Online
|
||||
[Resolved]
|
||||
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]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
|
||||
@ -165,26 +156,7 @@ namespace osu.Game.Online
|
||||
return;
|
||||
}
|
||||
|
||||
APIUser? singleUser = onlineAlertQueue.Count == 1 ? onlineAlertQueue.Single() : null;
|
||||
|
||||
notifications.Post(new SimpleNotification
|
||||
{
|
||||
Transient = true,
|
||||
IsImportant = false,
|
||||
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;
|
||||
}
|
||||
});
|
||||
notifications.Post(new FriendOnlineNotification(onlineAlertQueue.ToArray()));
|
||||
|
||||
onlineAlertQueue.Clear();
|
||||
lastOnlineAlertTime = null;
|
||||
@ -204,17 +176,60 @@ namespace osu.Game.Online
|
||||
return;
|
||||
}
|
||||
|
||||
notifications.Post(new SimpleNotification
|
||||
{
|
||||
Transient = true,
|
||||
IsImportant = false,
|
||||
Icon = FontAwesome.Solid.UserMinus,
|
||||
Text = $"Offline: {string.Join(@", ", offlineAlertQueue.Select(u => u.Username))}",
|
||||
IconColour = colours.Red
|
||||
});
|
||||
notifications.Post(new FriendOfflineNotification(offlineAlertQueue.ToArray()));
|
||||
|
||||
offlineAlertQueue.Clear();
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -178,9 +178,9 @@ namespace osu.Game
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
@ -233,8 +233,6 @@ namespace osu.Game
|
||||
|
||||
forwardGeneralLogsToNotifications();
|
||||
forwardTabletLogsToNotifications();
|
||||
|
||||
SentryLogger = new SentryLogger(this);
|
||||
}
|
||||
|
||||
#region IOverlayManager
|
||||
@ -308,7 +306,7 @@ namespace osu.Game
|
||||
protected override UserInputManager CreateUserInputManager()
|
||||
{
|
||||
var userInputManager = base.CreateUserInputManager();
|
||||
(userInputManager as OsuUserInputManager)?.PlayingState.BindTo(playingState);
|
||||
(userInputManager as OsuUserInputManager)?.PlayingState.BindTo(UserPlayingState);
|
||||
return userInputManager;
|
||||
}
|
||||
|
||||
@ -320,6 +318,12 @@ namespace osu.Game
|
||||
private readonly List<string> dragDropFiles = new List<string>();
|
||||
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)
|
||||
{
|
||||
base.SetHost(host);
|
||||
@ -403,7 +407,7 @@ namespace osu.Game
|
||||
// Transfer any runtime changes back to configuration file.
|
||||
SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID.ToString();
|
||||
|
||||
playingState.BindValueChanged(p =>
|
||||
UserPlayingState.BindValueChanged(p =>
|
||||
{
|
||||
BeatmapManager.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;
|
||||
}
|
||||
|
||||
private void screenChanged(IScreen current, IScreen newScreen)
|
||||
protected virtual void ScreenChanged([CanBeNull] IOsuScreen current, [CanBeNull] IOsuScreen newScreen)
|
||||
{
|
||||
SentrySdk.ConfigureScope(scope =>
|
||||
{
|
||||
@ -1560,10 +1564,10 @@ namespace osu.Game
|
||||
switch (current)
|
||||
{
|
||||
case Player player:
|
||||
player.PlayingState.UnbindFrom(playingState);
|
||||
player.PlayingState.UnbindFrom(UserPlayingState);
|
||||
|
||||
// reset for sanity.
|
||||
playingState.Value = LocalUserPlayingState.NotPlaying;
|
||||
UserPlayingState.Value = LocalUserPlayingState.NotPlaying;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -1580,7 +1584,7 @@ namespace osu.Game
|
||||
break;
|
||||
|
||||
case Player player:
|
||||
player.PlayingState.BindTo(playingState);
|
||||
player.PlayingState.BindTo(UserPlayingState);
|
||||
break;
|
||||
|
||||
default:
|
||||
@ -1588,30 +1592,32 @@ namespace osu.Game
|
||||
break;
|
||||
}
|
||||
|
||||
if (current is IOsuScreen currentOsuScreen)
|
||||
if (current != null)
|
||||
{
|
||||
backButtonVisibility.UnbindFrom(currentOsuScreen.BackButtonVisibility);
|
||||
OverlayActivationMode.UnbindFrom(currentOsuScreen.OverlayActivationMode);
|
||||
configUserActivity.UnbindFrom(currentOsuScreen.Activity);
|
||||
backButtonVisibility.UnbindFrom(current.BackButtonVisibility);
|
||||
OverlayActivationMode.UnbindFrom(current.OverlayActivationMode);
|
||||
configUserActivity.UnbindFrom(current.Activity);
|
||||
}
|
||||
|
||||
if (newScreen is IOsuScreen newOsuScreen)
|
||||
// Bind to new screen.
|
||||
if (newScreen != null)
|
||||
{
|
||||
backButtonVisibility.BindTo(newOsuScreen.BackButtonVisibility);
|
||||
OverlayActivationMode.BindTo(newOsuScreen.OverlayActivationMode);
|
||||
configUserActivity.BindTo(newOsuScreen.Activity);
|
||||
backButtonVisibility.BindTo(newScreen.BackButtonVisibility);
|
||||
OverlayActivationMode.BindTo(newScreen.OverlayActivationMode);
|
||||
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();
|
||||
else
|
||||
Toolbar.Show();
|
||||
|
||||
if (newOsuScreen.ShowFooter)
|
||||
if (newScreen.ShowFooter)
|
||||
{
|
||||
BackButton.Hide();
|
||||
ScreenFooter.SetButtons(newOsuScreen.CreateFooterButtons());
|
||||
ScreenFooter.SetButtons(newScreen.CreateFooterButtons());
|
||||
ScreenFooter.Show();
|
||||
}
|
||||
else
|
||||
@ -1619,16 +1625,16 @@ namespace osu.Game
|
||||
ScreenFooter.SetButtons(Array.Empty<ScreenFooterButton>());
|
||||
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)
|
||||
{
|
||||
screenChanged(lastScreen, newScreen);
|
||||
ScreenChanged((OsuScreen)lastScreen, (OsuScreen)newScreen);
|
||||
|
||||
if (newScreen == null)
|
||||
Exit();
|
||||
|
@ -21,7 +21,7 @@ using Realms;
|
||||
namespace osu.Game.Overlays.FirstRunSetup
|
||||
{
|
||||
[LocalisableDescription(typeof(FirstRunSetupBeatmapScreenStrings), nameof(FirstRunSetupBeatmapScreenStrings.Header))]
|
||||
public partial class ScreenBeatmaps : FirstRunSetupScreen
|
||||
public partial class ScreenBeatmaps : WizardScreen
|
||||
{
|
||||
private ProgressRoundedButton downloadBundledButton = null!;
|
||||
private ProgressRoundedButton downloadTutorialButton = null!;
|
||||
|
@ -20,7 +20,7 @@ using osu.Game.Overlays.Settings.Sections;
|
||||
namespace osu.Game.Overlays.FirstRunSetup
|
||||
{
|
||||
[LocalisableDescription(typeof(FirstRunSetupOverlayStrings), nameof(FirstRunSetupOverlayStrings.Behaviour))]
|
||||
public partial class ScreenBehaviour : FirstRunSetupScreen
|
||||
public partial class ScreenBehaviour : WizardScreen
|
||||
{
|
||||
private SearchContainer<SettingsSection> searchContainer;
|
||||
|
||||
|
@ -31,7 +31,7 @@ using osuTK;
|
||||
namespace osu.Game.Overlays.FirstRunSetup
|
||||
{
|
||||
[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);
|
||||
|
||||
|
@ -32,7 +32,7 @@ using osuTK;
|
||||
namespace osu.Game.Overlays.FirstRunSetup
|
||||
{
|
||||
[LocalisableDescription(typeof(GraphicsSettingsStrings), nameof(GraphicsSettingsStrings.UIScaling))]
|
||||
public partial class ScreenUIScale : FirstRunSetupScreen
|
||||
public partial class ScreenUIScale : WizardScreen
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
|
@ -23,7 +23,7 @@ using osuTK;
|
||||
namespace osu.Game.Overlays.FirstRunSetup
|
||||
{
|
||||
[LocalisableDescription(typeof(FirstRunSetupOverlayStrings), nameof(FirstRunSetupOverlayStrings.WelcomeTitle))]
|
||||
public partial class ScreenWelcome : FirstRunSetupScreen
|
||||
public partial class ScreenWelcome : WizardScreen
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(FrameworkConfigManager frameworkConfig)
|
||||
|
@ -1,38 +1,22 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.FirstRunSetup;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Footer;
|
||||
using osu.Game.Screens.Menu;
|
||||
|
||||
namespace osu.Game.Overlays
|
||||
{
|
||||
[Cached]
|
||||
public partial class FirstRunSetupOverlay : ShearedOverlayContainer
|
||||
public partial class FirstRunSetupOverlay : WizardOverlay
|
||||
{
|
||||
[Resolved]
|
||||
private IPerformFromScreenRunner performer { get; set; } = null!;
|
||||
@ -43,28 +27,8 @@ namespace osu.Game.Overlays
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
|
||||
private ScreenStack? stack;
|
||||
|
||||
public ShearedButton? NextButton => DisplayedFooterContent?.NextButton;
|
||||
|
||||
private readonly Bindable<bool> showFirstRunSetup = new Bindable<bool>();
|
||||
|
||||
private int? currentStepIndex;
|
||||
|
||||
/// <summary>
|
||||
/// The currently displayed screen, if any.
|
||||
/// </summary>
|
||||
public FirstRunSetupScreen? CurrentScreen => (FirstRunSetupScreen?)stack?.CurrentScreen;
|
||||
|
||||
private readonly List<Type> steps = new List<Type>();
|
||||
|
||||
private Container screenContent = null!;
|
||||
|
||||
private Container content = null!;
|
||||
|
||||
private LoadingSpinner loading = null!;
|
||||
private ScheduledDelegate? loadingShowDelegate;
|
||||
|
||||
public FirstRunSetupOverlay()
|
||||
: base(OverlayColourScheme.Purple)
|
||||
{
|
||||
@ -73,67 +37,15 @@ namespace osu.Game.Overlays
|
||||
[BackgroundDependencyLoader(permitNulls: true)]
|
||||
private void load(OsuColour colours, LegacyImportManager? legacyImportManager)
|
||||
{
|
||||
steps.Add(typeof(ScreenWelcome));
|
||||
steps.Add(typeof(ScreenUIScale));
|
||||
steps.Add(typeof(ScreenBeatmaps));
|
||||
AddStep<ScreenWelcome>();
|
||||
AddStep<ScreenUIScale>();
|
||||
AddStep<ScreenBeatmaps>();
|
||||
if (legacyImportManager?.SupportsImportFromStable == true)
|
||||
steps.Add(typeof(ScreenImportFromStable));
|
||||
steps.Add(typeof(ScreenBehaviour));
|
||||
AddStep<ScreenImportFromStable>();
|
||||
AddStep<ScreenBehaviour>();
|
||||
|
||||
Header.Title = FirstRunSetupOverlayStrings.FirstRunSetupTitle;
|
||||
Header.Description = FirstRunSetupOverlayStrings.FirstRunSetupDescription;
|
||||
|
||||
MainAreaContent.AddRange(new Drawable[]
|
||||
{
|
||||
content = new PopoverContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Bottom = 20 },
|
||||
Child = new GridContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(minSize: 640, maxSize: 800),
|
||||
new Dimension(),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
Empty(),
|
||||
new InputBlockingContainer
|
||||
{
|
||||
Masking = true,
|
||||
CornerRadius = 14,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourProvider.Background6,
|
||||
},
|
||||
loading = new LoadingSpinner(),
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Vertical = 20 },
|
||||
Child = screenContent = new Container { RelativeSizeAxes = Axes.Both, },
|
||||
},
|
||||
},
|
||||
},
|
||||
Empty(),
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -145,55 +57,6 @@ namespace osu.Game.Overlays
|
||||
if (showFirstRunSetup.Value) Show();
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private ScreenFooter footer { get; set; } = null!;
|
||||
|
||||
public new FirstRunSetupFooterContent? DisplayedFooterContent => base.DisplayedFooterContent as FirstRunSetupFooterContent;
|
||||
|
||||
public override VisibilityContainer CreateFooterContent()
|
||||
{
|
||||
var footerContent = new FirstRunSetupFooterContent
|
||||
{
|
||||
ShowNextStep = showNextStep,
|
||||
};
|
||||
|
||||
footerContent.OnLoadComplete += _ => updateButtons();
|
||||
return footerContent;
|
||||
}
|
||||
|
||||
public override bool OnBackButton()
|
||||
{
|
||||
if (currentStepIndex == 0)
|
||||
return false;
|
||||
|
||||
Debug.Assert(stack != null);
|
||||
|
||||
stack.CurrentScreen.Exit();
|
||||
currentStepIndex--;
|
||||
|
||||
updateButtons();
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
if (!e.Repeat)
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.Select:
|
||||
DisplayedFooterContent?.NextButton.TriggerClick();
|
||||
return true;
|
||||
|
||||
case GlobalAction.Back:
|
||||
footer.BackButton.TriggerClick();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return base.OnPressed(e);
|
||||
}
|
||||
|
||||
public override void Show()
|
||||
{
|
||||
// if we are valid for display, only do so after reaching the main menu.
|
||||
@ -207,24 +70,11 @@ namespace osu.Game.Overlays
|
||||
}, new[] { typeof(MainMenu) });
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
base.PopIn();
|
||||
|
||||
content.ScaleTo(0.99f)
|
||||
.ScaleTo(1, 400, Easing.OutQuint);
|
||||
|
||||
if (currentStepIndex == null)
|
||||
showFirstStep();
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
base.PopOut();
|
||||
|
||||
content.ScaleTo(0.99f, 400, Easing.OutQuint);
|
||||
|
||||
if (currentStepIndex != null)
|
||||
if (CurrentStepIndex != null)
|
||||
{
|
||||
notificationOverlay.Post(new SimpleNotification
|
||||
{
|
||||
@ -237,112 +87,14 @@ namespace osu.Game.Overlays
|
||||
},
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
stack?.FadeOut(100)
|
||||
.Expire();
|
||||
}
|
||||
}
|
||||
|
||||
private void showFirstStep()
|
||||
protected override void ShowNextStep()
|
||||
{
|
||||
Debug.Assert(currentStepIndex == null);
|
||||
base.ShowNextStep();
|
||||
|
||||
screenContent.Child = stack = new ScreenStack
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
|
||||
currentStepIndex = -1;
|
||||
showNextStep();
|
||||
}
|
||||
|
||||
private void showNextStep()
|
||||
{
|
||||
Debug.Assert(currentStepIndex != null);
|
||||
Debug.Assert(stack != null);
|
||||
|
||||
currentStepIndex++;
|
||||
|
||||
if (currentStepIndex < steps.Count)
|
||||
{
|
||||
var nextScreen = (Screen)Activator.CreateInstance(steps[currentStepIndex.Value])!;
|
||||
|
||||
loadingShowDelegate = Scheduler.AddDelayed(() => loading.Show(), 200);
|
||||
nextScreen.OnLoadComplete += _ =>
|
||||
{
|
||||
loadingShowDelegate?.Cancel();
|
||||
loading.Hide();
|
||||
};
|
||||
|
||||
stack.Push(nextScreen);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (CurrentStepIndex == null)
|
||||
showFirstRunSetup.Value = false;
|
||||
currentStepIndex = null;
|
||||
Hide();
|
||||
}
|
||||
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
private void updateButtons() => DisplayedFooterContent?.UpdateButtons(currentStepIndex, steps);
|
||||
|
||||
public partial class FirstRunSetupFooterContent : VisibilityContainer
|
||||
{
|
||||
public ShearedButton NextButton { get; private set; } = null!;
|
||||
|
||||
public Action? ShowNextStep;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = NextButton = new ShearedButton(0)
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Margin = new MarginPadding { Right = 12f },
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 1,
|
||||
Text = FirstRunSetupOverlayStrings.GetStarted,
|
||||
DarkerColour = colourProvider.Colour2,
|
||||
LighterColour = colourProvider.Colour1,
|
||||
Action = () => ShowNextStep?.Invoke(),
|
||||
};
|
||||
}
|
||||
|
||||
public void UpdateButtons(int? currentStep, IReadOnlyList<Type> steps)
|
||||
{
|
||||
NextButton.Enabled.Value = currentStep != null;
|
||||
|
||||
if (currentStep == null)
|
||||
return;
|
||||
|
||||
bool isFirstStep = currentStep == 0;
|
||||
bool isLastStep = currentStep == steps.Count - 1;
|
||||
|
||||
if (isFirstStep)
|
||||
NextButton.Text = FirstRunSetupOverlayStrings.GetStarted;
|
||||
else
|
||||
{
|
||||
NextButton.Text = isLastStep
|
||||
? CommonStrings.Finish
|
||||
: LocalisableString.Interpolate($@"{CommonStrings.Next} ({steps[currentStep.Value + 1].GetLocalisableDescription()})");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.FadeIn();
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
this.Delay(400).FadeOut();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Graphics;
|
||||
@ -62,6 +63,7 @@ namespace osu.Game.Overlays.Login
|
||||
},
|
||||
codeTextBox = new OsuTextBox
|
||||
{
|
||||
InputProperties = new TextInputProperties(TextInputType.Code),
|
||||
PlaceholderText = "Enter code",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
TabbableContentContainer = this,
|
||||
|
@ -35,7 +35,8 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = GameplaySettingsStrings.LightenDuringBreaks,
|
||||
Current = config.GetBindable<bool>(OsuSetting.LightenDuringBreaks)
|
||||
Current = config.GetBindable<bool>(OsuSetting.LightenDuringBreaks),
|
||||
Keywords = new[] { "dim", "level" }
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
|
@ -111,6 +111,16 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
SelectedItems.AddRange(targetComponents.SelectMany(list => list).Except(SelectedItems).ToArray());
|
||||
}
|
||||
|
||||
protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<ISerialisableDrawable> blueprint, Vector2[] originalSnapPositions)> blueprints)
|
||||
{
|
||||
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
|
||||
|
||||
// The final movement position, relative to movementBlueprintOriginalPosition.
|
||||
var referenceBlueprint = blueprints.First().blueprint;
|
||||
Vector2 movePosition = blueprints.First().originalSnapPositions.First() + distanceTravelled;
|
||||
return SelectionHandler.HandleMovement(new MoveSelectionEvent<ISerialisableDrawable>(referenceBlueprint, movePosition - referenceBlueprint.ScreenSpaceSelectionPoint));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move the current selection spatially by the specified delta, in screen coordinates (ie. the same coordinates as the blueprints).
|
||||
/// </summary>
|
||||
|
288
osu.Game/Overlays/WizardOverlay.cs
Normal file
288
osu.Game/Overlays/WizardOverlay.cs
Normal file
@ -0,0 +1,288 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Screens.Footer;
|
||||
|
||||
namespace osu.Game.Overlays
|
||||
{
|
||||
public partial class WizardOverlay : ShearedOverlayContainer
|
||||
{
|
||||
private ScreenStack? stack;
|
||||
|
||||
public ShearedButton? NextButton => DisplayedFooterContent?.NextButton;
|
||||
|
||||
protected int? CurrentStepIndex { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The currently displayed screen, if any.
|
||||
/// </summary>
|
||||
public WizardScreen? CurrentScreen => (WizardScreen?)stack?.CurrentScreen;
|
||||
|
||||
private readonly List<Type> steps = new List<Type>();
|
||||
|
||||
private Container screenContent = null!;
|
||||
|
||||
private Container content = null!;
|
||||
|
||||
private LoadingSpinner loading = null!;
|
||||
private ScheduledDelegate? loadingShowDelegate;
|
||||
|
||||
protected WizardOverlay(OverlayColourScheme scheme)
|
||||
: base(scheme)
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
MainAreaContent.AddRange(new Drawable[]
|
||||
{
|
||||
content = new PopoverContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Bottom = 20 },
|
||||
Child = new GridContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(minSize: 640, maxSize: 800),
|
||||
new Dimension(),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
Empty(),
|
||||
new InputBlockingContainer
|
||||
{
|
||||
Masking = true,
|
||||
CornerRadius = 14,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = ColourProvider.Background6,
|
||||
},
|
||||
loading = new LoadingSpinner(),
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Vertical = 20 },
|
||||
Child = screenContent = new Container { RelativeSizeAxes = Axes.Both, },
|
||||
},
|
||||
},
|
||||
},
|
||||
Empty(),
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private ScreenFooter footer { get; set; } = null!;
|
||||
|
||||
public new WizardFooterContent? DisplayedFooterContent => base.DisplayedFooterContent as WizardFooterContent;
|
||||
|
||||
public override VisibilityContainer CreateFooterContent()
|
||||
{
|
||||
var footerContent = new WizardFooterContent
|
||||
{
|
||||
ShowNextStep = ShowNextStep,
|
||||
};
|
||||
|
||||
footerContent.OnLoadComplete += _ => updateButtons();
|
||||
return footerContent;
|
||||
}
|
||||
|
||||
public override bool OnBackButton()
|
||||
{
|
||||
if (CurrentStepIndex == 0)
|
||||
return false;
|
||||
|
||||
Debug.Assert(stack != null);
|
||||
|
||||
stack.CurrentScreen.Exit();
|
||||
CurrentStepIndex--;
|
||||
|
||||
updateButtons();
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
if (!e.Repeat)
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.Select:
|
||||
DisplayedFooterContent?.NextButton.TriggerClick();
|
||||
return true;
|
||||
|
||||
case GlobalAction.Back:
|
||||
footer.BackButton.TriggerClick();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return base.OnPressed(e);
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
base.PopIn();
|
||||
|
||||
content.ScaleTo(0.99f)
|
||||
.ScaleTo(1, 400, Easing.OutQuint);
|
||||
|
||||
if (CurrentStepIndex == null)
|
||||
showFirstStep();
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
base.PopOut();
|
||||
|
||||
content.ScaleTo(0.99f, 400, Easing.OutQuint);
|
||||
|
||||
if (CurrentStepIndex == null)
|
||||
{
|
||||
stack?.FadeOut(100)
|
||||
.Expire();
|
||||
}
|
||||
}
|
||||
|
||||
protected void AddStep<T>()
|
||||
where T : WizardScreen
|
||||
{
|
||||
steps.Add(typeof(T));
|
||||
}
|
||||
|
||||
private void showFirstStep()
|
||||
{
|
||||
Debug.Assert(CurrentStepIndex == null);
|
||||
|
||||
screenContent.Child = stack = new ScreenStack
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
|
||||
CurrentStepIndex = -1;
|
||||
ShowNextStep();
|
||||
}
|
||||
|
||||
protected virtual void ShowNextStep()
|
||||
{
|
||||
Debug.Assert(CurrentStepIndex != null);
|
||||
Debug.Assert(stack != null);
|
||||
|
||||
CurrentStepIndex++;
|
||||
|
||||
if (CurrentStepIndex < steps.Count)
|
||||
{
|
||||
var nextScreen = (Screen)Activator.CreateInstance(steps[CurrentStepIndex.Value])!;
|
||||
|
||||
loadingShowDelegate = Scheduler.AddDelayed(() => loading.Show(), 200);
|
||||
nextScreen.OnLoadComplete += _ =>
|
||||
{
|
||||
loadingShowDelegate?.Cancel();
|
||||
loading.Hide();
|
||||
};
|
||||
|
||||
stack.Push(nextScreen);
|
||||
}
|
||||
else
|
||||
{
|
||||
CurrentStepIndex = null;
|
||||
Hide();
|
||||
}
|
||||
|
||||
updateButtons();
|
||||
}
|
||||
|
||||
private void updateButtons() => DisplayedFooterContent?.UpdateButtons(CurrentStepIndex, steps);
|
||||
|
||||
public partial class WizardFooterContent : VisibilityContainer
|
||||
{
|
||||
public ShearedButton NextButton { get; private set; } = null!;
|
||||
|
||||
public Action? ShowNextStep;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = NextButton = new ShearedButton(0)
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Margin = new MarginPadding { Right = 12f },
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 1,
|
||||
Text = FirstRunSetupOverlayStrings.GetStarted,
|
||||
DarkerColour = colourProvider.Colour2,
|
||||
LighterColour = colourProvider.Colour1,
|
||||
Action = () => ShowNextStep?.Invoke(),
|
||||
};
|
||||
}
|
||||
|
||||
public void UpdateButtons(int? currentStep, IReadOnlyList<Type> steps)
|
||||
{
|
||||
NextButton.Enabled.Value = currentStep != null;
|
||||
|
||||
if (currentStep == null)
|
||||
return;
|
||||
|
||||
bool isFirstStep = currentStep == 0;
|
||||
bool isLastStep = currentStep == steps.Count - 1;
|
||||
|
||||
if (isFirstStep)
|
||||
NextButton.Text = FirstRunSetupOverlayStrings.GetStarted;
|
||||
else
|
||||
{
|
||||
NextButton.Text = isLastStep
|
||||
? CommonStrings.Finish
|
||||
: LocalisableString.Interpolate($@"{CommonStrings.Next} ({steps[currentStep.Value + 1].GetLocalisableDescription()})");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.FadeIn();
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
this.Delay(400).FadeOut();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -13,9 +13,9 @@ using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.FirstRunSetup
|
||||
namespace osu.Game.Overlays
|
||||
{
|
||||
public abstract partial class FirstRunSetupScreen : Screen
|
||||
public abstract partial class WizardScreen : Screen
|
||||
{
|
||||
private const float offset = 100;
|
||||
|
@ -376,7 +376,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
/// <summary>
|
||||
/// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic.
|
||||
/// </summary>
|
||||
protected virtual ComposeBlueprintContainer CreateBlueprintContainer() => new ComposeBlueprintContainer(this);
|
||||
protected abstract ComposeBlueprintContainer CreateBlueprintContainer();
|
||||
|
||||
protected virtual Drawable CreateHitObjectInspector() => new HitObjectInspector();
|
||||
|
||||
@ -566,28 +566,6 @@ namespace osu.Game.Rulesets.Edit
|
||||
/// <returns>The most relevant <see cref="Playfield"/>.</returns>
|
||||
protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield;
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
{
|
||||
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
|
||||
double? targetTime = null;
|
||||
|
||||
if (snapType.HasFlag(SnapType.GlobalGrids))
|
||||
{
|
||||
if (playfield is ScrollingPlayfield scrollingPlayfield)
|
||||
{
|
||||
targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition);
|
||||
|
||||
// apply beat snapping
|
||||
targetTime = BeatSnapProvider.SnapTime(targetTime.Value);
|
||||
|
||||
// convert back to screen space
|
||||
screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return new SnapResult(screenSpacePosition, targetTime, playfield);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@ -596,7 +574,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
/// Generally used to access certain methods without requiring a generic type for <see cref="HitObjectComposer{T}" />.
|
||||
/// </summary>
|
||||
[Cached]
|
||||
public abstract partial class HitObjectComposer : CompositeDrawable, IPositionSnapProvider
|
||||
public abstract partial class HitObjectComposer : CompositeDrawable
|
||||
{
|
||||
public const float TOOLBOX_CONTRACTED_SIZE_LEFT = 60;
|
||||
public const float TOOLBOX_CONTRACTED_SIZE_RIGHT = 120;
|
||||
@ -639,11 +617,5 @@ namespace osu.Game.Rulesets.Edit
|
||||
/// <param name="timestamp">The time instant to seek to, in milliseconds.</param>
|
||||
/// <param name="objectDescription">The ruleset-specific description of objects to select at the given timestamp.</param>
|
||||
public virtual void SelectFromTimestamp(double timestamp, string objectDescription) { }
|
||||
|
||||
#region IPositionSnapProvider
|
||||
|
||||
public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Compose;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
@ -87,14 +88,13 @@ namespace osu.Game.Rulesets.Edit
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the time and position of this <see cref="PlacementBlueprint"/> based on the provided snap information.
|
||||
/// Updates the time and position of this <see cref="PlacementBlueprint"/>.
|
||||
/// </summary>
|
||||
/// <param name="result">The snap result information.</param>
|
||||
public override void UpdateTimeAndPosition(SnapResult result)
|
||||
public override SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double time)
|
||||
{
|
||||
if (PlacementActive == PlacementState.Waiting)
|
||||
{
|
||||
HitObject.StartTime = result.Time ?? EditorClock.CurrentTime;
|
||||
HitObject.StartTime = time;
|
||||
|
||||
if (HitObject is IHasComboInformation comboInformation)
|
||||
comboInformation.UpdateComboInformation(getPreviousHitObject() as IHasComboInformation);
|
||||
@ -129,6 +129,8 @@ namespace osu.Game.Rulesets.Edit
|
||||
for (int i = 0; i < hasRepeats.NodeSamples.Count; i++)
|
||||
hasRepeats.NodeSamples[i] = HitObject.Samples.Select(o => o.With()).ToList();
|
||||
}
|
||||
|
||||
return new SnapResult(screenSpacePosition, time);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,23 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
/// <summary>
|
||||
/// A snap provider which given a proposed position for a hit object, potentially offers a more correct position and time value inferred from the context of the beatmap.
|
||||
/// </summary>
|
||||
[Cached]
|
||||
public interface IPositionSnapProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Given a position, find a valid time and position snap.
|
||||
/// </summary>
|
||||
/// <param name="screenSpacePosition">The screen-space position to be snapped.</param>
|
||||
/// <param name="snapType">The type of snapping to apply.</param>
|
||||
/// <returns>The time and position post-snapping.</returns>
|
||||
SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit
|
||||
@ -75,18 +76,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
PlacementActive = PlacementState.Finished;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines which objects to snap to for the snap result in <see cref="UpdateTimeAndPosition"/>.
|
||||
/// </summary>
|
||||
public virtual SnapType SnapType => SnapType.All;
|
||||
|
||||
/// <summary>
|
||||
/// Updates the time and position of this <see cref="PlacementBlueprint"/> based on the provided snap information.
|
||||
/// </summary>
|
||||
/// <param name="result">The snap result information.</param>
|
||||
public virtual void UpdateTimeAndPosition(SnapResult result)
|
||||
{
|
||||
}
|
||||
public abstract SnapResult UpdateTimeAndPosition(Vector2 screenSpacePosition, double fallbackTime);
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
|
@ -117,6 +117,23 @@ namespace osu.Game.Rulesets.Edit
|
||||
}
|
||||
}
|
||||
|
||||
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
||||
{
|
||||
var scrollingPlayfield = PlayfieldAtScreenSpacePosition(screenSpacePosition) as ScrollingPlayfield;
|
||||
if (scrollingPlayfield == null)
|
||||
return new SnapResult(screenSpacePosition, null);
|
||||
|
||||
double? targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition);
|
||||
|
||||
// apply beat snapping
|
||||
targetTime = BeatSnapProvider.SnapTime(targetTime.Value);
|
||||
|
||||
// convert back to screen space
|
||||
screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value);
|
||||
|
||||
return new SnapResult(screenSpacePosition, targetTime, scrollingPlayfield);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
@ -1,32 +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;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
[Flags]
|
||||
public enum SnapType
|
||||
{
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Snapping to visible nearby objects.
|
||||
/// </summary>
|
||||
NearbyObjects = 1 << 0,
|
||||
|
||||
/// <summary>
|
||||
/// Grids which are global to the playfield.
|
||||
/// </summary>
|
||||
GlobalGrids = 1 << 1,
|
||||
|
||||
/// <summary>
|
||||
/// Grids which are relative to other nearby hit objects.
|
||||
/// </summary>
|
||||
RelativeGrids = 1 << 2,
|
||||
|
||||
AllGrids = RelativeGrids | GlobalGrids,
|
||||
|
||||
All = NearbyObjects | GlobalGrids | RelativeGrids,
|
||||
}
|
||||
}
|
@ -28,11 +28,12 @@ namespace osu.Game.Rulesets.Scoring
|
||||
result ??= new UnstableRateCalculationResult();
|
||||
|
||||
// Handle rewinding in the simplest way possible.
|
||||
if (hitEvents.Count < result.EventCount + 1)
|
||||
if (hitEvents.Count < result.LastProcessedIndex + 1)
|
||||
result = new UnstableRateCalculationResult();
|
||||
|
||||
for (int i = result.EventCount; i < hitEvents.Count; i++)
|
||||
for (int i = result.LastProcessedIndex + 1; i < hitEvents.Count; i++)
|
||||
{
|
||||
result.LastProcessedIndex = i;
|
||||
HitEvent e = hitEvents[i];
|
||||
|
||||
if (!AffectsUnstableRate(e))
|
||||
@ -84,6 +85,11 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </remarks>
|
||||
public class UnstableRateCalculationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// The last result index processed. For internal incremental calculation use.
|
||||
/// </summary>
|
||||
public int LastProcessedIndex = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Total events processed. For internal incremental calculation use.
|
||||
/// </summary>
|
||||
|
@ -577,6 +577,11 @@ namespace osu.Game.Rulesets.UI
|
||||
/// </summary>
|
||||
public virtual bool AllowGameplayOverlays => true;
|
||||
|
||||
/// <summary>
|
||||
/// On mobile devices, this specifies whether this ruleset requires the device to be in portrait orientation.
|
||||
/// </summary>
|
||||
public virtual bool RequiresPortraitOrientation => false;
|
||||
|
||||
/// <summary>
|
||||
/// Sets a replay to be used, overriding local input.
|
||||
/// </summary>
|
||||
|
@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
private readonly Bindable<bool> waitingOnFrames = new Bindable<bool>();
|
||||
|
||||
private readonly double gameplayStartTime;
|
||||
public double GameplayStartTime { get; }
|
||||
|
||||
private IGameplayClock? parentGameplayClock;
|
||||
|
||||
@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
framedClock = new FramedClock(manualClock = new ManualClock());
|
||||
|
||||
this.gameplayStartTime = gameplayStartTime;
|
||||
GameplayStartTime = gameplayStartTime;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
@ -257,8 +257,8 @@ namespace osu.Game.Rulesets.UI
|
||||
return;
|
||||
}
|
||||
|
||||
if (manualClock.CurrentTime < gameplayStartTime)
|
||||
manualClock.CurrentTime = proposedTime = Math.Min(gameplayStartTime, proposedTime);
|
||||
if (manualClock.CurrentTime < GameplayStartTime)
|
||||
manualClock.CurrentTime = proposedTime = Math.Min(GameplayStartTime, proposedTime);
|
||||
else if (Math.Abs(manualClock.CurrentTime - proposedTime) > sixty_frame_time * 1.2f)
|
||||
{
|
||||
proposedTime = proposedTime > manualClock.CurrentTime
|
||||
|
@ -43,9 +43,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
private readonly Dictionary<T, SelectionBlueprint<T>> blueprintMap = new Dictionary<T, SelectionBlueprint<T>>();
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private IPositionSnapProvider snapProvider { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IEditorChangeHandler changeHandler { get; set; }
|
||||
|
||||
@ -333,19 +330,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
protected void RemoveBlueprintFor(T item)
|
||||
{
|
||||
if (!blueprintMap.Remove(item, out var blueprint))
|
||||
if (!blueprintMap.Remove(item, out var blueprintToRemove))
|
||||
return;
|
||||
|
||||
blueprint.Deselect();
|
||||
blueprint.Selected -= OnBlueprintSelected;
|
||||
blueprint.Deselected -= OnBlueprintDeselected;
|
||||
blueprintToRemove.Deselect();
|
||||
blueprintToRemove.Selected -= OnBlueprintSelected;
|
||||
blueprintToRemove.Deselected -= OnBlueprintDeselected;
|
||||
|
||||
SelectionBlueprints.Remove(blueprint, true);
|
||||
SelectionBlueprints.Remove(blueprintToRemove, true);
|
||||
|
||||
if (movementBlueprints?.Contains(blueprint) == true)
|
||||
if (movementBlueprints?.Any(m => m.blueprint == blueprintToRemove) == true)
|
||||
finishSelectionMovement();
|
||||
|
||||
OnBlueprintRemoved(blueprint.Item);
|
||||
OnBlueprintRemoved(blueprintToRemove.Item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -538,8 +535,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
#region Selection Movement
|
||||
|
||||
private Vector2[][] movementBlueprintsOriginalPositions;
|
||||
private SelectionBlueprint<T>[] movementBlueprints;
|
||||
private (SelectionBlueprint<T> blueprint, Vector2[] originalSnapPositions)[] movementBlueprints;
|
||||
|
||||
/// <summary>
|
||||
/// Whether a blueprint is currently being dragged.
|
||||
@ -572,8 +568,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
return false;
|
||||
|
||||
// Movement is tracked from the blueprint of the earliest item, since it only makes sense to distance snap from that item
|
||||
movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).ToArray();
|
||||
movementBlueprintsOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSnapPoints).ToArray();
|
||||
movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).Select(b => (b, b.ScreenSpaceSnapPoints)).ToArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -594,68 +589,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
if (movementBlueprints == null)
|
||||
return false;
|
||||
|
||||
Debug.Assert(movementBlueprintsOriginalPositions != null);
|
||||
|
||||
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
|
||||
|
||||
if (snapProvider != null)
|
||||
{
|
||||
for (int i = 0; i < movementBlueprints.Length; i++)
|
||||
{
|
||||
if (checkSnappingBlueprintToNearbyObjects(movementBlueprints[i], distanceTravelled, movementBlueprintsOriginalPositions[i]))
|
||||
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 = movementBlueprintsOriginalPositions.First().First() + distanceTravelled;
|
||||
|
||||
// Retrieve a snapped position.
|
||||
var result = snapProvider?.FindSnappedPositionAndTime(movePosition, ~SnapType.NearbyObjects);
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(movementBlueprints.First(), movePosition - movementBlueprints.First().ScreenSpaceSelectionPoint));
|
||||
}
|
||||
|
||||
return ApplySnapResult(movementBlueprints, result);
|
||||
return TryMoveBlueprints(e, movementBlueprints);
|
||||
}
|
||||
|
||||
/// <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<T> 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 = snapProvider.FindSnappedPositionAndTime(testPosition, SnapType.NearbyObjects);
|
||||
|
||||
if (positionalResult.ScreenSpacePosition == testPosition) continue;
|
||||
|
||||
var delta = positionalResult.ScreenSpacePosition - currentPositions[i];
|
||||
|
||||
// attempt to move the objects, and abort any time based snapping if we can.
|
||||
if (SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(blueprint, delta)))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected virtual bool ApplySnapResult(SelectionBlueprint<T>[] blueprints, SnapResult result) =>
|
||||
SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(blueprints.First(), result.ScreenSpacePosition - blueprints.First().ScreenSpaceSelectionPoint));
|
||||
protected abstract bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<T> blueprint, Vector2[] originalSnapPositions)> blueprints);
|
||||
|
||||
/// <summary>
|
||||
/// Finishes the current movement of selected blueprints.
|
||||
@ -666,7 +603,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
if (movementBlueprints == null)
|
||||
return false;
|
||||
|
||||
movementBlueprintsOriginalPositions = null;
|
||||
movementBlueprints = null;
|
||||
|
||||
return true;
|
||||
|
@ -16,9 +16,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
public abstract partial class CircularDistanceSnapGrid : DistanceSnapGrid
|
||||
{
|
||||
[Resolved]
|
||||
private EditorClock editorClock { get; set; } = null!;
|
||||
|
||||
protected CircularDistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null)
|
||||
: base(referenceObject, startPosition, startTime, endTime)
|
||||
{
|
||||
@ -76,7 +73,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
}
|
||||
}
|
||||
|
||||
public override (Vector2 position, double time) GetSnappedPosition(Vector2 position)
|
||||
public override (Vector2 position, double time) GetSnappedPosition(Vector2 position, double? fixedTime = null)
|
||||
{
|
||||
if (MaxIntervals == 0)
|
||||
return (StartPosition, StartTime);
|
||||
@ -100,8 +97,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
if (travelLength < DistanceBetweenTicks)
|
||||
travelLength = DistanceBetweenTicks;
|
||||
|
||||
float snappedDistance = LimitedDistanceSnap.Value
|
||||
? SnapProvider.DurationToDistance(ReferenceObject, editorClock.CurrentTime - ReferenceObject.GetEndTime())
|
||||
float snappedDistance = fixedTime != null
|
||||
? SnapProvider.DurationToDistance(ReferenceObject, fixedTime.Value - ReferenceObject.GetEndTime())
|
||||
// When interacting with the resolved snap provider, the distance spacing multiplier should first be removed
|
||||
// to allow for snapping at a non-multiplied ratio.
|
||||
: SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier, DistanceSnapTarget.End);
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Humanizer;
|
||||
@ -33,7 +34,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// <summary>
|
||||
/// A blueprint container generally displayed as an overlay to a ruleset's playfield.
|
||||
/// </summary>
|
||||
public partial class ComposeBlueprintContainer : EditorBlueprintContainer
|
||||
public abstract partial class ComposeBlueprintContainer : EditorBlueprintContainer
|
||||
{
|
||||
private readonly Container<PlacementBlueprint> placementBlueprintContainer;
|
||||
|
||||
@ -52,7 +53,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// </remarks>
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => editorScreen?.MainContent.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
public ComposeBlueprintContainer(HitObjectComposer composer)
|
||||
protected override IEnumerable<SelectionBlueprint<HitObject>> ApplySelectionOrder(IEnumerable<SelectionBlueprint<HitObject>> blueprints) =>
|
||||
base.ApplySelectionOrder(blueprints)
|
||||
.OrderBy(b => Math.Min(Math.Abs(EditorClock.CurrentTime - b.Item.GetEndTime()), Math.Abs(EditorClock.CurrentTime - b.Item.StartTime)));
|
||||
|
||||
protected ComposeBlueprintContainer(HitObjectComposer composer)
|
||||
: base(composer)
|
||||
{
|
||||
placementBlueprintContainer = new Container<PlacementBlueprint>
|
||||
@ -340,12 +345,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
private void updatePlacementTimeAndPosition()
|
||||
{
|
||||
var snapResult = Composer.FindSnappedPositionAndTime(InputManager.CurrentState.Mouse.Position, CurrentPlacement.SnapType);
|
||||
|
||||
// if no time was found from positional snapping, we should still quantize to the beat.
|
||||
snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null);
|
||||
|
||||
CurrentPlacement.UpdateTimeAndPosition(snapResult);
|
||||
CurrentPlacement.UpdateTimeAndPosition(InputManager.CurrentState.Mouse.Position, Beatmap.SnapTime(EditorClock.CurrentTime, null));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -10,7 +10,6 @@ using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -61,18 +60,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
[Resolved]
|
||||
private BindableBeatDivisor beatDivisor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When enabled, distance snap should only snap to the current time (as per the editor clock).
|
||||
/// This is to emulate stable behaviour.
|
||||
/// </summary>
|
||||
protected Bindable<bool> LimitedDistanceSnap { get; private set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
LimitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
|
||||
}
|
||||
|
||||
private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit);
|
||||
|
||||
protected readonly HitObject ReferenceObject;
|
||||
@ -143,8 +130,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// Snaps a position to this grid.
|
||||
/// </summary>
|
||||
/// <param name="position">The original position in coordinate space local to this <see cref="DistanceSnapGrid"/>.</param>
|
||||
/// <param name="fixedTime">
|
||||
/// Whether the snap operation should be temporally constrained to a particular time instant,
|
||||
/// thus fixing the possible positions to a set distance from the <see cref="ReferenceObject"/>.
|
||||
/// </param>
|
||||
/// <returns>A tuple containing the snapped position in coordinate space local to this <see cref="DistanceSnapGrid"/> and the respective time value.</returns>
|
||||
public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position);
|
||||
public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double? fixedTime = null);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the applicable colour for a beat index.
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
@ -17,7 +16,7 @@ using osu.Game.Rulesets.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
public partial class EditorBlueprintContainer : BlueprintContainer<HitObject>
|
||||
public abstract partial class EditorBlueprintContainer : BlueprintContainer<HitObject>
|
||||
{
|
||||
[Resolved]
|
||||
protected EditorClock EditorClock { get; private set; }
|
||||
@ -73,27 +72,22 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
protected override IEnumerable<SelectionBlueprint<HitObject>> SortForMovement(IReadOnlyList<SelectionBlueprint<HitObject>> blueprints)
|
||||
=> blueprints.OrderBy(b => b.Item.StartTime);
|
||||
|
||||
protected override bool ApplySnapResult(SelectionBlueprint<HitObject>[] blueprints, SnapResult result)
|
||||
protected void ApplySnapResultTime(SnapResult result, double referenceTime)
|
||||
{
|
||||
if (!base.ApplySnapResult(blueprints, result))
|
||||
return false;
|
||||
if (!result.Time.HasValue)
|
||||
return;
|
||||
|
||||
if (result.Time.HasValue)
|
||||
// Apply the start time at the newly snapped-to position
|
||||
double offset = result.Time.Value - referenceTime;
|
||||
|
||||
if (offset != 0)
|
||||
{
|
||||
// Apply the start time at the newly snapped-to position
|
||||
double offset = result.Time.Value - blueprints.First().Item.StartTime;
|
||||
|
||||
if (offset != 0)
|
||||
Beatmap.PerformOnSelection(obj =>
|
||||
{
|
||||
Beatmap.PerformOnSelection(obj =>
|
||||
{
|
||||
obj.StartTime += offset;
|
||||
Beatmap.Update(obj);
|
||||
});
|
||||
}
|
||||
obj.StartTime += offset;
|
||||
Beatmap.Update(obj);
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void AddBlueprintFor(HitObject item)
|
||||
@ -131,10 +125,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override IEnumerable<SelectionBlueprint<HitObject>> ApplySelectionOrder(IEnumerable<SelectionBlueprint<HitObject>> blueprints) =>
|
||||
base.ApplySelectionOrder(blueprints)
|
||||
.OrderBy(b => Math.Min(Math.Abs(EditorClock.CurrentTime - b.Item.GetEndTime()), Math.Abs(EditorClock.CurrentTime - b.Item.StartTime)));
|
||||
|
||||
protected override SelectionBlueprintContainer CreateSelectionBlueprintContainer() => new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new EditorSelectionHandler();
|
||||
|
@ -22,7 +22,7 @@ using osuTK.Input;
|
||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
[Cached]
|
||||
public partial class Timeline : ZoomableScrollContainer, IPositionSnapProvider
|
||||
public partial class Timeline : ZoomableScrollContainer
|
||||
{
|
||||
private const float timeline_height = 80;
|
||||
|
||||
@ -332,7 +332,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
return (float)(time / editorClock.TrackLength * Content.DrawWidth);
|
||||
}
|
||||
|
||||
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)
|
||||
{
|
||||
double time = TimeAtPosition(Content.ToLocalSpace(screenSpacePosition).X);
|
||||
return new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(time));
|
||||
|
@ -107,6 +107,23 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
return base.OnDragStart(e);
|
||||
}
|
||||
|
||||
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 = timeline?.FindSnappedPositionAndTime(movePosition) ?? new SnapResult(movePosition, null);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private float dragTimeAccumulated;
|
||||
|
||||
protected override void Update()
|
||||
|
@ -330,6 +330,18 @@ namespace osu.Game.Screens.Edit
|
||||
editorTimelineShowTicks = config.GetBindable<bool>(OsuSetting.EditorTimelineShowTicks);
|
||||
editorContractSidebars = config.GetBindable<bool>(OsuSetting.EditorContractSidebars);
|
||||
|
||||
// These two settings don't work together. Make them mutually exclusive to let the user know.
|
||||
editorAutoSeekOnPlacement.BindValueChanged(enabled =>
|
||||
{
|
||||
if (enabled.NewValue)
|
||||
editorLimitedDistanceSnap.Value = false;
|
||||
});
|
||||
editorLimitedDistanceSnap.BindValueChanged(enabled =>
|
||||
{
|
||||
if (enabled.NewValue)
|
||||
editorAutoSeekOnPlacement.Value = false;
|
||||
});
|
||||
|
||||
AddInternal(new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
|
@ -8,6 +8,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Localisation;
|
||||
@ -97,7 +98,17 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
if (!source.Exists)
|
||||
return false;
|
||||
|
||||
var tagSource = TagLib.File.Create(source.FullName);
|
||||
TagLib.File? tagSource;
|
||||
|
||||
try
|
||||
{
|
||||
tagSource = TagLib.File.Create(source.FullName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, "The selected audio track appears to be corrupted. Please select another one.");
|
||||
return false;
|
||||
}
|
||||
|
||||
changeResource(source, applyToAllDifficulties, @"audio",
|
||||
metadata => metadata.AudioFile,
|
||||
@ -192,16 +203,40 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
editor?.Save();
|
||||
}
|
||||
|
||||
// to avoid scaring users, both background & audio choosers use fake `FileInfo`s with user-friendly filenames
|
||||
// when displaying an imported beatmap rather than the actual SHA-named file in storage.
|
||||
// however, that means that when a background or audio file is chosen that is broken or doesn't exist on disk when switching away from the fake files,
|
||||
// the rollback could enter an infinite loop, because the fake `FileInfo`s *also* don't exist on disk - at least not in the fake location they indicate.
|
||||
// to circumvent this issue, just allow rollback to proceed always without actually running any of the change logic to ensure visual consistency.
|
||||
// note that this means that `Change{BackgroundImage,AudioTrack}()` are required to not have made any modifications to the beatmap files
|
||||
// (or at least cleaned them up properly themselves) if they return `false`.
|
||||
private bool rollingBackBackgroundChange;
|
||||
private bool rollingBackAudioChange;
|
||||
|
||||
private void backgroundChanged(ValueChangedEvent<FileInfo?> file)
|
||||
{
|
||||
if (rollingBackBackgroundChange)
|
||||
return;
|
||||
|
||||
if (file.NewValue == null || !ChangeBackgroundImage(file.NewValue, backgroundChooser.ApplyToAllDifficulties.Value))
|
||||
{
|
||||
rollingBackBackgroundChange = true;
|
||||
backgroundChooser.Current.Value = file.OldValue;
|
||||
rollingBackBackgroundChange = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void audioTrackChanged(ValueChangedEvent<FileInfo?> file)
|
||||
{
|
||||
if (rollingBackAudioChange)
|
||||
return;
|
||||
|
||||
if (file.NewValue == null || !ChangeAudioTrack(file.NewValue, audioTrackChooser.ApplyToAllDifficulties.Value))
|
||||
{
|
||||
rollingBackAudioChange = true;
|
||||
audioTrackChooser.Current.Value = file.OldValue;
|
||||
rollingBackAudioChange = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -61,6 +61,17 @@ namespace osu.Game.Screens
|
||||
/// </summary>
|
||||
bool HideMenuCursorOnNonMouseInput { get; }
|
||||
|
||||
/// <summary>
|
||||
/// On mobile phones, this specifies whether this <see cref="OsuScreen"/> requires the device to be in portrait orientation.
|
||||
/// Tablet devices are unaffected by this property.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// By default, all screens in the game display in landscape orientation on phones.
|
||||
/// Setting this to <c>true</c> will display this screen in portrait orientation instead,
|
||||
/// and switch back to landscape when transitioning back to a regular non-portrait screen.
|
||||
/// </remarks>
|
||||
bool RequiresPortraitOrientation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether overlays should be able to be opened when this screen is current.
|
||||
/// </summary>
|
||||
|
@ -12,15 +12,19 @@ using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
@ -31,11 +35,17 @@ using Container = osu.Framework.Graphics.Containers.Container;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
|
||||
{
|
||||
public abstract partial class DrawableRoom : CompositeDrawable
|
||||
public abstract partial class DrawableRoom : CompositeDrawable, IHasContextMenu
|
||||
{
|
||||
protected const float CORNER_RADIUS = 10;
|
||||
private const float height = 100;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuGame? game { get; set; }
|
||||
|
||||
public readonly Room Room;
|
||||
|
||||
protected readonly Bindable<PlaylistItem?> SelectedItem = new Bindable<PlaylistItem?>();
|
||||
@ -335,6 +345,26 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
|
||||
}
|
||||
}
|
||||
|
||||
public virtual MenuItem[] ContextMenuItems
|
||||
{
|
||||
get
|
||||
{
|
||||
var items = new List<MenuItem>();
|
||||
|
||||
if (Room.RoomID.HasValue)
|
||||
{
|
||||
items.AddRange([
|
||||
new OsuMenuItem("View in browser", MenuItemType.Standard, () => game?.OpenUrlExternally(formatRoomUrl(Room.RoomID.Value))),
|
||||
new OsuMenuItem("Copy link", MenuItemType.Standard, () => game?.CopyUrlToClipboard(formatRoomUrl(Room.RoomID.Value)))
|
||||
]);
|
||||
}
|
||||
|
||||
return items.ToArray();
|
||||
|
||||
string formatRoomUrl(long id) => $@"{api.WebsiteRootUrl}/multiplayer/rooms/{id}";
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual UpdateableBeatmapBackgroundSprite CreateBackground() => new UpdateableBeatmapBackgroundSprite();
|
||||
|
||||
protected virtual IEnumerable<Drawable> CreateBottomDetails()
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user