1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-05 10:23:20 +08:00

Compare commits

...

92 Commits

Author SHA1 Message Date
Olivier Schipper
ede1f1015d
Merge 21de5a837a into aa0ee5cf3a 2024-12-04 10:38:33 -05:00
Salman Alshamrani
aa0ee5cf3a
Merge pull request #30970 from peppy/results-screen-quick-retry-transition
Fix quick retry transition from results screen
2024-12-04 07:12:38 -05:00
Salman Alshamrani
a7586c52d0
Merge branch 'master' into results-screen-quick-retry-transition 2024-12-04 06:31:14 -05:00
Dean Herbert
e555131b39
Merge pull request #30971 from smoogipoo/improve-multi-search
Improve multiplayer listing search by making it fuzzy
2024-12-04 00:11:32 -08:00
Dan Balasescu
ad4df82593
Improve multiplayer listing search by making it fuzzy 2024-12-04 16:26:36 +09:00
Dan Balasescu
a8963cf317
Merge pull request #30969 from peppy/buttons-search-term
Add "buttons" as a search term for key bindings
2024-12-04 15:52:12 +09:00
Dean Herbert
a4d58648e2
Fix quick retry transition from results screen 2024-12-04 14:31:39 +09:00
Dean Herbert
296fa69edd
Add "buttons" as a search term for key bindings 2024-12-04 14:30:59 +09:00
Dan Balasescu
f09d8f097a
Merge pull request #30953 from peppy/notification-while-chedcking-for-updates
Show an ongoing operation when checking for updates
2024-12-03 17:27:10 +09:00
Dan Balasescu
be05f2a1c2
Merge pull request #30929 from timschumi/rate-change-ready
Account for rate changing mods when disabling the "Ready" button
2024-12-03 16:30:16 +09:00
Dan Balasescu
6ff1dec7b2
Add tests 2024-12-03 15:45:58 +09:00
Dean Herbert
457957d3b8
Refactor check-update flow to better handle unobserved exceptions 2024-12-03 14:23:10 +09:00
Dean Herbert
2ceb3f6f85
Show an ongoing operation when checking for updates
Addresses https://github.com/ppy/osu/discussions/30950.
2024-12-03 13:43:20 +09:00
Dean Herbert
ce4aac4184
Merge pull request #30917 from bdach/fix-incorrect-taiko-legacy-combo
Fix strong drum rolls being counted for double the combo in legacy scoring attributes
2024-12-02 20:08:49 -08:00
Tim Schumacher
e920cfa187 Move rate-changing TODO to a common place in CalculateRateWithMods 2024-12-02 23:49:51 +01:00
Tim Schumacher
164b809c89 Document ready button enable state with some comments 2024-11-30 23:02:22 +01:00
Tim Schumacher
f4e155bfa6 Account for rate changing mods when disabling the "Ready" button 2024-11-30 16:01:32 +01:00
Bartłomiej Dach
3cfa455369
Fix strong drum rolls being counted for double the combo in legacy scoring attributes 2024-11-29 10:54:32 +01:00
Dean Herbert
21de5a837a
Merge branch 'master' into command-pattern-real-3 2024-11-13 19:38:43 +09:00
OliBomby
86310f8170 Merge remote-tracking branch 'upstream/master' into command-pattern-real-3 2024-11-03 23:10:45 +01:00
OliBomby
a7273df59b replace QueueUpdateHitObject with explicit set in transaction
This command doesn't make much sense as a "revertible change" and it requires assumptions about the debouncing of Update calls.
Instead I made a set in the Transaction that just stores the hit objects to update on undo/redo. I think this is pretty ugly and I'd prefer it if the `Apply()` of a change would invoke Update on its own, but I'm afraid that this might update the object too frequently and break drag behaviours.
2024-11-03 23:10:41 +01:00
OliBomby
7a54d480d1 Rename to RevertibleChangeExtensions 2024-10-19 17:45:36 +02:00
OliBomby
9cec28630e Delete SliderPathExtensions.cs 2024-10-19 17:44:51 +02:00
OliBomby
f0ab9f6128 Rename Submit extension method to Apply
I think this fits better because it makes clear that this applies the change
2024-10-18 23:12:07 +02:00
OliBomby
9f62626bfa Turn slider path extensions into IRevertibleChange 2024-10-18 22:48:41 +02:00
OliBomby
b710742d2f refactor to alternative extension method 2024-10-18 00:37:01 +02:00
OliBomby
cb5be12f47 fix typo 2024-10-18 00:14:27 +02:00
OliBomby
0ac520dec8 fix ?. on SafeSubmit calls 2024-10-18 00:03:53 +02:00
OliBomby
e580932cb3 Update comments 2024-10-16 20:48:10 +02:00
OliBomby
3d54e4891f remove unused SafeSubmit 2024-10-16 20:21:47 +02:00
OliBomby
09448eb6c4 Remove proxies and mergeable and old changehandler 2024-10-16 20:17:42 +02:00
OliBomby
d004e552ae fix warning 2024-10-13 02:11:34 +02:00
OliBomby
f6881f45a9 Update IEditorCommand.cs 2024-10-13 01:29:35 +02:00
OliBomby
20ce649326 fix lacking hitobject updates in undo history 2024-10-13 01:07:02 +02:00
OliBomby
c8e2adc884 fix selection blueprint not updating position when hitobject not selected 2024-10-13 01:06:06 +02:00
OliBomby
6741f1c87f fix warnings 2024-10-13 00:59:03 +02:00
OliBomby
4ddec49c34 Create UpdateHitObjectCommand.cs 2024-10-13 00:03:31 +02:00
OliBomby
75eefcfc9b fix mix changehandler and command handler 2024-10-12 22:31:35 +02:00
OliBomby
8cbd40f0e8 prevent hitsample assignment on open sample point 2024-10-12 21:51:09 +02:00
OliBomby
8fee3f537f combine change handler into command handler 2024-10-12 21:25:37 +02:00
OliBomby
db29feb3fa Make EditorCommandHandler a TransactionalCommitComponent 2024-10-12 21:24:53 +02:00
OliBomby
7a3bc731e0 clean up SliderPathExtensions 2024-10-12 12:48:29 +02:00
OliBomby
76981737ff move SetPathTypeCommand to correct folder 2024-10-12 12:27:33 +02:00
Marvin Schürz
ff26ab390c Use correct namespace in SliderPathCommandProxyExtensions 2024-10-11 02:58:59 +02:00
Marvin Schürz
ca00a54543 Remove unused imports 2024-10-11 02:58:47 +02:00
Marvin Schürz
b0eea931f7 Make ListCommandProxy.submit private 2024-10-11 02:58:30 +02:00
Marvin Schürz
bf195c06fe Convert more stuff to command proxies 2024-10-11 02:58:23 +02:00
Marvin Schürz
4ec51b9a74 Add documentation to EditorCommandHandler methods 2024-10-11 02:37:50 +02:00
Marvin Schürz
23be7fecff Refactor command merging 2024-10-11 02:34:13 +02:00
Marvin Schürz
76dd26dd87 Update stuff to command proxies 2024-10-11 01:57:47 +02:00
Marvin Schürz
0e9bafdbe5 Convert more stuff to commands 2024-10-11 01:51:43 +02:00
Marvin Schürz
8a55311101 Fix command merging for undo 2024-10-11 00:23:46 +02:00
Marvin Schürz
224c39f702 Remove IHasMutableXPosition, IHasMutableYPosition from IHasMutablePosition 2024-10-10 23:39:55 +02:00
Marvin Schürz
de5864ab1d Refactor command proxies 2024-10-10 23:24:22 +02:00
Marvin Schürz
57c12191e5 Add SetPathTypeCommand 2024-10-10 23:10:56 +02:00
Marvin Schürz
8aa5385e3f Add SetNewComboCommand 2024-10-10 23:10:51 +02:00
Marvin Schürz
5518db962f Update more commands to extends PropertyChangeCommand 2024-10-10 22:35:29 +02:00
Marvin Schürz
be90c47614 Update SetExpectedDistanceCommand to extend PropertyChangeCommand 2024-10-10 22:22:00 +02:00
Marvin Schürz
238f6c88b8 Implement IsRedundant for PropertyChangeCommand 2024-10-10 22:20:51 +02:00
Marvin Schürz
7e17b64452 Add some documentation for Commands 2024-10-10 22:20:36 +02:00
Marvin Schürz
68d3f0a683 Fix comparison in PropertyChangeCommand.MergeWith 2024-10-10 21:58:48 +02:00
Marvin Schürz
2175f77831 Use correct value for undo in PropertyChangeCommand 2024-10-10 21:40:36 +02:00
Marvin Schürz
6dccccb607 Make position commands extends PropertyChangeCommand 2024-10-10 21:40:26 +02:00
Marvin Schürz
2cf828bfc0 Rename SetXCommand and SetYCommand 2024-10-10 21:35:22 +02:00
Marvin Schürz
9d68673a1e Merge remote-tracking branch 'origin/feature/command-handler' into feature/command-handler
# Conflicts:
#	osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
#	osu.Game.Rulesets.Osu/Edit/Commands/OsuHitObjectCommandProxy.cs
#	osu.Game/Screens/Edit/Commands/SetPositionCommand.cs
2024-10-10 21:29:56 +02:00
Marvin Schürz
7d243ebcbe Rename MoveCommand to SetPositionCommand 2024-10-10 21:22:32 +02:00
maarvin
ce12b487a8
Merge pull request #2 from OliBomby/command-pattern-real-2
Implement variant type generic proxies without heap allocations
2024-10-10 21:10:05 +02:00
OliBomby
3c3678ffbd swap type arguments order for something more logical i think 2024-10-10 18:35:30 +02:00
OliBomby
fcda194c96 fix warning 2024-10-10 17:53:25 +02:00
OliBomby
ffadc7d781 fix mergeable commands 2024-10-10 17:52:57 +02:00
OliBomby
c30e70cc57 fix warnings 2024-10-10 17:10:44 +02:00
OliBomby
86a11f6567 Merge branch 'feature/command-handler' into command-pattern-real-2 2024-10-10 17:08:07 +02:00
Marvin Schürz
3fb986e8bb Fix formatting 2024-10-10 15:30:24 +02:00
Marvin Schürz
597396d64a Merge branch 'master' into feature/command-handler 2024-10-10 15:27:09 +02:00
Marvin Schürz
4a2995d7e5 Add PropertyChangeCommand 2024-10-10 15:27:03 +02:00
OliBomby
0422dc71cc Implement variant type generic proxies without heap allocations 2024-10-10 14:59:46 +02:00
Marvin Schürz
1924463465 Add mergeable commands 2024-10-10 14:05:50 +02:00
OliBomby
28e86badad attempt proxing 2024-10-10 01:20:56 +02:00
Marvin Schürz
4814ccbedd Don't commit in DragStart 2024-10-10 00:01:37 +02:00
OliBomby
f9b7f26ef3 fix setters 2024-10-09 23:26:17 +02:00
OliBomby
6b1fc292c5 ensure Proxy created when DI finished 2024-10-09 23:26:07 +02:00
OliBomby
05a87c9a6e Merge branch 'feature/command-handler' of https://github.com/minetoblend/osu into command-pattern-real-2 2024-10-09 22:26:12 +02:00
Marvin Schürz
39dc35712c Attempt to convert slider editing to command pattern 2024-10-09 21:20:07 +02:00
Marvin Schürz
307d52549e Make x/y position mutable in OsuHitObject 2024-10-08 21:25:03 +02:00
Marvin Schürz
1d953e0e6f Add MoveXCommand and MoveYCommand 2024-10-08 21:22:05 +02:00
Marvin Schürz
c50adc80b0 Add SetStartTimeCommand 2024-10-08 20:56:17 +02:00
Marvin Schürz
fb5d3deb91 Remove Component superclass from EditorCommandHandler 2024-10-08 20:56:04 +02:00
Marvin Schürz
508701f4dd Use commands for moving HitObjects in OsuSelectionHandler 2024-10-08 20:55:50 +02:00
Marvin Schürz
fe9e84b47d Add AddHitObjectCommand and RemoveHitObjectCommand 2024-10-08 20:29:27 +02:00
Marvin Schürz
867e986240 Add MoveCommand 2024-10-08 20:29:06 +02:00
Marvin Schürz
18f7321ac6 Add SafeSubmit extension for nullable command handler 2024-10-08 20:19:15 +02:00
Marvin Schürz
b2276fbee7 Add EditorCommandHandler 2024-10-08 20:18:16 +02:00
40 changed files with 1013 additions and 149 deletions

View File

@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Changes;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
using Direction = osu.Framework.Graphics.Direction;
@ -95,9 +96,10 @@ namespace osu.Game.Rulesets.Catch.Edit
if (h is JuiceStream juiceStream)
{
juiceStream.Path.Reverse(out Vector2 positionalOffset);
juiceStream.OriginalX += positionalOffset.X;
juiceStream.LegacyConvertedY += positionalOffset.Y;
var reverse = new ReverseSliderPathChange(juiceStream.Path);
reverse.Apply();
juiceStream.OriginalX += reverse.PositionalOffset.X;
juiceStream.LegacyConvertedY += reverse.PositionalOffset.Y;
EditorBeatmap.Update(juiceStream);
}
}

View File

@ -24,8 +24,10 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Changes;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Changes;
using osuTK;
using osuTK.Input;
@ -113,7 +115,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
return;
if (segment.Count > 3)
first.Type = PathType.BEZIER;
new PathControlPointTypeChange(first, PathType.BEZIER).Apply(changeHandler);
if (segment.Count != 3)
return;
@ -121,7 +123,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
ReadOnlySpan<Vector2> points = segment.Select(p => p.Position).ToArray();
RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points);
if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
first.Type = PathType.BEZIER;
new PathControlPointTypeChange(first, PathType.BEZIER).Apply(changeHandler);
}
/// <summary>
@ -371,26 +373,23 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
int thirdPointIndex = indexInSegment + 2;
if (pointsInSegment.Count > thirdPointIndex + 1)
pointsInSegment[thirdPointIndex].Type = pointsInSegment[0].Type;
new PathControlPointTypeChange(pointsInSegment[thirdPointIndex], pointsInSegment[0].Type).Apply(changeHandler);
}
hitObject.Path.ExpectedDistance.Value = null;
p.ControlPoint.Type = type;
new ExpectedDistanceChange(hitObject.Path, null).Apply(changeHandler);
new PathControlPointTypeChange(p.ControlPoint, type).Apply(changeHandler);
}
EnsureValidPathTypes();
if (hitObject.Path.Distance < originalDistance)
hitObject.SnapTo(distanceSnapProvider);
new SnapToChange<T>(hitObject, distanceSnapProvider).Apply(changeHandler);
else
hitObject.Path.ExpectedDistance.Value = originalDistance;
new ExpectedDistanceChange(hitObject.Path, originalDistance).Apply(changeHandler);
changeHandler?.EndChange();
}
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
#region Drag handling
private Vector2[] dragStartPositions;
@ -412,6 +411,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
changeHandler?.BeginChange();
}
[Resolved(CanBeNull = true)]
private NewBeatmapEditorChangeHandler changeHandler { get; set; }
public void DragInProgress(DragEvent e)
{
Vector2[] oldControlPoints = hitObject.Path.ControlPoints.Select(cp => cp.Position).ToArray();
@ -426,8 +428,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position;
hitObject.Position += movementDelta;
hitObject.StartTime = result?.Time ?? hitObject.StartTime;
new PositionChange(hitObject, hitObject.Position + movementDelta).Apply(changeHandler);
new StartTimeChange(hitObject, result?.Time ?? hitObject.StartTime).Apply(changeHandler);
for (int i = 1; i < hitObject.Path.ControlPoints.Count; i++)
{
@ -437,7 +439,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
// All other selected control points (if any) will move together with the head point
// (and so they will not move at all, relative to each other).
if (!selectedControlPoints.Contains(controlPoint))
controlPoint.Position -= movementDelta;
new PathControlPointPositionChange(controlPoint, controlPoint.Position - movementDelta).Apply(changeHandler);
}
}
else
@ -450,28 +452,28 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
PathControlPoint controlPoint = controlPoints[i];
if (selectedControlPoints.Contains(controlPoint))
controlPoint.Position = dragStartPositions[i] + movementDelta;
new PathControlPointPositionChange(controlPoint, dragStartPositions[i] + movementDelta).Apply(changeHandler);
}
}
// Snap the path to the current beat divisor before checking length validity.
hitObject.SnapTo(distanceSnapProvider);
new SnapToChange<T>(hitObject, distanceSnapProvider).Apply(changeHandler);
if (!hitObject.Path.HasValidLength)
{
for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++)
hitObject.Path.ControlPoints[i].Position = oldControlPoints[i];
new PathControlPointPositionChange(hitObject.Path.ControlPoints[i], oldControlPoints[i]).Apply(changeHandler);
hitObject.Position = oldPosition;
hitObject.StartTime = oldStartTime;
new PositionChange(hitObject, oldPosition).Apply(changeHandler);
new StartTimeChange(hitObject, oldStartTime).Apply(changeHandler);
// Snap the path length again to undo the invalid length.
hitObject.SnapTo(distanceSnapProvider);
new SnapToChange<T>(hitObject, distanceSnapProvider).Apply(changeHandler);
return;
}
// Maintain the path types in case they got defaulted to bezier at some point during the drag.
for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++)
hitObject.Path.ControlPoints[i].Type = dragPathTypes[i];
new PathControlPointTypeChange(hitObject.Path.ControlPoints[i], dragPathTypes[i]).Apply(changeHandler);
EnsureValidPathTypes();
}

View File

@ -21,9 +21,11 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Edit.Changes;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Changes;
using osu.Game.Screens.Edit.Compose;
using osuTK;
using osuTK.Input;
@ -50,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private EditorBeatmap? editorBeatmap { get; set; }
[Resolved]
private IEditorChangeHandler? changeHandler { get; set; }
private NewBeatmapEditorChangeHandler? changeHandler { get; set; }
[Resolved]
private BindableBeatDivisor? beatDivisor { get; set; }
@ -122,6 +124,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
pathVersion.BindValueChanged(_ => editorBeatmap?.Update(HitObject));
BodyPiece.UpdateFrom(HitObject);
HitObject.DefaultsApplied += _ => BodyPiece.UpdateFrom(HitObject);
if (editorBeatmap != null)
selectedObjects.BindTo(editorBeatmap.SelectedHitObjects);
@ -280,9 +283,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
if (Precision.AlmostEquals(proposedDistance, HitObject.Path.Distance) && Precision.AlmostEquals(proposedVelocity, HitObject.SliderVelocityMultiplier))
return;
HitObject.SliderVelocityMultiplier = proposedVelocity;
HitObject.Path.ExpectedDistance.Value = proposedDistance;
new SliderVelocityMultiplierChange(HitObject, proposedVelocity).Apply(changeHandler);
new ExpectedDistanceChange(HitObject.Path, proposedDistance).Apply(changeHandler);
editorBeatmap?.Update(HitObject);
changeHandler?.RecordUpdate(HitObject);
}
/// <summary>
@ -303,8 +307,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
if (Precision.AlmostBigger(segmentEnds[segmentIndex], 1, 1E-3))
{
sliderPath.ControlPoints.RemoveRange(i + 1, sliderPath.ControlPoints.Count - i - 1);
sliderPath.ControlPoints[^1].Type = null;
new RemoveRangePathControlPointChange(sliderPath.ControlPoints, i + 1, sliderPath.ControlPoints.Count - i - 1).Apply(changeHandler);
new PathControlPointTypeChange(sliderPath.ControlPoints[^1], null).Apply(changeHandler);
break;
}
@ -442,11 +446,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
var pathControlPoint = new PathControlPoint { Position = position };
// Move the control points from the insertion index onwards to make room for the insertion
controlPoints.Insert(insertionIndex, pathControlPoint);
new InsertPathControlPointChange(HitObject.Path.ControlPoints, insertionIndex, pathControlPoint).Apply(changeHandler);
ControlPointVisualiser?.EnsureValidPathTypes();
HitObject.SnapTo(distanceSnapProvider);
new SnapToChange<Slider>(HitObject, distanceSnapProvider).Apply(changeHandler);
return pathControlPoint;
}
@ -462,15 +466,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
// The first control point in the slider must have a type, so take it from the previous "first" one
// Todo: Should be handled within SliderPath itself
if (c == controlPoints[0] && controlPoints.Count > 1 && controlPoints[1].Type == null)
controlPoints[1].Type = controlPoints[0].Type;
new PathControlPointTypeChange(controlPoints[1], controlPoints[0].Type).Apply(changeHandler);
controlPoints.Remove(c);
new RemovePathControlPointChange(HitObject.Path.ControlPoints, c).Apply(changeHandler);
}
ControlPointVisualiser?.EnsureValidPathTypes();
// Snap the slider to the current beat divisor before checking length validity.
HitObject.SnapTo(distanceSnapProvider);
new SnapToChange<Slider>(HitObject, distanceSnapProvider).Apply(changeHandler);
// If there are 0 or 1 remaining control points, or the slider has an invalid length, it is in a degenerate form and should be deleted
if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLength)
@ -483,8 +487,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
// So the slider needs to be offset by this amount instead, and all control points offset backwards such that the path is re-positioned at (0, 0)
Vector2 first = controlPoints[0].Position;
foreach (var c in controlPoints)
c.Position -= first;
HitObject.Position += first;
new PathControlPointPositionChange(c, c.Position - first).Apply(changeHandler);
new PositionChange(HitObject, HitObject.Position + first).Apply(changeHandler);
}
private void splitControlPoints(List<PathControlPoint> controlPointsToSplitAt)
@ -514,7 +519,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
// Extract the split portion and remove from the original slider.
var splitControlPoints = controlPoints.Take(index + 1).ToList();
controlPoints.RemoveRange(0, index);
new RemoveRangePathControlPointChange(HitObject.Path.ControlPoints, 0, index).Apply(changeHandler);
var newSlider = new Slider
{
@ -528,18 +533,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
};
// Increase the start time of the slider before adding the new slider so the new slider is immediately inserted at the correct index and internal state remains valid.
HitObject.StartTime += split_gap;
new StartTimeChange(HitObject, HitObject.StartTime + split_gap).Apply(changeHandler);
editorBeatmap.Add(newSlider);
new AddHitObjectChange(editorBeatmap, newSlider).Apply(changeHandler);
HitObject.NewCombo = false;
HitObject.Path.ExpectedDistance.Value -= newSlider.Path.CalculatedDistance;
HitObject.StartTime += newSlider.SpanDuration;
new NewComboChange(HitObject, false).Apply(changeHandler);
new ExpectedDistanceChange(HitObject.Path, HitObject.Path.ExpectedDistance.Value - newSlider.Path.CalculatedDistance).Apply(changeHandler);
new StartTimeChange(HitObject, HitObject.StartTime + newSlider.SpanDuration).Apply(changeHandler);
// In case the remainder of the slider has no length left over, give it length anyways so we don't get a 0 length slider.
if (HitObject.Path.ExpectedDistance.Value <= Precision.DOUBLE_EPSILON)
{
HitObject.Path.ExpectedDistance.Value = null;
new ExpectedDistanceChange(HitObject.Path, null).Apply(changeHandler);
}
}
@ -547,8 +552,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
// As a final step, we must reset its control points to have an origin of (0,0).
Vector2 first = controlPoints[0].Position;
foreach (var c in controlPoints)
c.Position -= first;
HitObject.Position += first;
new PathControlPointPositionChange(c, c.Position - first).Apply(changeHandler);
new PositionChange(HitObject, HitObject.Position + first).Apply(changeHandler);
}
private void convertToStream()
@ -576,19 +582,19 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
Vector2 position = HitObject.Position + HitObject.Path.PositionAt(pathPosition);
editorBeatmap.Add(new HitCircle
new AddHitObjectChange(editorBeatmap, new HitCircle
{
StartTime = time,
Position = position,
NewCombo = i == 0 && HitObject.NewCombo,
Samples = HitObject.HeadCircle.Samples.Select(s => s.With()).ToList()
});
}).Apply(changeHandler);
i += 1;
time = HitObject.StartTime + i * streamSpacing;
}
editorBeatmap.Remove(HitObject);
new RemoveHitObjectChange(editorBeatmap, HitObject).Apply(changeHandler);
changeHandler?.EndChange();
}

View File

@ -0,0 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Changes;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Changes
{
public class PositionChange : PropertyChange<OsuHitObject, Vector2>
{
public PositionChange(OsuHitObject target, Vector2 value)
: base(target, value)
{
}
protected override Vector2 ReadValue(OsuHitObject target) => target.Position;
protected override void WriteValue(OsuHitObject target, Vector2 value) => target.Position = value;
}
}

View File

@ -15,8 +15,11 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Edit.Changes;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Changes;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Utils;
using osuTK;
@ -51,6 +54,9 @@ namespace osu.Game.Rulesets.Osu.Edit
return false;
}
[Resolved(canBeNull: true)]
private NewBeatmapEditorChangeHandler? changeHandler { get; set; }
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent)
{
var hitObjects = selectedMovableObjects;
@ -72,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Edit
// this will potentially move the selection out of bounds...
foreach (var h in hitObjects)
h.Position += localDelta;
new PositionChange(h, h.Position + localDelta).Apply(changeHandler);
// but this will be corrected.
moveSelectionInBounds();
@ -105,12 +111,13 @@ namespace osu.Game.Rulesets.Osu.Edit
foreach (var h in hitObjects)
{
if (moreThanOneObject)
h.StartTime = endTime - (h.GetEndTime() - startTime);
new StartTimeChange(h, endTime - (h.GetEndTime() - startTime)).Apply(changeHandler);
if (h is Slider slider)
{
slider.Path.Reverse(out Vector2 offset);
slider.Position += offset;
var reverse = new ReverseSliderPathChange(slider.Path);
reverse.Apply(changeHandler);
new PositionChange(slider, slider.Position + reverse.PositionalOffset).Apply(changeHandler);
}
}
@ -118,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Edit
hitObjects = hitObjects.OrderBy(obj => obj.StartTime).ToList();
for (int i = 0; i < hitObjects.Count; ++i)
hitObjects[i].NewCombo = newComboOrder[i];
new NewComboChange(hitObjects[i], newComboOrder[i]).Apply(changeHandler);
return true;
}
@ -167,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Edit
if (!Precision.AlmostEquals(flippedPosition, h.Position))
{
h.Position = flippedPosition;
new PositionChange(h, flippedPosition).Apply(changeHandler);
didFlip = true;
}
@ -176,7 +183,7 @@ namespace osu.Game.Rulesets.Osu.Edit
didFlip = true;
foreach (var cp in slider.Path.ControlPoints)
cp.Position = GeometryUtils.GetFlippedPosition(flipAxis, controlPointFlipQuad, cp.Position);
new PathControlPointPositionChange(cp, GeometryUtils.GetFlippedPosition(flipAxis, controlPointFlipQuad, cp.Position)).Apply(changeHandler);
}
}
@ -206,7 +213,7 @@ namespace osu.Game.Rulesets.Osu.Edit
delta.Y -= quad.BottomRight.Y - DrawHeight;
foreach (var h in hitObjects)
h.Position += delta;
new PositionChange(h, h.Position + delta).Apply(changeHandler);
}
/// <summary>
@ -245,7 +252,7 @@ namespace osu.Game.Rulesets.Osu.Edit
if (mergedHitObject.Path.ControlPoints.Count == 0)
{
mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(Vector2.Zero, PathType.LINEAR));
new InsertPathControlPointChange(mergedHitObject.Path.ControlPoints, mergedHitObject.Path.ControlPoints.Count, new PathControlPoint(Vector2.Zero, PathType.LINEAR)).Apply(changeHandler);
}
// Merge all the selected hit objects into one slider path.
@ -259,15 +266,15 @@ namespace osu.Game.Rulesets.Osu.Edit
float distanceToLastControlPoint = Vector2.Distance(mergedHitObject.Path.ControlPoints[^1].Position, offset);
// Calculate the distance required to travel to the expected distance of the merging slider.
mergedHitObject.Path.ExpectedDistance.Value = mergedHitObject.Path.CalculatedDistance + distanceToLastControlPoint + hasPath.Path.Distance;
new ExpectedDistanceChange(mergedHitObject.Path, mergedHitObject.Path.CalculatedDistance + distanceToLastControlPoint + hasPath.Path.Distance).Apply(changeHandler);
// Remove the last control point if it sits exactly on the start of the next control point.
if (Precision.AlmostEquals(distanceToLastControlPoint, 0))
{
mergedHitObject.Path.ControlPoints.RemoveAt(mergedHitObject.Path.ControlPoints.Count - 1);
new RemovePathControlPointChange(mergedHitObject.Path.ControlPoints, mergedHitObject.Path.ControlPoints.Count - 1).Apply(changeHandler);
}
mergedHitObject.Path.ControlPoints.AddRange(hasPath.Path.ControlPoints.Select(o => new PathControlPoint(o.Position + offset, o.Type)));
new AddRangePathControlPointChange(mergedHitObject.Path.ControlPoints, hasPath.Path.ControlPoints.Select(o => new PathControlPoint(o.Position + offset, o.Type))).Apply(changeHandler);
lastCircle = false;
}
else
@ -275,11 +282,11 @@ namespace osu.Game.Rulesets.Osu.Edit
// Turn the last control point into a linear type if this is the first merging circle in a sequence, so the subsequent control points can be inherited path type.
if (!lastCircle)
{
mergedHitObject.Path.ControlPoints.Last().Type = PathType.LINEAR;
new PathControlPointTypeChange(mergedHitObject.Path.ControlPoints.Last(), PathType.LINEAR).Apply(changeHandler);
}
mergedHitObject.Path.ControlPoints.Add(new PathControlPoint(selectedMergeableObject.Position - mergedHitObject.Position));
mergedHitObject.Path.ExpectedDistance.Value = null;
new InsertPathControlPointChange(mergedHitObject.Path.ControlPoints, mergedHitObject.Path.ControlPoints.Count, new PathControlPoint(selectedMergeableObject.Position - mergedHitObject.Position)).Apply(changeHandler);
new ExpectedDistanceChange(mergedHitObject.Path, null).Apply(changeHandler);
lastCircle = true;
}
}
@ -289,17 +296,17 @@ namespace osu.Game.Rulesets.Osu.Edit
{
foreach (var selectedMergeableObject in mergeableObjects.Skip(1))
{
EditorBeatmap.Remove(selectedMergeableObject);
new RemoveHitObjectChange(EditorBeatmap, selectedMergeableObject).Apply(changeHandler);
}
}
else
{
foreach (var selectedMergeableObject in mergeableObjects)
{
EditorBeatmap.Remove(selectedMergeableObject);
new RemoveHitObjectChange(EditorBeatmap, selectedMergeableObject).Apply(changeHandler);
}
EditorBeatmap.Add(mergedHitObject);
new AddHitObjectChange(EditorBeatmap, mergedHitObject).Apply(changeHandler);
}
// Make sure the merged hitobject is selected.

View File

@ -9,8 +9,10 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Changes;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Changes;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Utils;
using osuTK;
@ -20,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Edit
public partial class OsuSelectionRotationHandler : SelectionRotationHandler
{
[Resolved]
private IEditorChangeHandler? changeHandler { get; set; }
private NewBeatmapEditorChangeHandler? changeHandler { get; set; }
private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>();
@ -78,14 +80,17 @@ namespace osu.Game.Rulesets.Osu.Edit
foreach (var ho in objectsInRotation)
{
ho.Position = GeometryUtils.RotatePointAroundOrigin(originalPositions[ho], actualOrigin, rotation);
new PositionChange(ho, GeometryUtils.RotatePointAroundOrigin(originalPositions[ho], actualOrigin, rotation)).Apply(changeHandler);
if (ho is IHasPath withPath)
{
var originalPath = originalPathControlPointPositions[withPath];
for (int i = 0; i < withPath.Path.ControlPoints.Count; ++i)
withPath.Path.ControlPoints[i].Position = GeometryUtils.RotatePointAroundOrigin(originalPath[i], Vector2.Zero, rotation);
{
new PathControlPointPositionChange(withPath.Path.ControlPoints[i],
GeometryUtils.RotatePointAroundOrigin(originalPath[i], Vector2.Zero, rotation)).Apply(changeHandler);
}
}
}
}

View File

@ -13,9 +13,11 @@ using osu.Framework.Utils;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Changes;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Changes;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Utils;
using osuTK;
@ -35,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Edit
public Bindable<bool> IsScalingSlider { get; private set; } = new BindableBool();
[Resolved]
private IEditorChangeHandler? changeHandler { get; set; }
private NewBeatmapEditorChangeHandler? changeHandler { get; set; }
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider? snapProvider { get; set; }
@ -113,7 +115,7 @@ namespace osu.Game.Rulesets.Osu.Edit
foreach (var (ho, originalState) in objectsInScale)
{
ho.Position = GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position, axisRotation);
new PositionChange(ho, GeometryUtils.GetScaledPosition(scale, actualOrigin, originalState.Position, axisRotation)).Apply(changeHandler);
}
}
@ -166,15 +168,16 @@ namespace osu.Game.Rulesets.Osu.Edit
// Maintain the path types in case they were defaulted to bezier at some point during scaling
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
{
slider.Path.ControlPoints[i].Position = GeometryUtils.GetScaledPosition(scale, Vector2.Zero, originalInfo.PathControlPointPositions[i], axisRotation);
slider.Path.ControlPoints[i].Type = originalInfo.PathControlPointTypes[i];
new PathControlPointPositionChange(slider.Path.ControlPoints[i],
GeometryUtils.GetScaledPosition(scale, Vector2.Zero, originalInfo.PathControlPointPositions[i], axisRotation)).Apply(changeHandler);
new PathControlPointTypeChange(slider.Path.ControlPoints[i], originalInfo.PathControlPointTypes[i]).Apply(changeHandler);
}
// Snap the slider's length to the current beat divisor
// to calculate the final resulting duration / bounding box before the final checks.
slider.SnapTo(snapProvider);
new SnapToChange<Slider>(slider, snapProvider).Apply(changeHandler);
slider.Position = GeometryUtils.GetScaledPosition(scale, origin, originalInfo.Position, axisRotation);
new PositionChange(slider, GeometryUtils.GetScaledPosition(scale, origin, originalInfo.Position, axisRotation)).Apply(changeHandler);
//if sliderhead or sliderend end up outside playfield, revert scaling.
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
@ -184,12 +187,14 @@ namespace osu.Game.Rulesets.Osu.Edit
return;
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
slider.Path.ControlPoints[i].Position = originalInfo.PathControlPointPositions[i];
{
new PathControlPointPositionChange(slider.Path.ControlPoints[i], originalInfo.PathControlPointPositions[i]).Apply(changeHandler);
}
slider.Position = originalInfo.Position;
new PositionChange(slider, originalInfo.Position).Apply(changeHandler);
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
slider.SnapTo(snapProvider);
new SnapToChange<Slider>(slider, snapProvider).Apply(changeHandler);
}
private (bool X, bool Y) isQuadInBounds(Quad quad)
@ -327,7 +332,7 @@ namespace osu.Game.Rulesets.Osu.Edit
delta.Y -= quad.BottomRight.Y - OsuPlayfield.BASE_SIZE.Y;
foreach (var (h, _) in objectsInScale!)
h.Position += delta;
new PositionChange(h, h.Position + delta).Apply(changeHandler);
}
private struct OriginalHitObjectState

View File

@ -144,6 +144,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
foreach (var nested in hitObject.NestedHitObjects)
simulateHit(nested, ref attributes);
return;
case StrongNestedHitObject:
// we never need to deal with these directly.
// the only thing strong hits do in terms of scoring is double their object's score increase,
// which is already handled at the parent object level via the `strongable.IsStrong` check lower down in this method.
// not handling these here can lead to them falsely being counted as combo-increasing when handling strong drum rolls!
return;
}
if (hitObject is DrumRollTick tick)

View File

@ -5,25 +5,64 @@ using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual.OnlinePlay;
namespace osu.Game.Tests.Visual.Playlists
{
public partial class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene
{
private const double track_length = 10000;
[Resolved]
private IAPIProvider api { get; set; } = null!;
protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
private BeatmapManager beatmaps = null!;
private RulesetStore rulesets = null!;
private BeatmapSetInfo? importedSet;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(new ScoreManager(rulesets, () => beatmaps, LocalStorage, Realm, API));
Dependencies.Cache(Realm);
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
Realm.Write(r =>
{
foreach (var set in r.All<BeatmapSetInfo>())
{
foreach (var b in set.Beatmaps)
{
// These will all have a virtual track length of 1000, see WorkingBeatmap.GetVirtualTrack().
b.Length = track_length - 1000;
}
}
});
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
}
[Test]
public void TestStatusUpdateOnEnter()
{
@ -69,5 +108,42 @@ namespace osu.Game.Tests.Visual.Playlists
AddAssert("close button present", () => roomScreen.ChildrenOfType<DangerousRoundedButton>().Any());
AddUntilStep("wait for close button to disappear", () => !roomScreen.ChildrenOfType<DangerousRoundedButton>().Any());
}
[TestCase(120_000, true)] // Definitely enough time.
[TestCase(45_000, true)] // Enough time.
[TestCase(35_000, false)] // Not enough time to complete beatmap after lenience.
[TestCase(20_000, false)] // Not enough time.
[TestCase(5_000, false)] // Not enough time to complete beatmap before lenience.
[TestCase(37_500, true, 2)] // Enough time to complete beatmap after mods are applied.
public void TestReadyButtonEnablementPeriod(int offsetMs, bool enabled, double rate = 1)
{
Room room = null!;
PlaylistsRoomSubScreen roomScreen = null!;
AddStep("create room", () =>
{
RoomManager.AddRoom(room = new Room
{
Name = @"Test Room",
Host = api.LocalUser.Value,
Category = RoomCategory.Normal,
StartDate = DateTimeOffset.Now,
EndDate = DateTimeOffset.Now.AddMilliseconds(offsetMs),
Playlist =
[
new PlaylistItem(importedSet!.Beatmaps[0])
{
RequiredMods = rate == 1
? []
: [new APIMod(new OsuModDoubleTime { SpeedChange = { Value = rate } })]
}
]
});
});
AddStep("push screen", () => LoadScreen(roomScreen = new PlaylistsRoomSubScreen(room)));
AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen());
AddUntilStep("ready button enabled", () => roomScreen.ChildrenOfType<PlaylistsReadyButton>().SingleOrDefault()?.Enabled.Value, () => Is.EqualTo(enabled));
}
}
}

View File

@ -44,6 +44,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString CheckUpdate => new TranslatableString(getKey(@"check_update"), @"Check for updates");
/// <summary>
/// "Checking for updates"
/// </summary>
public static LocalisableString CheckingForUpdates => new TranslatableString(getKey(@"checking_for_updates"), @"Checking for updates");
/// <summary>
/// "Open osu! folder"
/// </summary>

View File

@ -4,7 +4,6 @@
using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Logging;
@ -13,6 +12,7 @@ using osu.Framework.Screens;
using osu.Framework.Statistics;
using osu.Game.Configuration;
using osu.Game.Localisation;
using osu.Game.Online.Multiplayer;
using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.Settings.Sections.Maintenance;
using osu.Game.Updater;
@ -36,8 +36,11 @@ namespace osu.Game.Overlays.Settings.Sections.General
[Resolved]
private Storage storage { get; set; } = null!;
[Resolved]
private OsuGame? game { get; set; }
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, OsuGame? game)
private void load(OsuConfigManager config)
{
Add(new SettingsEnumDropdown<ReleaseStream>
{
@ -50,23 +53,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
Add(checkForUpdatesButton = new SettingsButton
{
Text = GeneralSettingsStrings.CheckUpdate,
Action = () =>
{
checkForUpdatesButton.Enabled.Value = false;
Task.Run(updateManager.CheckForUpdateAsync).ContinueWith(task => Schedule(() =>
{
if (!task.GetResultSafely())
{
notifications?.Post(new SimpleNotification
{
Text = GeneralSettingsStrings.RunningLatestRelease(game!.Version),
Icon = FontAwesome.Solid.CheckCircle,
});
}
checkForUpdatesButton.Enabled.Value = true;
}));
}
Action = () => checkForUpdates().FireAndForget()
});
}
@ -94,6 +81,44 @@ namespace osu.Game.Overlays.Settings.Sections.General
}
}
private async Task checkForUpdates()
{
if (updateManager == null || game == null)
return;
checkForUpdatesButton.Enabled.Value = false;
var checkingNotification = new ProgressNotification
{
Text = GeneralSettingsStrings.CheckingForUpdates,
};
notifications?.Post(checkingNotification);
try
{
bool foundUpdate = await updateManager.CheckForUpdateAsync().ConfigureAwait(true);
if (!foundUpdate)
{
notifications?.Post(new SimpleNotification
{
Text = GeneralSettingsStrings.RunningLatestRelease(game.Version),
Icon = FontAwesome.Solid.CheckCircle,
});
}
}
catch
{
}
finally
{
// This sequence allows the notification to be immediately dismissed.
checkingNotification.State = ProgressNotificationState.Cancelled;
checkingNotification.Close(false);
checkForUpdatesButton.Enabled.Value = true;
}
}
private void exportLogs()
{
ProgressNotification notification = new ProgressNotification

View File

@ -13,7 +13,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{
protected override LocalisableString Header => BindingSettingsStrings.ShortcutAndGameplayBindings;
public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { @"keybindings", @"controls", @"keyboard", @"keys" });
public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { @"keybindings", @"controls", @"keyboard", @"keys", @"buttons" });
public BindingSettings(KeyBindingPanel keyConfig)
{

View File

@ -28,6 +28,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Changes;
using osu.Game.Screens.Edit.Components.RadioButtons;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose;
@ -67,6 +68,9 @@ namespace osu.Game.Rulesets.Edit
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
[Resolved(canBeNull: true)]
private NewBeatmapEditorChangeHandler changeHandler { get; set; }
public override ComposeBlueprintContainer BlueprintContainer => blueprintContainer;
private ComposeBlueprintContainer blueprintContainer;
@ -272,7 +276,8 @@ namespace osu.Game.Rulesets.Edit
TernaryStates = CreateTernaryButtons().ToArray();
togglesCollection.AddRange(TernaryStates.Select(b => new DrawableTernaryButton(b)));
sampleBankTogglesCollection.AddRange(BlueprintContainer.SampleBankTernaryStates.Zip(BlueprintContainer.SampleAdditionBankTernaryStates).Select(b => new SampleBankTernaryButton(b.First, b.Second)));
sampleBankTogglesCollection.AddRange(BlueprintContainer.SampleBankTernaryStates.Zip(BlueprintContainer.SampleAdditionBankTernaryStates)
.Select(b => new SampleBankTernaryButton(b.First, b.Second)));
SetSelectTool();
@ -550,13 +555,13 @@ namespace osu.Game.Rulesets.Edit
public void CommitPlacement(HitObject hitObject)
{
EditorBeatmap.PlacementObject.Value = null;
EditorBeatmap.Add(hitObject);
new AddHitObjectChange(EditorBeatmap, hitObject).Apply(changeHandler);
if (autoSeekOnPlacement.Value && EditorClock.CurrentTime < hitObject.StartTime)
EditorClock.SeekSmoothlyTo(hitObject.StartTime);
}
public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject);
public void Delete(HitObject hitObject) => new RemoveHitObjectChange(EditorBeatmap, hitObject).Apply(changeHandler);
#endregion

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit.Changes
{
public class AddHitObjectChange : IRevertibleChange
{
public EditorBeatmap Beatmap;
public HitObject HitObject;
public AddHitObjectChange(EditorBeatmap beatmap, HitObject hitObject)
{
Beatmap = beatmap;
HitObject = hitObject;
}
public void Apply() => Beatmap.Add(HitObject);
public void Revert() => Beatmap.Remove(HitObject);
}
}

View File

@ -0,0 +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.Collections.Generic;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit.Changes
{
/// <summary>
/// Adds a range of <see cref="PathControlPoint"/>s to the provided <see cref="BindableList{T}"/>.
/// </summary>
public class AddRangePathControlPointChange : CompositeChange
{
private readonly BindableList<PathControlPoint> controlPoints;
private readonly IEnumerable<PathControlPoint> points;
public AddRangePathControlPointChange(BindableList<PathControlPoint> controlPoints, IEnumerable<PathControlPoint> points)
{
this.controlPoints = controlPoints;
this.points = points;
}
protected override void SubmitChanges()
{
foreach (var point in points)
Submit(new InsertPathControlPointChange(controlPoints, controlPoints.Count, point));
}
}
}

View File

@ -0,0 +1,46 @@
// 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;
namespace osu.Game.Screens.Edit.Changes
{
public abstract class CompositeChange : IRevertibleChange
{
private List<IRevertibleChange>? changes;
public void Apply()
{
if (changes == null)
{
changes = new List<IRevertibleChange>();
SubmitChanges();
return;
}
foreach (var change in changes)
change.Apply();
}
public void Revert()
{
if (changes == null)
throw new System.InvalidOperationException("Cannot revert before applying.");
for (int i = changes.Count - 1; i >= 0; i--)
changes[i].Revert();
}
protected void Submit(IRevertibleChange change)
{
change.Apply();
changes!.Add(change);
}
/// <summary>
/// Applies the tracks the changes of this <see cref="CompositeChange"/>.
/// </summary>
/// <remarks>Use <see cref="Submit"/> to apply the <see cref="IRevertibleChange"/> created in this method.</remarks>
protected abstract void SubmitChanges();
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit.Changes
{
public class ExpectedDistanceChange : PropertyChange<SliderPath, double?>
{
public ExpectedDistanceChange(SliderPath target, double? value)
: base(target, value)
{
}
protected override double? ReadValue(SliderPath target) => target.ExpectedDistance.Value;
protected override void WriteValue(SliderPath target, double? value) => target.ExpectedDistance.Value = value;
}
}

View File

@ -0,0 +1,32 @@
// 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.
namespace osu.Game.Screens.Edit.Changes
{
/// <summary>
/// Represents a change which can be undone.
/// </summary>
public interface IRevertibleChange
{
/// <summary>
/// Applies this change to the current state.
/// </summary>
void Apply();
/// <summary>
/// Applies the inverse of this change to the current state.
/// </summary>
void Revert();
}
public static class RevertibleChangeExtension
{
public static void Apply(this IRevertibleChange change, NewBeatmapEditorChangeHandler? changeHandler, bool commitImmediately = false)
{
if (changeHandler != null)
changeHandler.Submit(change, commitImmediately);
else
change.Apply();
}
}
}

View File

@ -0,0 +1,28 @@
// 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.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit.Changes
{
public class InsertPathControlPointChange : IRevertibleChange
{
public readonly IList<PathControlPoint> Target;
public readonly int InsertionIndex;
public readonly PathControlPoint Item;
public InsertPathControlPointChange(IList<PathControlPoint> target, int insertionIndex, PathControlPoint item)
{
Target = target;
InsertionIndex = insertionIndex;
Item = item;
}
public void Apply() => Target.Insert(InsertionIndex, Item);
public void Revert() => Target.RemoveAt(InsertionIndex);
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Screens.Edit.Changes
{
public class NewComboChange : PropertyChange<IHasComboInformation, bool>
{
public NewComboChange(IHasComboInformation target, bool value)
: base(target, value)
{
}
protected override bool ReadValue(IHasComboInformation target) => target.NewCombo;
protected override void WriteValue(IHasComboInformation target, bool value) => target.NewCombo = value;
}
}

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Objects;
using osuTK;
namespace osu.Game.Screens.Edit.Changes
{
public class PathControlPointPositionChange : PropertyChange<PathControlPoint, Vector2>
{
public PathControlPointPositionChange(PathControlPoint target, Vector2 value)
: base(target, value)
{
}
protected override Vector2 ReadValue(PathControlPoint target) => target.Position;
protected override void WriteValue(PathControlPoint target, Vector2 value) => target.Position = value;
}
}

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Screens.Edit.Changes
{
public class PathControlPointTypeChange : PropertyChange<PathControlPoint, PathType?>
{
public PathControlPointTypeChange(PathControlPoint target, PathType? value)
: base(target, value)
{
}
protected override PathType? ReadValue(PathControlPoint target) => target.Type;
protected override void WriteValue(PathControlPoint target, PathType? value) => target.Type = value;
}
}

View File

@ -0,0 +1,49 @@
// 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.
namespace osu.Game.Screens.Edit.Changes
{
/// <summary>
/// Represents a single property update on a given <see cref="Target"/>.
/// </summary>
/// <typeparam name="TTarget">Type of the object owning the property</typeparam>
/// <typeparam name="TValue">Type of the property to update</typeparam>
public abstract class PropertyChange<TTarget, TValue> : IRevertibleChange where TTarget : class
{
/// <summary>
/// Reads the current value of the property from the target.
/// </summary>
protected abstract TValue ReadValue(TTarget target);
/// <summary>
/// Writes the new value to the target object.
/// </summary>
protected abstract void WriteValue(TTarget target, TValue value);
/// <summary>
/// The target object, which owns the property to change.
/// </summary>
public readonly TTarget Target;
/// <summary>
/// The value to change the property to.
/// </summary>
public readonly TValue Value;
/// <summary>
/// The original value of the property before the change.
/// </summary>
public readonly TValue OriginalValue;
protected PropertyChange(TTarget target, TValue value)
{
Target = target;
Value = value;
OriginalValue = ReadValue(target);
}
public void Apply() => WriteValue(Target, Value);
public void Revert() => WriteValue(Target, OriginalValue);
}
}

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit.Changes
{
public class RemoveHitObjectChange : IRevertibleChange
{
public EditorBeatmap Beatmap;
public HitObject HitObject;
public RemoveHitObjectChange(EditorBeatmap beatmap, HitObject hitObject)
{
Beatmap = beatmap;
HitObject = hitObject;
}
public void Apply() => Beatmap.Remove(HitObject);
public void Revert() => Beatmap.Add(HitObject);
}
}

View File

@ -0,0 +1,35 @@
// 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.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit.Changes
{
public class RemovePathControlPointChange : IRevertibleChange
{
public readonly IList<PathControlPoint> Target;
public readonly int Index;
public readonly PathControlPoint Item;
public RemovePathControlPointChange(IList<PathControlPoint> target, int index)
{
Target = target;
Index = index;
Item = target[index];
}
public RemovePathControlPointChange(IList<PathControlPoint> target, PathControlPoint item)
{
Target = target;
Index = target.IndexOf(item);
Item = item;
}
public void Apply() => Target.RemoveAt(Index);
public void Revert() => Target.Insert(Index, Item);
}
}

View File

@ -0,0 +1,31 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit.Changes
{
/// <summary>
/// Removes a range of <see cref="PathControlPoint"/>s from the provided <see cref="BindableList{T}"/>.
/// </summary>
public class RemoveRangePathControlPointChange : CompositeChange
{
private readonly BindableList<PathControlPoint> controlPoints;
private readonly int startIndex;
private readonly int count;
public RemoveRangePathControlPointChange(BindableList<PathControlPoint> controlPoints, int startIndex, int count)
{
this.controlPoints = controlPoints;
this.startIndex = startIndex;
this.count = count;
}
protected override void SubmitChanges()
{
for (int i = 0; i < count; i++)
Submit(new RemovePathControlPointChange(controlPoints, startIndex));
}
}
}

View File

@ -3,29 +3,34 @@
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Rulesets.Objects
namespace osu.Game.Screens.Edit.Changes
{
public static class SliderPathExtensions
/// <summary>
/// Reverse the direction of this path.
/// </summary>
public class ReverseSliderPathChange : CompositeChange
{
/// <summary>
/// Snaps the provided <paramref name="hitObject"/>'s duration using the <paramref name="snapProvider"/>.
/// The positional offset of the resulting path. It should be added to the start position of the path.
/// </summary>
public static void SnapTo<THitObject>(this THitObject hitObject, IDistanceSnapProvider? snapProvider)
where THitObject : HitObject, IHasPath
{
hitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(hitObject, (float)hitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? hitObject.Path.CalculatedDistance;
}
public Vector2 PositionalOffset { get; private set; }
private readonly SliderPath sliderPath;
/// <summary>
/// Reverse the direction of this path.
/// </summary>
/// <param name="sliderPath">The <see cref="SliderPath"/>.</param>
/// <param name="positionalOffset">The positional offset of the resulting path. It should be added to the start position of this path.</param>
public static void Reverse(this SliderPath sliderPath, out Vector2 positionalOffset)
public ReverseSliderPathChange(SliderPath sliderPath)
{
this.sliderPath = sliderPath;
}
protected override void SubmitChanges()
{
var controlPoints = sliderPath.ControlPoints;
@ -33,7 +38,7 @@ namespace osu.Game.Rulesets.Objects
// Inherited points after a linear point, as well as the first control point if it inherited,
// should be treated as linear points, so their types are temporarily changed to linear.
inheritedLinearPoints.ForEach(p => p.Type = PathType.LINEAR);
inheritedLinearPoints.ForEach(p => Submit(new PathControlPointTypeChange(p, PathType.LINEAR)));
double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray();
@ -46,11 +51,11 @@ namespace osu.Game.Rulesets.Objects
segmentEnds = segmentEnds[..^1];
}
controlPoints.RemoveAt(controlPoints.Count - 1);
Submit(new RemovePathControlPointChange(controlPoints, controlPoints.Count - 1));
}
// Restore original control point types.
inheritedLinearPoints.ForEach(p => p.Type = null);
inheritedLinearPoints.ForEach(p => Submit(new PathControlPointTypeChange(p, null)));
// Recalculate middle perfect curve control points at the end of the slider path.
if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PERFECT_CURVE && controlPoints[^2].Type == null && segmentEnds.Any())
@ -61,30 +66,25 @@ namespace osu.Game.Rulesets.Objects
var circleArcPath = new List<Vector2>();
sliderPath.GetPathToProgress(circleArcPath, lastSegmentStart / lastSegmentEnd, 1);
controlPoints[^2].Position = circleArcPath[circleArcPath.Count / 2];
Submit(new PathControlPointPositionChange(controlPoints[^2], circleArcPath[circleArcPath.Count / 2]));
}
sliderPath.reverseControlPoints(out positionalOffset);
reverseControlPoints();
}
/// <summary>
/// Reverses the order of the provided <see cref="SliderPath"/>'s <see cref="PathControlPoint"/>s.
/// </summary>
/// <param name="sliderPath">The <see cref="SliderPath"/>.</param>
/// <param name="positionalOffset">The positional offset of the resulting path. It should be added to the start position of this path.</param>
private static void reverseControlPoints(this SliderPath sliderPath, out Vector2 positionalOffset)
private void reverseControlPoints()
{
var points = sliderPath.ControlPoints.ToArray();
positionalOffset = sliderPath.PositionAt(1);
PositionalOffset = sliderPath.PositionAt(1);
sliderPath.ControlPoints.Clear();
Submit(new RemoveRangePathControlPointChange(sliderPath.ControlPoints, 0, sliderPath.ControlPoints.Count));
PathType? lastType = null;
for (int i = 0; i < points.Length; i++)
{
var p = points[i];
p.Position -= positionalOffset;
var p = new PathControlPoint(points[i].Position, points[i].Type);
p.Position -= PositionalOffset;
// propagate types forwards to last null type
if (i == points.Length - 1)
@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Objects
else if (p.Type != null)
(p.Type, lastType) = (lastType, p.Type);
sliderPath.ControlPoints.Insert(0, p);
Submit(new InsertPathControlPointChange(sliderPath.ControlPoints, 0, p));
}
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Screens.Edit.Changes
{
public class SliderVelocityMultiplierChange : PropertyChange<IHasSliderVelocity, double>
{
public SliderVelocityMultiplierChange(IHasSliderVelocity target, double value)
: base(target, value)
{
}
protected override double ReadValue(IHasSliderVelocity target) => target.SliderVelocityMultiplier;
protected override void WriteValue(IHasSliderVelocity target, double value) => target.SliderVelocityMultiplier = value;
}
}

View File

@ -0,0 +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 osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Screens.Edit.Changes
{
/// <summary>
/// Snaps the provided <see cref="HitObject"/>'s duration using the <see cref="IDistanceSnapProvider"/>.
/// </summary>
public class SnapToChange<THitObject> : CompositeChange where THitObject : HitObject, IHasPath
{
private readonly THitObject hitObject;
private readonly IDistanceSnapProvider? snapProvider;
public SnapToChange(THitObject hitObject, IDistanceSnapProvider? snapProvider)
{
this.hitObject = hitObject;
this.snapProvider = snapProvider;
}
protected override void SubmitChanges()
{
double newDistance = snapProvider?.FindSnappedDistance(hitObject, (float)hitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? hitObject.Path.CalculatedDistance;
Submit(new ExpectedDistanceChange(hitObject.Path, newDistance));
}
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit.Changes
{
public class StartTimeChange : PropertyChange<HitObject, double>
{
public StartTimeChange(HitObject target, double value)
: base(target, value)
{
}
protected override double ReadValue(HitObject target) => target.StartTime;
protected override void WriteValue(HitObject target, double value) => target.StartTime = value;
}
}

View File

@ -47,7 +47,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private IPositionSnapProvider snapProvider { get; set; }
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
private NewBeatmapEditorChangeHandler changeHandler { get; set; }
protected readonly BindableList<T> SelectedItems = new BindableList<T>();

View File

@ -14,6 +14,7 @@ using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Screens.Edit.Changes;
namespace osu.Game.Screens.Edit.Compose.Components
{
@ -25,6 +26,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
[Resolved]
protected EditorBeatmap Beatmap { get; private set; }
[Resolved(canBeNull: true)]
private NewBeatmapEditorChangeHandler changeHandler { get; set; }
protected readonly HitObjectComposer Composer;
private HitObjectUsageEventBuffer usageEventBuffer;
@ -87,8 +91,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
Beatmap.PerformOnSelection(obj =>
{
obj.StartTime += offset;
new StartTimeChange(obj, obj.StartTime + offset).Apply(changeHandler);
Beatmap.Update(obj);
changeHandler?.RecordUpdate(obj);
});
}
}
@ -119,7 +124,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
// handle positional change etc.
foreach (var blueprint in SelectionBlueprints)
{
Beatmap.Update(blueprint.Item);
changeHandler?.RecordUpdate(blueprint.Item);
}
}
protected override bool OnDoubleClick(DoubleClickEvent e)

View File

@ -52,7 +52,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected SelectionBox SelectionBox { get; private set; } = null!;
[Resolved(CanBeNull = true)]
protected IEditorChangeHandler? ChangeHandler { get; private set; }
protected NewBeatmapEditorChangeHandler? ChangeHandler { get; private set; }
public SelectionRotationHandler RotationHandler { get; private set; } = null!;

View File

@ -177,6 +177,8 @@ namespace osu.Game.Screens.Edit
[CanBeNull] // Should be non-null once it can support custom rulesets.
private EditorChangeHandler changeHandler;
private NewBeatmapEditorChangeHandler newChangeHandler;
private DependencyContainer dependencies;
private bool isNewBeatmap;
@ -302,6 +304,9 @@ namespace osu.Game.Screens.Edit
dependencies.CacheAs<IEditorChangeHandler>(changeHandler);
}
newChangeHandler = new NewBeatmapEditorChangeHandler(editorBeatmap);
dependencies.CacheAs(newChangeHandler);
beatDivisor.SetArbitraryDivisor(editorBeatmap.BeatmapInfo.BeatDivisor);
beatDivisor.BindValueChanged(divisor => editorBeatmap.BeatmapInfo.BeatDivisor = divisor.NewValue);
@ -440,8 +445,8 @@ namespace osu.Game.Screens.Edit
}
});
changeHandler?.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
changeHandler?.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
newChangeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
newChangeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
editorBackgroundDim.BindValueChanged(_ => dimBackground());
}
@ -971,9 +976,9 @@ namespace osu.Game.Screens.Edit
#endregion
protected void Undo() => changeHandler?.RestoreState(-1);
protected void Undo() => newChangeHandler.Undo();
protected void Redo() => changeHandler?.RestoreState(1);
protected void Redo() => newChangeHandler.Redo();
protected void SetPreviewPointToCurrentTime()
{

View File

@ -0,0 +1,206 @@
// 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.Bindables;
using osu.Framework.Logging;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Edit.Changes;
namespace osu.Game.Screens.Edit
{
public partial class NewBeatmapEditorChangeHandler : TransactionalCommitComponent
{
private readonly EditorBeatmap editorBeatmap;
public readonly Bindable<bool> CanUndo = new BindableBool();
public readonly Bindable<bool> CanRedo = new BindableBool();
public bool HasUncommittedChanges => currentTransaction.UndoChanges.Count != 0;
private Transaction currentTransaction;
private readonly Stack<Transaction> undoStack = new Stack<Transaction>();
private readonly Stack<Transaction> redoStack = new Stack<Transaction>();
private bool isRestoring;
public NewBeatmapEditorChangeHandler(EditorBeatmap editorBeatmap)
{
currentTransaction = new Transaction();
this.editorBeatmap = editorBeatmap;
editorBeatmap.TransactionBegan += BeginChange;
editorBeatmap.TransactionEnded += EndChange;
editorBeatmap.SaveStateTriggered += SaveState;
}
/// <summary>
/// Submits a change to be applied and added to the history.
/// </summary>
/// <param name="change">Change to be applied.</param>
/// <param name="commitImmediately">Whether to commit the current transaction and push it onto the undo stack immediately.</param>
public void Submit(IRevertibleChange change, bool commitImmediately = false)
{
change.Apply();
record(change);
if (commitImmediately)
UpdateState();
}
/// <summary>
/// Submits a collection of changes to be applied and added to the history.
/// </summary>
/// <param name="changes">Changes to be applied.</param>
/// <param name="commitImmediately">Whether to commit the current transaction and push it onto the undo stack immediately.</param>
public void Submit(IEnumerable<IRevertibleChange> changes, bool commitImmediately = false)
{
foreach (var change in changes)
Submit(change);
if (commitImmediately)
UpdateState();
}
protected override void UpdateState()
{
if (isRestoring)
return;
if (!HasUncommittedChanges)
{
Logger.Log("Nothing to commit");
return;
}
undoStack.Push(currentTransaction);
redoStack.Clear();
Logger.Log($"Added {currentTransaction.UndoChanges.Count} change(s) to undo stack");
currentTransaction = new Transaction();
historyChanged();
}
/// <summary>
/// Undoes the last transaction from the undo stack.
/// Returns false if there are is nothing to undo.
/// </summary>
public bool Undo()
{
if (undoStack.Count == 0)
return false;
var transaction = undoStack.Pop();
revertTransaction(transaction);
redoStack.Push(transaction);
historyChanged();
return true;
}
/// <summary>
/// Redoes the last transaction from the redo stack.
/// Returns false if there are is nothing to redo.
/// </summary>
public bool Redo()
{
if (redoStack.Count == 0)
return false;
var transaction = redoStack.Pop();
applyTransaction(transaction);
undoStack.Push(transaction);
historyChanged();
return true;
}
private void revertTransaction(Transaction transaction)
{
isRestoring = true;
editorBeatmap.BeginChange();
foreach (var change in transaction.UndoChanges.Reverse())
change.Revert();
foreach (var hitObject in transaction.HitObjectUpdates)
editorBeatmap.Update(hitObject);
editorBeatmap.EndChange();
isRestoring = false;
}
private void applyTransaction(Transaction transaction)
{
isRestoring = true;
editorBeatmap.BeginChange();
foreach (var change in transaction.UndoChanges)
change.Apply();
foreach (var hitObject in transaction.HitObjectUpdates)
editorBeatmap.Update(hitObject);
editorBeatmap.EndChange();
isRestoring = false;
}
private void historyChanged()
{
CanUndo.Value = undoStack.Count > 0;
CanRedo.Value = redoStack.Count > 0;
}
private void record(IRevertibleChange change)
{
currentTransaction.Add(change);
}
public void RecordUpdate(HitObject hitObject)
{
currentTransaction.RecordUpdate(hitObject);
}
private readonly struct Transaction
{
public Transaction()
{
undoChanges = new List<IRevertibleChange>();
}
private readonly List<IRevertibleChange> undoChanges;
private readonly HashSet<HitObject> hitObjectUpdates = new HashSet<HitObject>();
/// <summary>
/// The changes to undo the given transaction.
/// Stored in reverse order of original changes to match execution order when undoing.
/// </summary>
public IReadOnlyList<IRevertibleChange> UndoChanges => undoChanges;
public IReadOnlySet<HitObject> HitObjectUpdates => hitObjectUpdates;
public void Add(IRevertibleChange change)
{
undoChanges.Add(change);
}
public void RecordUpdate(HitObject hitObject)
{
hitObjectUpdates.Add(hitObject);
}
}
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -80,19 +81,34 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
bool matchingFilter = true;
matchingFilter &= criteria.Ruleset == null || r.Room.PlaylistItemStats?.RulesetIDs.Any(id => id == criteria.Ruleset.OnlineID) != false;
if (!string.IsNullOrEmpty(criteria.SearchString))
{
// Room name isn't translatable, so ToString() is used here for simplicity.
matchingFilter &= r.FilterTerms.Any(term => term.ToString().Contains(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase));
}
matchingFilter &= matchPermissions(r, criteria.Permissions);
// Room name isn't translatable, so ToString() is used here for simplicity.
string[] filterTerms = r.FilterTerms.Select(t => t.ToString()).ToArray();
string[] searchTerms = criteria.SearchString.Split(' ', StringSplitOptions.RemoveEmptyEntries);
matchingFilter &= searchTerms.All(searchTerm => filterTerms.Any(filterTerm => checkTerm(filterTerm, searchTerm)));
r.MatchingFilter = matchingFilter;
}
});
// Lifted from SearchContainer.
static bool checkTerm(string haystack, string needle)
{
int index = 0;
for (int i = 0; i < needle.Length; i++)
{
int found = CultureInfo.InvariantCulture.CompareInfo.IndexOf(haystack, needle[i], index, CompareOptions.OrdinalIgnoreCase);
if (found < 0)
return false;
index = found + 1;
}
return true;
}
static bool matchPermissions(DrawableLoungeRoom room, RoomPermissionsFilter accessType)
{
switch (accessType)

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using osu.Framework.Allocation;
@ -10,7 +11,9 @@ using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Utils;
namespace osu.Game.Screens.OnlinePlay.Playlists
{
@ -19,6 +22,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
[Resolved]
private IBindable<WorkingBeatmap> gameBeatmap { get; set; } = null!;
[Resolved]
private IBindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
private readonly Room room;
public PlaylistsReadyButton(Room room)
@ -63,14 +69,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
base.Update();
Enabled.Value = hasRemainingAttempts && enoughTimeLeft;
Enabled.Value = hasRemainingAttempts && enoughTimeLeft();
}
public override LocalisableString TooltipText
{
get
{
if (!enoughTimeLeft)
if (!enoughTimeLeft())
return "No time left!";
if (!hasRemainingAttempts)
@ -80,9 +86,17 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
}
}
private bool enoughTimeLeft =>
// This should probably consider the length of the currently selected item, rather than a constant 30 seconds.
room.EndDate != null && DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(gameBeatmap.Value.Track.Length) < room.EndDate;
private bool enoughTimeLeft()
{
double rate = ModUtils.CalculateRateWithMods(mods.Value);
// We want to avoid users not being able to submit scores if they chose to not skip,
// so track length is chosen over playable length.
double trackLength = Math.Round(gameBeatmap.Value.Track.Length / rate);
// Additional 30 second delay added to account for load and/or submit time.
return room.EndDate != null && DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(trackLength) < room.EndDate;
}
protected override void Dispose(bool isDisposing)
{

View File

@ -55,6 +55,8 @@ namespace osu.Game.Screens.Ranking
[Resolved]
private Player? player { get; set; }
private bool skipExitTransition;
[Resolved]
private IAPIProvider api { get; set; } = null!;
@ -203,6 +205,7 @@ namespace osu.Game.Screens.Ranking
{
if (!this.IsCurrentScreen()) return;
skipExitTransition = true;
player?.Restart(true);
},
});
@ -313,7 +316,8 @@ namespace osu.Game.Screens.Ranking
// HitObject references from HitEvent.
Score?.HitEvents.Clear();
this.FadeOut(100);
if (!skipExitTransition)
this.FadeOut(100);
return false;
}

View File

@ -401,7 +401,6 @@ namespace osu.Game.Screens.Select
if (beatmap == null || bpmLabelContainer == null)
return;
// this doesn't consider mods which apply variable rates, yet.
double rate = ModUtils.CalculateRateWithMods(mods.Value);
int bpmMax = FormatUtils.RoundBPM(beatmap.ControlPointInfo.BPMMaximum, rate);

View File

@ -286,6 +286,7 @@ namespace osu.Game.Utils
{
double rate = 1;
// TODO: This doesn't consider mods which apply variable rates, yet.
foreach (var mod in mods.OfType<IApplicableToRate>())
rate = mod.ApplyToRate(0, rate);