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

Compare commits

...

120 Commits

Author SHA1 Message Date
Olivier Schipper
35323152a9
Merge 21de5a837a into ce8e4120b7 2024-12-02 21:23:36 +01:00
Dean Herbert
ce8e4120b7
Merge pull request #30947 from bdach/undesirable-deselect-on-control-click
Do not deselect objects when control-clicking without hitting anything
2024-12-02 07:17:27 -08:00
Bartłomiej Dach
b505ecc7ba
Do not deselect objects when control-clicking without hitting anything
As per feedback in
https://discord.com/channels/90072389919997952/1259818301517725707/1310270647187935284.
2024-12-02 13:51:43 +01:00
Bartłomiej Dach
b14dde937d
Add failing test case 2024-12-02 13:51:41 +01:00
Bartłomiej Dach
6c0ccc5ebe
Merge pull request #30863 from frenzibyte/improve-back-button-display
Delay back button appearance when performing a quick restart
2024-12-02 11:49:45 +01:00
Bartłomiej Dach
52b8753a12
Merge pull request #30749 from Sheppsu/multi-spectator-settings-sidebar
Add player settings to multi spectator screen
2024-12-02 11:34:57 +01:00
Dean Herbert
5b2558cec2
Merge pull request #28473 from bdach/beatmap-info-purge
Move unnecessary properties from `BeatmapInfo` / realm to `IBeatmap`
2024-12-02 16:19:12 +09:00
Dean Herbert
23522b02d8
Use local instead of field for local only usage 2024-12-01 19:53:57 +09:00
Dean Herbert
6afe083ec9
Fix settings showing up during gameplay 2024-12-01 18:44:26 +09:00
Dean Herbert
ddac71628d
Merge branch 'master' into multi-spectator-settings-sidebar 2024-12-01 18:33:46 +09:00
Salman Alshamrani
932afcde01 Make editor make sense 2024-11-28 17:43:32 -05:00
Salman Alshamrani
f792b6de00 Fix comment 2024-11-27 06:07:10 -05:00
Salman Alshamrani
4ae3ccfe48 Make BackButtonVisibility in game class private 2024-11-27 06:05:02 -05:00
Dean Herbert
0f73941808
Combine new implementation back into the old one and use everywhere 2024-11-27 17:47:42 +09:00
Dean Herbert
7fdf13911b
Adjust the colour of non-pinned settings groups' headers to be more legible 2024-11-27 17:47:27 +09:00
Dean Herbert
782ce24ca6
Move player settings out of right flow 2024-11-27 17:09:15 +09:00
Dean Herbert
9c707ed341
Rename class and fix padding considerations 2024-11-27 16:47:54 +09:00
Dean Herbert
5ce55e9cb4
Merge branch 'master' into multi-spectator-settings-sidebar 2024-11-27 16:35:05 +09:00
Salman Alshamrani
3e1b4f4ac5 Rename AllowBackButton to AllowUserExit and rewrite visibility flow structure
Co-authored-by: Dean Herbert <pe@ppy.sh>
2024-11-26 16:52:39 -05:00
Bartłomiej Dach
46d1f00590
Fix Beatmap.Countdown not being copied on conversion 2024-11-26 11:39:03 +01:00
Bartłomiej Dach
cf905d0f5c
Merge branch 'master' into beatmap-info-purge 2024-11-26 10:21:16 +01:00
Salman Alshamrani
ae9119eef0 Hide back button when quick-restarting unless load time takes long 2024-11-24 05:40:06 -05:00
Salman Alshamrani
2420793466 Allow controlling back button visibility state from screens 2024-11-24 05:39:43 -05:00
Sheppsu
3713bb48b7 expand and contract settings from hover 2024-11-23 01:09:58 -05:00
Bartłomiej Dach
ead7e99c59
Fix incorrect comment 2024-11-22 11:06:36 +01:00
Dean Herbert
086a34f5c0
Merge branch 'master' into beatmap-info-purge 2024-11-22 18:47:32 +09:00
Sheppsu
7d4062d2ad remove redundant Scale attribute 2024-11-18 04:04:28 -05:00
Sheppsu
29e7adcd3b add player settings to multi spectator screen 2024-11-18 03:57:50 -05: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
Dean Herbert
58fe502af4
Merge branch 'master' into beatmap-info-purge 2024-09-15 04:53:35 +09:00
Bartłomiej Dach
1d4d806362
Fix WidescreenStoryboard breakage after moving out of BeatmapInfo 2024-07-23 12:19:45 +02:00
Dean Herbert
d707e29ff7
Merge branch 'master' into beatmap-info-purge 2024-07-23 12:09:32 +09:00
Bartłomiej Dach
04527f3c9d
Fix TestBeatmap not transferring newly migrated properties 2024-06-13 09:30:09 +02:00
Bartłomiej Dach
c67e2dc301
Bump schema version 2024-06-13 09:30:08 +02:00
Bartłomiej Dach
9fbf2872e1
Remove no longer applicable region marking 2024-06-12 14:27:40 +02:00
Bartłomiej Dach
dd50d6fa6e
Move CountdownOffset out of BeatmapInfo 2024-06-12 14:15:00 +02:00
Bartłomiej Dach
d373f752d6
Move Countdown out of BeatmapInfo 2024-06-12 14:15:00 +02:00
Bartłomiej Dach
7f2a6f6f5a
Move TimelineZoom out of BeatmapInfo 2024-06-12 14:15:00 +02:00
Bartłomiej Dach
6685c5ab74
Move GridSize out of BeatmapInfo 2024-06-12 14:15:00 +02:00
Bartłomiej Dach
3634307d7c
Move DistanceSpacing out of BeatmapInfo 2024-06-12 14:14:58 +02:00
Bartłomiej Dach
c216283bf4
Move SamplesMatchPlaybackRate out of BeatmapInfo 2024-06-12 13:32:23 +02:00
Bartłomiej Dach
f64a0624a5
Move EpilepsyWarning out of BeatmapInfo 2024-06-12 13:28:41 +02:00
Bartłomiej Dach
1ab86ebd24
Move WidescreenStoryboard out of BeatmapInfo 2024-06-12 13:23:53 +02:00
Bartłomiej Dach
a6b7600bf2
Move LetterboxInBreaks out of BeatmapInfo 2024-06-12 13:18:44 +02:00
Bartłomiej Dach
011c2e3651
Move SpecialStyle out of BeatmapInfo 2024-06-12 13:12:30 +02:00
Bartłomiej Dach
0a4560a03e
Move StackLeniency out of BeatmapInfo 2024-06-12 13:04:33 +02:00
Bartłomiej Dach
4339e2dc4a
Move AudioLeadIn out of BeatmapInfo 2024-06-12 13:04:28 +02:00
84 changed files with 1387 additions and 346 deletions

View File

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

View File

@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
{ {
Caption = "Use special (N+1) style", Caption = "Use special (N+1) style",
HintText = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.", HintText = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.",
Current = { Value = Beatmap.BeatmapInfo.SpecialStyle } Current = { Value = Beatmap.SpecialStyle }
}, },
healthDrainSlider = new FormSliderBar<float> healthDrainSlider = new FormSliderBar<float>
{ {
@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
// for now, update these on commit rather than making BeatmapMetadata bindables. // 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. // after switching database engines we can reconsider if switching to bindables is a good direction.
Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value; Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value;
Beatmap.BeatmapInfo.SpecialStyle = specialStyle.Current.Value; Beatmap.SpecialStyle = specialStyle.Current.Value;
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value; Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value; Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value; Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;

View File

@ -231,6 +231,36 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("slider still has 2 anchors", () => secondSlider.Path.ControlPoints.Count, () => Is.EqualTo(2)); AddAssert("slider still has 2 anchors", () => secondSlider.Path.ControlPoints.Count, () => Is.EqualTo(2));
} }
[Test]
public void TestControlClickDoesNotDiscardExistingSelectionEvenIfNothingHit()
{
var firstSlider = new Slider
{
StartTime = 0,
Position = new Vector2(0, 0),
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(),
new PathControlPoint(new Vector2(100))
}
}
};
AddStep("add object", () => EditorBeatmap.AddRange([firstSlider]));
AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.AddRange([firstSlider]));
AddStep("move mouse to middle of playfield", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre));
AddStep("control-click left mouse", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddAssert("selection preserved", () => EditorBeatmap.SelectedHitObjects.Count, () => Is.EqualTo(1));
}
private ComposeBlueprintContainer blueprintContainer private ComposeBlueprintContainer blueprintContainer
=> Editor.ChildrenOfType<ComposeBlueprintContainer>().First(); => Editor.ChildrenOfType<ComposeBlueprintContainer>().First();

View File

@ -191,7 +191,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private void gridSizeIs(int size) private void gridSizeIs(int size)
=> AddAssert($"grid size is {size}", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Single().Spacing.Value == new Vector2(size) => AddAssert($"grid size is {size}", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Single().Spacing.Value == new Vector2(size)
&& EditorBeatmap.BeatmapInfo.GridSize == size); && EditorBeatmap.GridSize == size);
[Test] [Test]
public void TestGridTypeToggling() public void TestGridTypeToggling()

View File

@ -83,10 +83,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
}) })
} }
}, },
BeatmapInfo = StackLeniency = 0,
{
StackLeniency = 0,
}
}, },
ReplayFrames = new List<ReplayFrame> ReplayFrames = new List<ReplayFrame>
{ {

View File

@ -74,12 +74,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
{ {
BeatmapInfo = new BeatmapInfo BeatmapInfo = new BeatmapInfo
{ {
StackLeniency = 0,
Difficulty = new BeatmapDifficulty Difficulty = new BeatmapDifficulty
{ {
ApproachRate = 8.5f ApproachRate = 8.5f
} }
}, },
StackLeniency = 0,
ControlPointInfo = controlPointInfo ControlPointInfo = controlPointInfo
}; };

View File

@ -465,7 +465,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void performTest(List<ReplayFrame> frames, Beatmap<OsuHitObject> beatmap) private void performTest(List<ReplayFrame> frames, Beatmap<OsuHitObject> beatmap)
{ {
beatmap.BeatmapInfo.Ruleset = new OsuRuleset().RulesetInfo; beatmap.BeatmapInfo.Ruleset = new OsuRuleset().RulesetInfo;
beatmap.BeatmapInfo.StackLeniency = 0; beatmap.StackLeniency = 0;
beatmap.BeatmapInfo.Difficulty = new BeatmapDifficulty beatmap.BeatmapInfo.Difficulty = new BeatmapDifficulty
{ {
SliderMultiplier = 4, SliderMultiplier = 4,

View File

@ -56,13 +56,13 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
h.StackHeight = 0; h.StackHeight = 0;
if (beatmap.BeatmapInfo.BeatmapVersion >= 6) if (beatmap.BeatmapInfo.BeatmapVersion >= 6)
applyStacking(beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1); applyStacking(beatmap, hitObjects, 0, hitObjects.Count - 1);
else else
applyStackingOld(beatmap.BeatmapInfo, hitObjects); applyStackingOld(beatmap, hitObjects);
} }
} }
private static void applyStacking(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects, int startIndex, int endIndex) private static void applyStacking(IBeatmap beatmap, List<OsuHitObject> hitObjects, int startIndex, int endIndex)
{ {
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex); ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
ArgumentOutOfRangeException.ThrowIfNegative(startIndex); ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
continue; continue;
double endTime = stackBaseObject.GetEndTime(); double endTime = stackBaseObject.GetEndTime();
double stackThreshold = objectN.TimePreempt * beatmapInfo.StackLeniency; double stackThreshold = objectN.TimePreempt * beatmap.StackLeniency;
if (objectN.StartTime - endTime > stackThreshold) if (objectN.StartTime - endTime > stackThreshold)
// We are no longer within stacking range of the next object. // We are no longer within stacking range of the next object.
@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
OsuHitObject objectI = hitObjects[i]; OsuHitObject objectI = hitObjects[i];
if (objectI.StackHeight != 0 || objectI is Spinner) continue; if (objectI.StackHeight != 0 || objectI is Spinner) continue;
double stackThreshold = objectI.TimePreempt * beatmapInfo.StackLeniency; double stackThreshold = objectI.TimePreempt * beatmap.StackLeniency;
/* If this object is a hitcircle, then we enter this "special" case. /* If this object is a hitcircle, then we enter this "special" case.
* It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider. * It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider.
@ -214,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
} }
} }
private static void applyStackingOld(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects) private static void applyStackingOld(IBeatmap beatmap, List<OsuHitObject> hitObjects)
{ {
for (int i = 0; i < hitObjects.Count; i++) for (int i = 0; i < hitObjects.Count; i++)
{ {
@ -228,7 +228,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
for (int j = i + 1; j < hitObjects.Count; j++) for (int j = i + 1; j < hitObjects.Count; j++)
{ {
double stackThreshold = hitObjects[i].TimePreempt * beatmapInfo.StackLeniency; double stackThreshold = hitObjects[i].TimePreempt * beatmap.StackLeniency;
if (hitObjects[j].StartTime - stackThreshold > startTime) if (hitObjects[j].StartTime - stackThreshold > startTime)
break; break;

View File

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

View File

@ -21,9 +21,11 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; 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;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Changes;
using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -50,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private EditorBeatmap? editorBeatmap { get; set; } private EditorBeatmap? editorBeatmap { get; set; }
[Resolved] [Resolved]
private IEditorChangeHandler? changeHandler { get; set; } private NewBeatmapEditorChangeHandler? changeHandler { get; set; }
[Resolved] [Resolved]
private BindableBeatDivisor? beatDivisor { get; set; } private BindableBeatDivisor? beatDivisor { get; set; }
@ -122,6 +124,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
pathVersion.BindValueChanged(_ => editorBeatmap?.Update(HitObject)); pathVersion.BindValueChanged(_ => editorBeatmap?.Update(HitObject));
BodyPiece.UpdateFrom(HitObject); BodyPiece.UpdateFrom(HitObject);
HitObject.DefaultsApplied += _ => BodyPiece.UpdateFrom(HitObject);
if (editorBeatmap != null) if (editorBeatmap != null)
selectedObjects.BindTo(editorBeatmap.SelectedHitObjects); 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)) if (Precision.AlmostEquals(proposedDistance, HitObject.Path.Distance) && Precision.AlmostEquals(proposedVelocity, HitObject.SliderVelocityMultiplier))
return; return;
HitObject.SliderVelocityMultiplier = proposedVelocity; new SliderVelocityMultiplierChange(HitObject, proposedVelocity).Apply(changeHandler);
HitObject.Path.ExpectedDistance.Value = proposedDistance; new ExpectedDistanceChange(HitObject.Path, proposedDistance).Apply(changeHandler);
editorBeatmap?.Update(HitObject); editorBeatmap?.Update(HitObject);
changeHandler?.RecordUpdate(HitObject);
} }
/// <summary> /// <summary>
@ -303,8 +307,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
if (Precision.AlmostBigger(segmentEnds[segmentIndex], 1, 1E-3)) if (Precision.AlmostBigger(segmentEnds[segmentIndex], 1, 1E-3))
{ {
sliderPath.ControlPoints.RemoveRange(i + 1, sliderPath.ControlPoints.Count - i - 1); new RemoveRangePathControlPointChange(sliderPath.ControlPoints, i + 1, sliderPath.ControlPoints.Count - i - 1).Apply(changeHandler);
sliderPath.ControlPoints[^1].Type = null; new PathControlPointTypeChange(sliderPath.ControlPoints[^1], null).Apply(changeHandler);
break; break;
} }
@ -442,11 +446,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
var pathControlPoint = new PathControlPoint { Position = position }; var pathControlPoint = new PathControlPoint { Position = position };
// Move the control points from the insertion index onwards to make room for the insertion // 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(); ControlPointVisualiser?.EnsureValidPathTypes();
HitObject.SnapTo(distanceSnapProvider); new SnapToChange<Slider>(HitObject, distanceSnapProvider).Apply(changeHandler);
return pathControlPoint; 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 // 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 // Todo: Should be handled within SliderPath itself
if (c == controlPoints[0] && controlPoints.Count > 1 && controlPoints[1].Type == null) 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(); ControlPointVisualiser?.EnsureValidPathTypes();
// Snap the slider to the current beat divisor before checking length validity. // 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 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) 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) // 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; Vector2 first = controlPoints[0].Position;
foreach (var c in controlPoints) foreach (var c in controlPoints)
c.Position -= first; new PathControlPointPositionChange(c, c.Position - first).Apply(changeHandler);
HitObject.Position += first;
new PositionChange(HitObject, HitObject.Position + first).Apply(changeHandler);
} }
private void splitControlPoints(List<PathControlPoint> controlPointsToSplitAt) 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. // Extract the split portion and remove from the original slider.
var splitControlPoints = controlPoints.Take(index + 1).ToList(); var splitControlPoints = controlPoints.Take(index + 1).ToList();
controlPoints.RemoveRange(0, index); new RemoveRangePathControlPointChange(HitObject.Path.ControlPoints, 0, index).Apply(changeHandler);
var newSlider = new Slider 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. // 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; new NewComboChange(HitObject, false).Apply(changeHandler);
HitObject.Path.ExpectedDistance.Value -= newSlider.Path.CalculatedDistance; new ExpectedDistanceChange(HitObject.Path, HitObject.Path.ExpectedDistance.Value - newSlider.Path.CalculatedDistance).Apply(changeHandler);
HitObject.StartTime += newSlider.SpanDuration; 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. // 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) 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). // As a final step, we must reset its control points to have an origin of (0,0).
Vector2 first = controlPoints[0].Position; Vector2 first = controlPoints[0].Position;
foreach (var c in controlPoints) foreach (var c in controlPoints)
c.Position -= first; new PathControlPointPositionChange(c, c.Position - first).Apply(changeHandler);
HitObject.Position += first;
new PositionChange(HitObject, HitObject.Position + first).Apply(changeHandler);
} }
private void convertToStream() private void convertToStream()
@ -576,19 +582,19 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
Vector2 position = HitObject.Position + HitObject.Path.PositionAt(pathPosition); Vector2 position = HitObject.Position + HitObject.Path.PositionAt(pathPosition);
editorBeatmap.Add(new HitCircle new AddHitObjectChange(editorBeatmap, new HitCircle
{ {
StartTime = time, StartTime = time,
Position = position, Position = position,
NewCombo = i == 0 && HitObject.NewCombo, NewCombo = i == 0 && HitObject.NewCombo,
Samples = HitObject.HeadCircle.Samples.Select(s => s.With()).ToList() Samples = HitObject.HeadCircle.Samples.Select(s => s.With()).ToList()
}); }).Apply(changeHandler);
i += 1; i += 1;
time = HitObject.StartTime + i * streamSpacing; time = HitObject.StartTime + i * streamSpacing;
} }
editorBeatmap.Remove(HitObject); new RemoveHitObjectChange(editorBeatmap, HitObject).Apply(changeHandler);
changeHandler?.EndChange(); 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

@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Edit
}, },
}; };
Spacing.Value = editorBeatmap.BeatmapInfo.GridSize; Spacing.Value = editorBeatmap.GridSize;
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -204,7 +204,7 @@ namespace osu.Game.Rulesets.Osu.Edit
spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:#,0.##}"; spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:#,0.##}";
spacingSlider.ExpandedLabelText = $"Spacing: {spacing.NewValue:#,0.##}"; spacingSlider.ExpandedLabelText = $"Spacing: {spacing.NewValue:#,0.##}";
SpacingVector.Value = new Vector2(spacing.NewValue); SpacingVector.Value = new Vector2(spacing.NewValue);
editorBeatmap.BeatmapInfo.GridSize = (int)spacing.NewValue; editorBeatmap.GridSize = (int)spacing.NewValue;
}, true); }, true);
GridLinesRotation.BindValueChanged(rotation => GridLinesRotation.BindValueChanged(rotation =>

View File

@ -15,8 +15,11 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Edit.Changes;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI; 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.Screens.Edit.Compose.Components;
using osu.Game.Utils; using osu.Game.Utils;
using osuTK; using osuTK;
@ -51,6 +54,9 @@ namespace osu.Game.Rulesets.Osu.Edit
return false; return false;
} }
[Resolved(canBeNull: true)]
private NewBeatmapEditorChangeHandler? changeHandler { get; set; }
public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent) public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent)
{ {
var hitObjects = selectedMovableObjects; var hitObjects = selectedMovableObjects;
@ -72,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Edit
// this will potentially move the selection out of bounds... // this will potentially move the selection out of bounds...
foreach (var h in hitObjects) foreach (var h in hitObjects)
h.Position += localDelta; new PositionChange(h, h.Position + localDelta).Apply(changeHandler);
// but this will be corrected. // but this will be corrected.
moveSelectionInBounds(); moveSelectionInBounds();
@ -105,12 +111,13 @@ namespace osu.Game.Rulesets.Osu.Edit
foreach (var h in hitObjects) foreach (var h in hitObjects)
{ {
if (moreThanOneObject) if (moreThanOneObject)
h.StartTime = endTime - (h.GetEndTime() - startTime); new StartTimeChange(h, endTime - (h.GetEndTime() - startTime)).Apply(changeHandler);
if (h is Slider slider) if (h is Slider slider)
{ {
slider.Path.Reverse(out Vector2 offset); var reverse = new ReverseSliderPathChange(slider.Path);
slider.Position += offset; 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(); hitObjects = hitObjects.OrderBy(obj => obj.StartTime).ToList();
for (int i = 0; i < hitObjects.Count; ++i) for (int i = 0; i < hitObjects.Count; ++i)
hitObjects[i].NewCombo = newComboOrder[i]; new NewComboChange(hitObjects[i], newComboOrder[i]).Apply(changeHandler);
return true; return true;
} }
@ -167,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Edit
if (!Precision.AlmostEquals(flippedPosition, h.Position)) if (!Precision.AlmostEquals(flippedPosition, h.Position))
{ {
h.Position = flippedPosition; new PositionChange(h, flippedPosition).Apply(changeHandler);
didFlip = true; didFlip = true;
} }
@ -176,7 +183,7 @@ namespace osu.Game.Rulesets.Osu.Edit
didFlip = true; didFlip = true;
foreach (var cp in slider.Path.ControlPoints) 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; delta.Y -= quad.BottomRight.Y - DrawHeight;
foreach (var h in hitObjects) foreach (var h in hitObjects)
h.Position += delta; new PositionChange(h, h.Position + delta).Apply(changeHandler);
} }
/// <summary> /// <summary>
@ -245,7 +252,7 @@ namespace osu.Game.Rulesets.Osu.Edit
if (mergedHitObject.Path.ControlPoints.Count == 0) 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. // 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); float distanceToLastControlPoint = Vector2.Distance(mergedHitObject.Path.ControlPoints[^1].Position, offset);
// Calculate the distance required to travel to the expected distance of the merging slider. // 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. // Remove the last control point if it sits exactly on the start of the next control point.
if (Precision.AlmostEquals(distanceToLastControlPoint, 0)) 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; lastCircle = false;
} }
else 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. // 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) 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)); new InsertPathControlPointChange(mergedHitObject.Path.ControlPoints, mergedHitObject.Path.ControlPoints.Count, new PathControlPoint(selectedMergeableObject.Position - mergedHitObject.Position)).Apply(changeHandler);
mergedHitObject.Path.ExpectedDistance.Value = null; new ExpectedDistanceChange(mergedHitObject.Path, null).Apply(changeHandler);
lastCircle = true; lastCircle = true;
} }
} }
@ -289,17 +296,17 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
foreach (var selectedMergeableObject in mergeableObjects.Skip(1)) foreach (var selectedMergeableObject in mergeableObjects.Skip(1))
{ {
EditorBeatmap.Remove(selectedMergeableObject); new RemoveHitObjectChange(EditorBeatmap, selectedMergeableObject).Apply(changeHandler);
} }
} }
else else
{ {
foreach (var selectedMergeableObject in mergeableObjects) 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. // Make sure the merged hitobject is selected.

View File

@ -9,8 +9,10 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Changes;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Changes;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Utils; using osu.Game.Utils;
using osuTK; using osuTK;
@ -20,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Edit
public partial class OsuSelectionRotationHandler : SelectionRotationHandler public partial class OsuSelectionRotationHandler : SelectionRotationHandler
{ {
[Resolved] [Resolved]
private IEditorChangeHandler? changeHandler { get; set; } private NewBeatmapEditorChangeHandler? changeHandler { get; set; }
private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>(); private BindableList<HitObject> selectedItems { get; } = new BindableList<HitObject>();
@ -78,14 +80,17 @@ namespace osu.Game.Rulesets.Osu.Edit
foreach (var ho in objectsInRotation) 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) if (ho is IHasPath withPath)
{ {
var originalPath = originalPathControlPointPositions[withPath]; var originalPath = originalPathControlPointPositions[withPath];
for (int i = 0; i < withPath.Path.ControlPoints.Count; ++i) 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.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Changes;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Changes;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Utils; using osu.Game.Utils;
using osuTK; using osuTK;
@ -35,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Edit
public Bindable<bool> IsScalingSlider { get; private set; } = new BindableBool(); public Bindable<bool> IsScalingSlider { get; private set; } = new BindableBool();
[Resolved] [Resolved]
private IEditorChangeHandler? changeHandler { get; set; } private NewBeatmapEditorChangeHandler? changeHandler { get; set; }
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IDistanceSnapProvider? snapProvider { get; set; } private IDistanceSnapProvider? snapProvider { get; set; }
@ -113,7 +115,7 @@ namespace osu.Game.Rulesets.Osu.Edit
foreach (var (ho, originalState) in objectsInScale) 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 // 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++) for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
{ {
slider.Path.ControlPoints[i].Position = GeometryUtils.GetScaledPosition(scale, Vector2.Zero, originalInfo.PathControlPointPositions[i], axisRotation); new PathControlPointPositionChange(slider.Path.ControlPoints[i],
slider.Path.ControlPoints[i].Type = originalInfo.PathControlPointTypes[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 // Snap the slider's length to the current beat divisor
// to calculate the final resulting duration / bounding box before the final checks. // 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. //if sliderhead or sliderend end up outside playfield, revert scaling.
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider }); Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
@ -184,12 +187,14 @@ namespace osu.Game.Rulesets.Osu.Edit
return; return;
for (int i = 0; i < slider.Path.ControlPoints.Count; i++) 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. // 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) 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; delta.Y -= quad.BottomRight.Y - OsuPlayfield.BASE_SIZE.Y;
foreach (var (h, _) in objectsInScale!) foreach (var (h, _) in objectsInScale!)
h.Position += delta; new PositionChange(h, h.Position + delta).Apply(changeHandler);
} }
private struct OriginalHitObjectState private struct OriginalHitObjectState

View File

@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
{ {
Caption = "Stack Leniency", Caption = "Stack Leniency",
HintText = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.", HintText = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.",
Current = new BindableFloat(Beatmap.BeatmapInfo.StackLeniency) Current = new BindableFloat(Beatmap.StackLeniency)
{ {
Default = 0.7f, Default = 0.7f,
MinValue = 0, MinValue = 0,
@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value; Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value; Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;
Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value; Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value;
Beatmap.BeatmapInfo.StackLeniency = stackLeniency.Current.Value; Beatmap.StackLeniency = stackLeniency.Current.Value;
Beatmap.UpdateAllHitObjects(); Beatmap.UpdateAllHitObjects();
Beatmap.SaveState(); Beatmap.SaveState();

View File

@ -80,16 +80,16 @@ namespace osu.Game.Tests.Beatmaps.Formats
var metadata = beatmap.Metadata; var metadata = beatmap.Metadata;
Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", metadata.AudioFile); Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", metadata.AudioFile);
Assert.AreEqual(0, beatmapInfo.AudioLeadIn); Assert.AreEqual(0, beatmap.AudioLeadIn);
Assert.AreEqual(164471, metadata.PreviewTime); Assert.AreEqual(164471, metadata.PreviewTime);
Assert.AreEqual(0.7f, beatmapInfo.StackLeniency); Assert.AreEqual(0.7f, beatmap.StackLeniency);
Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0); Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0);
Assert.IsFalse(beatmapInfo.LetterboxInBreaks); Assert.IsFalse(beatmap.LetterboxInBreaks);
Assert.IsFalse(beatmapInfo.SpecialStyle); Assert.IsFalse(beatmap.SpecialStyle);
Assert.IsFalse(beatmapInfo.WidescreenStoryboard); Assert.IsFalse(beatmap.WidescreenStoryboard);
Assert.IsFalse(beatmapInfo.SamplesMatchPlaybackRate); Assert.IsFalse(beatmap.SamplesMatchPlaybackRate);
Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown); Assert.AreEqual(CountdownType.None, beatmap.Countdown);
Assert.AreEqual(0, beatmapInfo.CountdownOffset); Assert.AreEqual(0, beatmap.CountdownOffset);
} }
} }
@ -101,7 +101,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))
{ {
var beatmapInfo = decoder.Decode(stream).BeatmapInfo; var beatmap = decoder.Decode(stream);
int[] expectedBookmarks = int[] expectedBookmarks =
{ {
@ -109,13 +109,13 @@ namespace osu.Game.Tests.Beatmaps.Formats
95901, 106450, 116999, 119637, 130186, 140735, 151285, 95901, 106450, 116999, 119637, 130186, 140735, 151285,
161834, 164471, 175020, 185570, 196119, 206669, 209306 161834, 164471, 175020, 185570, 196119, 206669, 209306
}; };
Assert.AreEqual(expectedBookmarks.Length, beatmapInfo.Bookmarks.Length); Assert.AreEqual(expectedBookmarks.Length, beatmap.BeatmapInfo.Bookmarks.Length);
for (int i = 0; i < expectedBookmarks.Length; i++) for (int i = 0; i < expectedBookmarks.Length; i++)
Assert.AreEqual(expectedBookmarks[i], beatmapInfo.Bookmarks[i]); Assert.AreEqual(expectedBookmarks[i], beatmap.BeatmapInfo.Bookmarks[i]);
Assert.AreEqual(1.8, beatmapInfo.DistanceSpacing); Assert.AreEqual(1.8, beatmap.DistanceSpacing);
Assert.AreEqual(4, beatmapInfo.BeatDivisor); Assert.AreEqual(4, beatmap.BeatmapInfo.BeatDivisor);
Assert.AreEqual(4, beatmapInfo.GridSize); Assert.AreEqual(4, beatmap.GridSize);
Assert.AreEqual(2, beatmapInfo.TimelineZoom); Assert.AreEqual(2, beatmap.TimelineZoom);
} }
} }
@ -993,15 +993,15 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.Multiple(() => Assert.Multiple(() =>
{ {
Assert.That(decoded.BeatmapInfo.AudioLeadIn, Is.EqualTo(0)); Assert.That(decoded.AudioLeadIn, Is.EqualTo(0));
Assert.That(decoded.BeatmapInfo.StackLeniency, Is.EqualTo(0.7f)); Assert.That(decoded.StackLeniency, Is.EqualTo(0.7f));
Assert.That(decoded.BeatmapInfo.SpecialStyle, Is.False); Assert.That(decoded.SpecialStyle, Is.False);
Assert.That(decoded.BeatmapInfo.LetterboxInBreaks, Is.False); Assert.That(decoded.LetterboxInBreaks, Is.False);
Assert.That(decoded.BeatmapInfo.WidescreenStoryboard, Is.False); Assert.That(decoded.WidescreenStoryboard, Is.False);
Assert.That(decoded.BeatmapInfo.EpilepsyWarning, Is.False); Assert.That(decoded.EpilepsyWarning, Is.False);
Assert.That(decoded.BeatmapInfo.SamplesMatchPlaybackRate, Is.False); Assert.That(decoded.SamplesMatchPlaybackRate, Is.False);
Assert.That(decoded.BeatmapInfo.Countdown, Is.EqualTo(CountdownType.None)); Assert.That(decoded.Countdown, Is.EqualTo(CountdownType.None));
Assert.That(decoded.BeatmapInfo.CountdownOffset, Is.EqualTo(0)); Assert.That(decoded.CountdownOffset, Is.EqualTo(0));
Assert.That(decoded.BeatmapInfo.Metadata.PreviewTime, Is.EqualTo(-1)); Assert.That(decoded.BeatmapInfo.Metadata.PreviewTime, Is.EqualTo(-1));
Assert.That(decoded.BeatmapInfo.Ruleset.OnlineID, Is.EqualTo(0)); Assert.That(decoded.BeatmapInfo.Ruleset.OnlineID, Is.EqualTo(0));
}); });

View File

@ -51,14 +51,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
{ {
var beatmap = decodeAsJson(normal); var beatmap = decodeAsJson(normal);
var beatmapInfo = beatmap.BeatmapInfo; var beatmapInfo = beatmap.BeatmapInfo;
Assert.AreEqual(0, beatmapInfo.AudioLeadIn); Assert.AreEqual(0, beatmap.AudioLeadIn);
Assert.AreEqual(0.7f, beatmapInfo.StackLeniency); Assert.AreEqual(0.7f, beatmap.StackLeniency);
Assert.AreEqual(false, beatmapInfo.SpecialStyle); Assert.AreEqual(false, beatmap.SpecialStyle);
Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0); Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0);
Assert.AreEqual(false, beatmapInfo.LetterboxInBreaks); Assert.AreEqual(false, beatmap.LetterboxInBreaks);
Assert.AreEqual(false, beatmapInfo.WidescreenStoryboard); Assert.AreEqual(false, beatmap.WidescreenStoryboard);
Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown); Assert.AreEqual(CountdownType.None, beatmap.Countdown);
Assert.AreEqual(0, beatmapInfo.CountdownOffset); Assert.AreEqual(0, beatmap.CountdownOffset);
} }
[Test] [Test]
@ -76,10 +76,10 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual(expectedBookmarks.Length, beatmapInfo.Bookmarks.Length); Assert.AreEqual(expectedBookmarks.Length, beatmapInfo.Bookmarks.Length);
for (int i = 0; i < expectedBookmarks.Length; i++) for (int i = 0; i < expectedBookmarks.Length; i++)
Assert.AreEqual(expectedBookmarks[i], beatmapInfo.Bookmarks[i]); Assert.AreEqual(expectedBookmarks[i], beatmapInfo.Bookmarks[i]);
Assert.AreEqual(1.8, beatmapInfo.DistanceSpacing); Assert.AreEqual(1.8, beatmap.DistanceSpacing);
Assert.AreEqual(4, beatmapInfo.BeatDivisor); Assert.AreEqual(4, beatmapInfo.BeatDivisor);
Assert.AreEqual(4, beatmapInfo.GridSize); Assert.AreEqual(4, beatmap.GridSize);
Assert.AreEqual(2, beatmapInfo.TimelineZoom); Assert.AreEqual(2, beatmap.TimelineZoom);
} }
[Test] [Test]

View File

@ -41,7 +41,7 @@ namespace osu.Game.Tests.Database
Assert.That(lastChanges?.ModifiedIndices, Is.Empty); Assert.That(lastChanges?.ModifiedIndices, Is.Empty);
Assert.That(lastChanges?.NewModifiedIndices, Is.Empty); Assert.That(lastChanges?.NewModifiedIndices, Is.Empty);
realm.Write(r => r.All<BeatmapSetInfo>().First().Beatmaps.First().CountdownOffset = 5); realm.Write(r => r.All<BeatmapSetInfo>().First().Beatmaps.First().EditorTimestamp = 5);
realm.Run(r => r.Refresh()); realm.Run(r => r.Refresh());
Assert.That(collectionChanges, Is.EqualTo(1)); Assert.That(collectionChanges, Is.EqualTo(1));

View File

@ -56,7 +56,7 @@ namespace osu.Game.Tests.Visual.Editing
{ {
AddStep("turn countdown off", () => designSection.EnableCountdown.Current.Value = false); AddStep("turn countdown off", () => designSection.EnableCountdown.Current.Value = false);
AddAssert("beatmap has correct type", () => editorBeatmap.BeatmapInfo.Countdown == CountdownType.None); AddAssert("beatmap has correct type", () => editorBeatmap.Countdown == CountdownType.None);
AddUntilStep("other controls hidden", () => !designSection.CountdownSettings.IsPresent); AddUntilStep("other controls hidden", () => !designSection.CountdownSettings.IsPresent);
} }
@ -65,12 +65,12 @@ namespace osu.Game.Tests.Visual.Editing
{ {
AddStep("turn countdown on", () => designSection.EnableCountdown.Current.Value = true); AddStep("turn countdown on", () => designSection.EnableCountdown.Current.Value = true);
AddAssert("beatmap has correct type", () => editorBeatmap.BeatmapInfo.Countdown == CountdownType.Normal); AddAssert("beatmap has correct type", () => editorBeatmap.Countdown == CountdownType.Normal);
AddUntilStep("other controls shown", () => designSection.CountdownSettings.IsPresent); AddUntilStep("other controls shown", () => designSection.CountdownSettings.IsPresent);
AddStep("change countdown speed", () => designSection.CountdownSpeed.Current.Value = CountdownType.DoubleSpeed); AddStep("change countdown speed", () => designSection.CountdownSpeed.Current.Value = CountdownType.DoubleSpeed);
AddAssert("beatmap has correct type", () => editorBeatmap.BeatmapInfo.Countdown == CountdownType.DoubleSpeed); AddAssert("beatmap has correct type", () => editorBeatmap.Countdown == CountdownType.DoubleSpeed);
AddUntilStep("other controls still shown", () => designSection.CountdownSettings.IsPresent); AddUntilStep("other controls still shown", () => designSection.CountdownSettings.IsPresent);
} }
@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Editing
{ {
AddStep("turn countdown on", () => designSection.EnableCountdown.Current.Value = true); AddStep("turn countdown on", () => designSection.EnableCountdown.Current.Value = true);
AddAssert("beatmap has correct type", () => editorBeatmap.BeatmapInfo.Countdown == CountdownType.Normal); AddAssert("beatmap has correct type", () => editorBeatmap.Countdown == CountdownType.Normal);
checkOffsetAfter("1", 1); checkOffsetAfter("1", 1);
checkOffsetAfter(string.Empty, 0); checkOffsetAfter(string.Empty, 0);
@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("commit text", () => InputManager.Key(Key.Enter)); AddStep("commit text", () => InputManager.Key(Key.Enter));
AddAssert($"displayed value is {expectedFinalValue}", () => designSection.CountdownOffset.Current.Value == expectedFinalValue.ToString(CultureInfo.InvariantCulture)); AddAssert($"displayed value is {expectedFinalValue}", () => designSection.CountdownOffset.Current.Value == expectedFinalValue.ToString(CultureInfo.InvariantCulture));
AddAssert($"beatmap value is {expectedFinalValue}", () => editorBeatmap.BeatmapInfo.CountdownOffset == expectedFinalValue); AddAssert($"beatmap value is {expectedFinalValue}", () => editorBeatmap.CountdownOffset == expectedFinalValue);
} }
private partial class TestDesignSection : DesignSection private partial class TestDesignSection : DesignSection

View File

@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Set beat divisor", () => Editor.Dependencies.Get<BindableBeatDivisor>().Value = 16); AddStep("Set beat divisor", () => Editor.Dependencies.Get<BindableBeatDivisor>().Value = 16);
AddStep("Set timeline zoom", () => AddStep("Set timeline zoom", () =>
{ {
originalTimelineZoom = EditorBeatmap.BeatmapInfo.TimelineZoom; originalTimelineZoom = EditorBeatmap.TimelineZoom;
var timeline = Editor.ChildrenOfType<Timeline>().Single(); var timeline = Editor.ChildrenOfType<Timeline>().Single();
InputManager.MoveMouseTo(timeline); InputManager.MoveMouseTo(timeline);
@ -81,19 +81,19 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("Ensure timeline zoom changed", () => AddAssert("Ensure timeline zoom changed", () =>
{ {
changedTimelineZoom = EditorBeatmap.BeatmapInfo.TimelineZoom; changedTimelineZoom = EditorBeatmap.TimelineZoom;
return !Precision.AlmostEquals(changedTimelineZoom, originalTimelineZoom); return !Precision.AlmostEquals(changedTimelineZoom, originalTimelineZoom);
}); });
SaveEditor(); SaveEditor();
AddAssert("Beatmap has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor == 16); AddAssert("Beatmap has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor == 16);
AddAssert("Beatmap has correct timeline zoom", () => EditorBeatmap.BeatmapInfo.TimelineZoom == changedTimelineZoom); AddAssert("Beatmap has correct timeline zoom", () => EditorBeatmap.TimelineZoom == changedTimelineZoom);
ReloadEditorToSameBeatmap(); ReloadEditorToSameBeatmap();
AddAssert("Beatmap still has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor == 16); AddAssert("Beatmap still has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor == 16);
AddAssert("Beatmap still has correct timeline zoom", () => EditorBeatmap.BeatmapInfo.TimelineZoom == changedTimelineZoom); AddAssert("Beatmap still has correct timeline zoom", () => EditorBeatmap.TimelineZoom == changedTimelineZoom);
} }
[Test] [Test]

View File

@ -205,7 +205,7 @@ namespace osu.Game.Tests.Visual.Editing
{ {
double originalSpacing = 0; double originalSpacing = 0;
AddStep("retrieve original spacing", () => originalSpacing = editorBeatmap.BeatmapInfo.DistanceSpacing); AddStep("retrieve original spacing", () => originalSpacing = editorBeatmap.DistanceSpacing);
AddStep("hold ctrl", () => InputManager.PressKey(Key.LControl)); AddStep("hold ctrl", () => InputManager.PressKey(Key.LControl));
AddStep("hold alt", () => InputManager.PressKey(Key.LAlt)); AddStep("hold alt", () => InputManager.PressKey(Key.LAlt));
@ -215,7 +215,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("release alt", () => InputManager.ReleaseKey(Key.LAlt)); AddStep("release alt", () => InputManager.ReleaseKey(Key.LAlt));
AddStep("release ctrl", () => InputManager.ReleaseKey(Key.LControl)); AddStep("release ctrl", () => InputManager.ReleaseKey(Key.LControl));
AddAssert("distance spacing increased by 0.5", () => editorBeatmap.BeatmapInfo.DistanceSpacing == originalSpacing + 0.5); AddAssert("distance spacing increased by 0.5", () => editorBeatmap.DistanceSpacing == originalSpacing + 0.5);
} }
public partial class EditorBeatmapContainer : PopoverContainer public partial class EditorBeatmapContainer : PopoverContainer

View File

@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo) loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo)
{ {
BeatmapInfo = { AudioLeadIn = leadIn } AudioLeadIn = leadIn
}); });
checkFirstFrameTime(expectedStartTime); checkFirstFrameTime(expectedStartTime);
@ -133,7 +133,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
Text = $"GameplayStartTime: {DrawableRuleset.GameplayStartTime} " Text = $"GameplayStartTime: {DrawableRuleset.GameplayStartTime} "
+ $"FirstHitObjectTime: {FirstHitObjectTime} " + $"FirstHitObjectTime: {FirstHitObjectTime} "
+ $"LeadInTime: {Beatmap.Value.BeatmapInfo.AudioLeadIn} " + $"LeadInTime: {Beatmap.Value.Beatmap.AudioLeadIn} "
+ $"FirstFrameClockTime: {FirstFrameClockTime}" + $"FirstFrameClockTime: {FirstFrameClockTime}"
}); });
} }

View File

@ -136,10 +136,10 @@ namespace osu.Game.Tests.Visual.Gameplay
var workingBeatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); var workingBeatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
// Add intro time to test quick retry skipping (TestQuickRetry). // Add intro time to test quick retry skipping (TestQuickRetry).
workingBeatmap.BeatmapInfo.AudioLeadIn = 60000; workingBeatmap.Beatmap.AudioLeadIn = 60000;
// Set up data for testing disclaimer display. // Set up data for testing disclaimer display.
workingBeatmap.BeatmapInfo.EpilepsyWarning = epilepsyWarning ?? false; workingBeatmap.Beatmap.EpilepsyWarning = epilepsyWarning ?? false;
workingBeatmap.BeatmapInfo.Status = onlineStatus ?? BeatmapOnlineStatus.Ranked; workingBeatmap.BeatmapInfo.Status = onlineStatus ?? BeatmapOnlineStatus.Ranked;
Beatmap.Value = workingBeatmap; Beatmap.Value = workingBeatmap;

View File

@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo) loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo)
{ {
BeatmapInfo = { AudioLeadIn = 60000 } AudioLeadIn = 60000
}); });
AddUntilStep("wait for skip overlay", () => Player.ChildrenOfType<SkipOverlay>().First().IsButtonVisible); AddUntilStep("wait for skip overlay", () => Player.ChildrenOfType<SkipOverlay>().First().IsButtonVisible);

View File

@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("load storyboard with only video", () => AddStep("load storyboard with only video", () =>
{ {
// LegacyStoryboardDecoder doesn't parse WidescreenStoryboard, so it is set manually // LegacyStoryboardDecoder doesn't parse WidescreenStoryboard, so it is set manually
loadStoryboard("storyboard_only_video.osu", s => s.BeatmapInfo.WidescreenStoryboard = false); loadStoryboard("storyboard_only_video.osu", s => s.Beatmap.WidescreenStoryboard = false);
}); });
AddAssert("storyboard is correct width", () => Precision.AlmostEquals(storyboard?.Width ?? 0f, 480 * 16 / 9f)); AddAssert("storyboard is correct width", () => Precision.AlmostEquals(storyboard?.Width ?? 0f, 480 * 16 / 9f));

View File

@ -406,13 +406,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
/// <summary> /// <summary>
/// Tests spectating with a beatmap that has a high <see cref="BeatmapInfo.AudioLeadIn"/> value. /// Tests spectating with a beatmap that has a high <see cref="IBeatmap.AudioLeadIn"/> value.
/// ///
/// This test is not intended not to check the correct initial time value, but only to guard against /// This test is not intended not to check the correct initial time value, but only to guard against
/// gameplay potentially getting stuck in a stopped state due to lead in time being present. /// gameplay potentially getting stuck in a stopped state due to lead in time being present.
/// </summary> /// </summary>
[Test] [Test]
public void TestAudioLeadIn() => testLeadIn(b => b.BeatmapInfo.AudioLeadIn = 2000); public void TestAudioLeadIn() => testLeadIn(b => b.Beatmap.AudioLeadIn = 2000);
/// <summary> /// <summary>
/// Tests spectating with a beatmap that has a storyboard element with a negative start time (i.e. intro storyboard element). /// Tests spectating with a beatmap that has a storyboard element with a negative start time (i.e. intro storyboard element).

View File

@ -115,6 +115,30 @@ namespace osu.Game.Beatmaps
return mostCommon.beatLength; return mostCommon.beatLength;
} }
public double AudioLeadIn { get; set; }
public float StackLeniency { get; set; } = 0.7f;
public bool SpecialStyle { get; set; }
public bool LetterboxInBreaks { get; set; }
public bool WidescreenStoryboard { get; set; } = true;
public bool EpilepsyWarning { get; set; }
public bool SamplesMatchPlaybackRate { get; set; }
public double DistanceSpacing { get; set; } = 1.0;
public int GridSize { get; set; }
public double TimelineZoom { get; set; } = 1.0;
public CountdownType Countdown { get; set; } = CountdownType.None;
public int CountdownOffset { get; set; }
IBeatmap IBeatmap.Clone() => Clone(); IBeatmap IBeatmap.Clone() => Clone();
public Beatmap<T> Clone() => (Beatmap<T>)MemberwiseClone(); public Beatmap<T> Clone() => (Beatmap<T>)MemberwiseClone();

View File

@ -73,6 +73,18 @@ namespace osu.Game.Beatmaps
beatmap.HitObjects = convertHitObjects(original.HitObjects, original, cancellationToken).OrderBy(s => s.StartTime).ToList(); beatmap.HitObjects = convertHitObjects(original.HitObjects, original, cancellationToken).OrderBy(s => s.StartTime).ToList();
beatmap.Breaks = original.Breaks; beatmap.Breaks = original.Breaks;
beatmap.UnhandledEventLines = original.UnhandledEventLines; beatmap.UnhandledEventLines = original.UnhandledEventLines;
beatmap.AudioLeadIn = original.AudioLeadIn;
beatmap.StackLeniency = original.StackLeniency;
beatmap.SpecialStyle = original.SpecialStyle;
beatmap.LetterboxInBreaks = original.LetterboxInBreaks;
beatmap.WidescreenStoryboard = original.WidescreenStoryboard;
beatmap.EpilepsyWarning = original.EpilepsyWarning;
beatmap.SamplesMatchPlaybackRate = original.SamplesMatchPlaybackRate;
beatmap.DistanceSpacing = original.DistanceSpacing;
beatmap.GridSize = original.GridSize;
beatmap.TimelineZoom = original.TimelineZoom;
beatmap.Countdown = original.Countdown;
beatmap.CountdownOffset = original.CountdownOffset;
return beatmap; return beatmap;
} }

View File

@ -428,17 +428,7 @@ namespace osu.Game.Beatmaps
Hash = hash, Hash = hash,
DifficultyName = decodedInfo.DifficultyName, DifficultyName = decodedInfo.DifficultyName,
OnlineID = decodedInfo.OnlineID, OnlineID = decodedInfo.OnlineID,
AudioLeadIn = decodedInfo.AudioLeadIn,
StackLeniency = decodedInfo.StackLeniency,
SpecialStyle = decodedInfo.SpecialStyle,
LetterboxInBreaks = decodedInfo.LetterboxInBreaks,
WidescreenStoryboard = decodedInfo.WidescreenStoryboard,
EpilepsyWarning = decodedInfo.EpilepsyWarning,
SamplesMatchPlaybackRate = decodedInfo.SamplesMatchPlaybackRate,
DistanceSpacing = decodedInfo.DistanceSpacing,
BeatDivisor = decodedInfo.BeatDivisor, BeatDivisor = decodedInfo.BeatDivisor,
GridSize = decodedInfo.GridSize,
TimelineZoom = decodedInfo.TimelineZoom,
MD5Hash = memoryStream.ComputeMD5Hash(), MD5Hash = memoryStream.ComputeMD5Hash(),
EndTimeObjectCount = decoded.HitObjects.Count(h => h is IHasDuration), EndTimeObjectCount = decoded.HitObjects.Count(h => h is IHasDuration),
TotalObjectCount = decoded.HitObjects.Count TotalObjectCount = decoded.HitObjects.Count

View File

@ -6,14 +6,12 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Collections; using osu.Game.Collections;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Models; using osu.Game.Models;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapSet.Scores; using osu.Game.Overlays.BeatmapSet.Scores;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Scoring; using osu.Game.Scoring;
using Realms; using Realms;
@ -136,60 +134,18 @@ namespace osu.Game.Beatmaps
Status = BeatmapOnlineStatus.None; Status = BeatmapOnlineStatus.None;
} }
#region Properties we may not want persisted (but also maybe no harm?)
public double AudioLeadIn { get; set; }
public float StackLeniency { get; set; } = 0.7f;
public bool SpecialStyle { get; set; }
public bool LetterboxInBreaks { get; set; }
public bool WidescreenStoryboard { get; set; } = true;
public bool EpilepsyWarning { get; set; }
public bool SamplesMatchPlaybackRate { get; set; }
/// <summary> /// <summary>
/// The time at which this beatmap was last played by the local user. /// The time at which this beatmap was last played by the local user.
/// </summary> /// </summary>
public DateTimeOffset? LastPlayed { get; set; } public DateTimeOffset? LastPlayed { get; set; }
/// <summary>
/// The ratio of distance travelled per time unit.
/// Generally used to decouple the spacing between hit objects from the enforced "velocity" of the beatmap (see <see cref="DifficultyControlPoint.SliderVelocity"/>).
/// </summary>
/// <remarks>
/// The most common method of understanding is that at a default value of 1.0, the time-to-distance ratio will match the slider velocity of the beatmap
/// at the current point in time. Increasing this value will make hit objects more spaced apart when compared to the cursor movement required to track a slider.
///
/// This is only a hint property, used by the editor in <see cref="IDistanceSnapProvider"/> implementations. It does not directly affect the beatmap or gameplay.
/// </remarks>
public double DistanceSpacing { get; set; } = 1.0;
public int BeatDivisor { get; set; } = 4; public int BeatDivisor { get; set; } = 4;
public int GridSize { get; set; }
public double TimelineZoom { get; set; } = 1.0;
/// <summary> /// <summary>
/// The time in milliseconds when last exiting the editor with this beatmap loaded. /// The time in milliseconds when last exiting the editor with this beatmap loaded.
/// </summary> /// </summary>
public double? EditorTimestamp { get; set; } public double? EditorTimestamp { get; set; }
[Ignored]
public CountdownType Countdown { get; set; } = CountdownType.None;
/// <summary>
/// The number of beats to move the countdown backwards (compared to its default location).
/// </summary>
public int CountdownOffset { get; set; }
#endregion
public bool Equals(BeatmapInfo? other) public bool Equals(BeatmapInfo? other)
{ {
if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(this, other)) return true;

View File

@ -82,7 +82,7 @@ namespace osu.Game.Beatmaps.Formats
this.beatmap.BeatmapInfo.BeatmapVersion = FormatVersion; this.beatmap.BeatmapInfo.BeatmapVersion = FormatVersion;
parser = new ConvertHitObjectParser(getOffsetTime(), FormatVersion); parser = new ConvertHitObjectParser(getOffsetTime(), FormatVersion);
applyLegacyDefaults(this.beatmap.BeatmapInfo); ApplyLegacyDefaults(this.beatmap);
base.ParseStreamInto(stream, beatmap); base.ParseStreamInto(stream, beatmap);
@ -190,9 +190,9 @@ namespace osu.Game.Beatmaps.Formats
/// This method's intention is to restore those legacy defaults. /// This method's intention is to restore those legacy defaults.
/// See also: https://osu.ppy.sh/wiki/en/Client/File_formats/Osu_%28file_format%29 /// See also: https://osu.ppy.sh/wiki/en/Client/File_formats/Osu_%28file_format%29
/// </summary> /// </summary>
private static void applyLegacyDefaults(BeatmapInfo beatmapInfo) internal static void ApplyLegacyDefaults(Beatmap beatmap)
{ {
beatmapInfo.WidescreenStoryboard = false; beatmap.WidescreenStoryboard = false;
} }
protected override void ParseLine(Beatmap beatmap, Section section, string line) protected override void ParseLine(Beatmap beatmap, Section section, string line)
@ -244,7 +244,7 @@ namespace osu.Game.Beatmaps.Formats
break; break;
case @"AudioLeadIn": case @"AudioLeadIn":
beatmap.BeatmapInfo.AudioLeadIn = Parsing.ParseInt(pair.Value); beatmap.AudioLeadIn = Parsing.ParseInt(pair.Value);
break; break;
case @"PreviewTime": case @"PreviewTime":
@ -261,7 +261,7 @@ namespace osu.Game.Beatmaps.Formats
break; break;
case @"StackLeniency": case @"StackLeniency":
beatmap.BeatmapInfo.StackLeniency = Parsing.ParseFloat(pair.Value); beatmap.StackLeniency = Parsing.ParseFloat(pair.Value);
break; break;
case @"Mode": case @"Mode":
@ -269,31 +269,31 @@ namespace osu.Game.Beatmaps.Formats
break; break;
case @"LetterboxInBreaks": case @"LetterboxInBreaks":
beatmap.BeatmapInfo.LetterboxInBreaks = Parsing.ParseInt(pair.Value) == 1; beatmap.LetterboxInBreaks = Parsing.ParseInt(pair.Value) == 1;
break; break;
case @"SpecialStyle": case @"SpecialStyle":
beatmap.BeatmapInfo.SpecialStyle = Parsing.ParseInt(pair.Value) == 1; beatmap.SpecialStyle = Parsing.ParseInt(pair.Value) == 1;
break; break;
case @"WidescreenStoryboard": case @"WidescreenStoryboard":
beatmap.BeatmapInfo.WidescreenStoryboard = Parsing.ParseInt(pair.Value) == 1; beatmap.WidescreenStoryboard = Parsing.ParseInt(pair.Value) == 1;
break; break;
case @"EpilepsyWarning": case @"EpilepsyWarning":
beatmap.BeatmapInfo.EpilepsyWarning = Parsing.ParseInt(pair.Value) == 1; beatmap.EpilepsyWarning = Parsing.ParseInt(pair.Value) == 1;
break; break;
case @"SamplesMatchPlaybackRate": case @"SamplesMatchPlaybackRate":
beatmap.BeatmapInfo.SamplesMatchPlaybackRate = Parsing.ParseInt(pair.Value) == 1; beatmap.SamplesMatchPlaybackRate = Parsing.ParseInt(pair.Value) == 1;
break; break;
case @"Countdown": case @"Countdown":
beatmap.BeatmapInfo.Countdown = Enum.Parse<CountdownType>(pair.Value); beatmap.Countdown = Enum.Parse<CountdownType>(pair.Value);
break; break;
case @"CountdownOffset": case @"CountdownOffset":
beatmap.BeatmapInfo.CountdownOffset = Parsing.ParseInt(pair.Value); beatmap.CountdownOffset = Parsing.ParseInt(pair.Value);
break; break;
} }
} }
@ -313,7 +313,7 @@ namespace osu.Game.Beatmaps.Formats
break; break;
case @"DistanceSpacing": case @"DistanceSpacing":
beatmap.BeatmapInfo.DistanceSpacing = Math.Max(0, Parsing.ParseDouble(pair.Value)); beatmap.DistanceSpacing = Math.Max(0, Parsing.ParseDouble(pair.Value));
break; break;
case @"BeatDivisor": case @"BeatDivisor":
@ -321,11 +321,11 @@ namespace osu.Game.Beatmaps.Formats
break; break;
case @"GridSize": case @"GridSize":
beatmap.BeatmapInfo.GridSize = Parsing.ParseInt(pair.Value); beatmap.GridSize = Parsing.ParseInt(pair.Value);
break; break;
case @"TimelineZoom": case @"TimelineZoom":
beatmap.BeatmapInfo.TimelineZoom = Math.Max(0, Parsing.ParseDouble(pair.Value)); beatmap.TimelineZoom = Math.Max(0, Parsing.ParseDouble(pair.Value));
break; break;
} }
} }

View File

@ -79,14 +79,14 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine("[General]"); writer.WriteLine("[General]");
if (!string.IsNullOrEmpty(beatmap.Metadata.AudioFile)) writer.WriteLine(FormattableString.Invariant($"AudioFilename: {Path.GetFileName(beatmap.Metadata.AudioFile)}")); if (!string.IsNullOrEmpty(beatmap.Metadata.AudioFile)) writer.WriteLine(FormattableString.Invariant($"AudioFilename: {Path.GetFileName(beatmap.Metadata.AudioFile)}"));
writer.WriteLine(FormattableString.Invariant($"AudioLeadIn: {beatmap.BeatmapInfo.AudioLeadIn}")); writer.WriteLine(FormattableString.Invariant($"AudioLeadIn: {beatmap.AudioLeadIn}"));
writer.WriteLine(FormattableString.Invariant($"PreviewTime: {beatmap.Metadata.PreviewTime}")); writer.WriteLine(FormattableString.Invariant($"PreviewTime: {beatmap.Metadata.PreviewTime}"));
writer.WriteLine(FormattableString.Invariant($"Countdown: {(int)beatmap.BeatmapInfo.Countdown}")); writer.WriteLine(FormattableString.Invariant($"Countdown: {(int)beatmap.Countdown}"));
writer.WriteLine(FormattableString.Invariant( writer.WriteLine(FormattableString.Invariant(
$"SampleSet: {toLegacySampleBank(((beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePoints.FirstOrDefault() ?? SampleControlPoint.DEFAULT).SampleBank)}")); $"SampleSet: {toLegacySampleBank(((beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePoints.FirstOrDefault() ?? SampleControlPoint.DEFAULT).SampleBank)}"));
writer.WriteLine(FormattableString.Invariant($"StackLeniency: {beatmap.BeatmapInfo.StackLeniency}")); writer.WriteLine(FormattableString.Invariant($"StackLeniency: {beatmap.StackLeniency}"));
writer.WriteLine(FormattableString.Invariant($"Mode: {onlineRulesetID}")); writer.WriteLine(FormattableString.Invariant($"Mode: {onlineRulesetID}"));
writer.WriteLine(FormattableString.Invariant($"LetterboxInBreaks: {(beatmap.BeatmapInfo.LetterboxInBreaks ? '1' : '0')}")); writer.WriteLine(FormattableString.Invariant($"LetterboxInBreaks: {(beatmap.LetterboxInBreaks ? '1' : '0')}"));
// if (beatmap.BeatmapInfo.UseSkinSprites) // if (beatmap.BeatmapInfo.UseSkinSprites)
// writer.WriteLine(@"UseSkinSprites: 1"); // writer.WriteLine(@"UseSkinSprites: 1");
// if (b.AlwaysShowPlayfield) // if (b.AlwaysShowPlayfield)
@ -95,14 +95,14 @@ namespace osu.Game.Beatmaps.Formats
// writer.WriteLine(@"OverlayPosition: " + b.OverlayPosition); // writer.WriteLine(@"OverlayPosition: " + b.OverlayPosition);
// if (!string.IsNullOrEmpty(b.SkinPreference)) // if (!string.IsNullOrEmpty(b.SkinPreference))
// writer.WriteLine(@"SkinPreference:" + b.SkinPreference); // writer.WriteLine(@"SkinPreference:" + b.SkinPreference);
if (beatmap.BeatmapInfo.EpilepsyWarning) if (beatmap.EpilepsyWarning)
writer.WriteLine(@"EpilepsyWarning: 1"); writer.WriteLine(@"EpilepsyWarning: 1");
if (beatmap.BeatmapInfo.CountdownOffset > 0) if (beatmap.CountdownOffset > 0)
writer.WriteLine(FormattableString.Invariant($@"CountdownOffset: {beatmap.BeatmapInfo.CountdownOffset}")); writer.WriteLine(FormattableString.Invariant($@"CountdownOffset: {beatmap.CountdownOffset}"));
if (onlineRulesetID == 3) if (onlineRulesetID == 3)
writer.WriteLine(FormattableString.Invariant($"SpecialStyle: {(beatmap.BeatmapInfo.SpecialStyle ? '1' : '0')}")); writer.WriteLine(FormattableString.Invariant($"SpecialStyle: {(beatmap.SpecialStyle ? '1' : '0')}"));
writer.WriteLine(FormattableString.Invariant($"WidescreenStoryboard: {(beatmap.BeatmapInfo.WidescreenStoryboard ? '1' : '0')}")); writer.WriteLine(FormattableString.Invariant($"WidescreenStoryboard: {(beatmap.WidescreenStoryboard ? '1' : '0')}"));
if (beatmap.BeatmapInfo.SamplesMatchPlaybackRate) if (beatmap.SamplesMatchPlaybackRate)
writer.WriteLine(@"SamplesMatchPlaybackRate: 1"); writer.WriteLine(@"SamplesMatchPlaybackRate: 1");
} }
@ -112,10 +112,10 @@ namespace osu.Game.Beatmaps.Formats
if (beatmap.BeatmapInfo.Bookmarks.Length > 0) if (beatmap.BeatmapInfo.Bookmarks.Length > 0)
writer.WriteLine(FormattableString.Invariant($"Bookmarks: {string.Join(',', beatmap.BeatmapInfo.Bookmarks)}")); writer.WriteLine(FormattableString.Invariant($"Bookmarks: {string.Join(',', beatmap.BeatmapInfo.Bookmarks)}"));
writer.WriteLine(FormattableString.Invariant($"DistanceSpacing: {beatmap.BeatmapInfo.DistanceSpacing}")); writer.WriteLine(FormattableString.Invariant($"DistanceSpacing: {beatmap.DistanceSpacing}"));
writer.WriteLine(FormattableString.Invariant($"BeatDivisor: {beatmap.BeatmapInfo.BeatDivisor}")); writer.WriteLine(FormattableString.Invariant($"BeatDivisor: {beatmap.BeatmapInfo.BeatDivisor}"));
writer.WriteLine(FormattableString.Invariant($"GridSize: {beatmap.BeatmapInfo.GridSize}")); writer.WriteLine(FormattableString.Invariant($"GridSize: {beatmap.GridSize}"));
writer.WriteLine(FormattableString.Invariant($"TimelineZoom: {beatmap.BeatmapInfo.TimelineZoom}")); writer.WriteLine(FormattableString.Invariant($"TimelineZoom: {beatmap.TimelineZoom}"));
} }
private void handleMetadata(TextWriter writer) private void handleMetadata(TextWriter writer)

View File

@ -38,6 +38,17 @@ namespace osu.Game.Beatmaps.Formats
SetFallbackDecoder<Storyboard>(() => new LegacyStoryboardDecoder()); SetFallbackDecoder<Storyboard>(() => new LegacyStoryboardDecoder());
} }
protected override Storyboard CreateTemplateObject()
{
var sb = base.CreateTemplateObject();
var beatmap = new Beatmap();
LegacyBeatmapDecoder.ApplyLegacyDefaults(beatmap);
sb.Beatmap = beatmap;
return sb;
}
protected override void ParseStreamInto(LineBufferedReader stream, Storyboard storyboard) protected override void ParseStreamInto(LineBufferedReader stream, Storyboard storyboard)
{ {
this.storyboard = storyboard; this.storyboard = storyboard;
@ -73,6 +84,10 @@ namespace osu.Game.Beatmaps.Formats
case "UseSkinSprites": case "UseSkinSprites":
storyboard.UseSkinSprites = pair.Value == "1"; storyboard.UseSkinSprites = pair.Value == "1";
break; break;
case @"WidescreenStoryboard":
storyboard.Beatmap.WidescreenStoryboard = Parsing.ParseInt(pair.Value) == 1;
break;
} }
} }

View File

@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Lists; using osu.Framework.Lists;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -69,6 +70,43 @@ namespace osu.Game.Beatmaps
/// </summary> /// </summary>
double GetMostCommonBeatLength(); double GetMostCommonBeatLength();
double AudioLeadIn { get; internal set; }
float StackLeniency { get; internal set; }
bool SpecialStyle { get; internal set; }
bool LetterboxInBreaks { get; internal set; }
bool WidescreenStoryboard { get; internal set; }
bool EpilepsyWarning { get; internal set; }
bool SamplesMatchPlaybackRate { get; internal set; }
/// <summary>
/// The ratio of distance travelled per time unit.
/// Generally used to decouple the spacing between hit objects from the enforced "velocity" of the beatmap (see <see cref="DifficultyControlPoint.SliderVelocity"/>).
/// </summary>
/// <remarks>
/// The most common method of understanding is that at a default value of 1.0, the time-to-distance ratio will match the slider velocity of the beatmap
/// at the current point in time. Increasing this value will make hit objects more spaced apart when compared to the cursor movement required to track a slider.
///
/// This is only a hint property, used by the editor in <see cref="IDistanceSnapProvider"/> implementations. It does not directly affect the beatmap or gameplay.
/// </remarks>
double DistanceSpacing { get; internal set; }
int GridSize { get; internal set; }
double TimelineZoom { get; internal set; }
CountdownType Countdown { get; internal set; }
/// <summary>
/// The number of beats to move the countdown backwards (compared to its default location).
/// </summary>
int CountdownOffset { get; internal set; }
/// <summary> /// <summary>
/// Creates a shallow-clone of this beatmap and returns it. /// Creates a shallow-clone of this beatmap and returns it.
/// </summary> /// </summary>

View File

@ -62,7 +62,12 @@ namespace osu.Game.Beatmaps
#region Resource getters #region Resource getters
protected virtual Waveform GetWaveform() => new Waveform(null); protected virtual Waveform GetWaveform() => new Waveform(null);
protected virtual Storyboard GetStoryboard() => new Storyboard { BeatmapInfo = BeatmapInfo };
protected virtual Storyboard GetStoryboard() => new Storyboard
{
BeatmapInfo = BeatmapInfo,
Beatmap = Beatmap,
};
protected abstract IBeatmap GetBeatmap(); protected abstract IBeatmap GetBeatmap();
public abstract Texture GetBackground(); public abstract Texture GetBackground();

View File

@ -94,8 +94,9 @@ namespace osu.Game.Database
/// 41 2024-04-17 Add ScoreInfo.TotalScoreWithoutMods for future mod multiplier rebalances. /// 41 2024-04-17 Add ScoreInfo.TotalScoreWithoutMods for future mod multiplier rebalances.
/// 42 2024-08-07 Update mania key bindings to reflect changes to ManiaAction /// 42 2024-08-07 Update mania key bindings to reflect changes to ManiaAction
/// 43 2024-10-14 Reset keybind for toggling FPS display to avoid conflict with "convert to stream" in the editor, if not already changed by user. /// 43 2024-10-14 Reset keybind for toggling FPS display to avoid conflict with "convert to stream" in the editor, if not already changed by user.
/// 44 2024-11-22 Removed several properties from BeatmapInfo which did not need to be persisted to realm.
/// </summary> /// </summary>
private const int schema_version = 43; private const int schema_version = 44;
/// <summary> /// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods. /// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.

View File

@ -174,6 +174,11 @@ namespace osu.Game
/// </summary> /// </summary>
public readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(); public readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>();
/// <summary>
/// Whether the back button is currently displayed.
/// </summary>
private readonly IBindable<bool> backButtonVisibility = new Bindable<bool>();
IBindable<LocalUserPlayingState> ILocalUserPlayInfo.PlayingState => playingState; IBindable<LocalUserPlayingState> ILocalUserPlayInfo.PlayingState => playingState;
private readonly Bindable<LocalUserPlayingState> playingState = new Bindable<LocalUserPlayingState>(); private readonly Bindable<LocalUserPlayingState> playingState = new Bindable<LocalUserPlayingState>();
@ -1019,7 +1024,7 @@ namespace osu.Game
if (!(ScreenStack.CurrentScreen is IOsuScreen currentScreen)) if (!(ScreenStack.CurrentScreen is IOsuScreen currentScreen))
return; return;
if (!((Drawable)currentScreen).IsLoaded || (currentScreen.AllowBackButton && !currentScreen.OnBackButton())) if (!((Drawable)currentScreen).IsLoaded || (currentScreen.AllowUserExit && !currentScreen.OnBackButton()))
ScreenStack.Exit(); ScreenStack.Exit();
} }
}, },
@ -1189,6 +1194,14 @@ namespace osu.Game
if (mode.NewValue != OverlayActivation.All) CloseAllOverlays(); if (mode.NewValue != OverlayActivation.All) CloseAllOverlays();
}; };
backButtonVisibility.ValueChanged += visible =>
{
if (visible.NewValue)
BackButton.Show();
else
BackButton.Hide();
};
// Importantly, this should be run after binding PostNotification to the import handlers so they can present the import after game startup. // Importantly, this should be run after binding PostNotification to the import handlers so they can present the import after game startup.
handleStartupImport(); handleStartupImport();
} }
@ -1581,12 +1594,14 @@ namespace osu.Game
if (current is IOsuScreen currentOsuScreen) if (current is IOsuScreen currentOsuScreen)
{ {
backButtonVisibility.UnbindFrom(currentOsuScreen.BackButtonVisibility);
OverlayActivationMode.UnbindFrom(currentOsuScreen.OverlayActivationMode); OverlayActivationMode.UnbindFrom(currentOsuScreen.OverlayActivationMode);
API.Activity.UnbindFrom(currentOsuScreen.Activity); API.Activity.UnbindFrom(currentOsuScreen.Activity);
} }
if (newScreen is IOsuScreen newOsuScreen) if (newScreen is IOsuScreen newOsuScreen)
{ {
backButtonVisibility.BindTo(newOsuScreen.BackButtonVisibility);
OverlayActivationMode.BindTo(newOsuScreen.OverlayActivationMode); OverlayActivationMode.BindTo(newOsuScreen.OverlayActivationMode);
API.Activity.BindTo(newOsuScreen.Activity); API.Activity.BindTo(newOsuScreen.Activity);
@ -1597,11 +1612,6 @@ namespace osu.Game
else else
Toolbar.Show(); Toolbar.Show();
if (newOsuScreen.AllowBackButton)
BackButton.Show();
else
BackButton.Hide();
if (newOsuScreen.ShowFooter) if (newOsuScreen.ShowFooter)
{ {
BackButton.Hide(); BackButton.Hide();

View File

@ -26,7 +26,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private OsuGame game { get; set; } private OsuGame game { get; set; }
public override bool AllowBackButton => false; public override bool AllowUserExit => false;
public override bool AllowExternalScreenChange => false; public override bool AllowExternalScreenChange => false;

View File

@ -184,7 +184,7 @@ namespace osu.Game.Overlays
content.ResizeHeightTo(0, animate ? transition_duration : 0, Easing.OutQuint); content.ResizeHeightTo(0, animate ? transition_duration : 0, Easing.OutQuint);
} }
headerContent.FadeColour(Expanded.Value ? Color4.White : OsuColour.Gray(0.5f), 200, Easing.OutQuint); headerContent.FadeColour(Expanded.Value ? Color4.White : OsuColour.Gray(0.7f), 200, Easing.OutQuint);
} }
private void updateFadeState() private void updateFadeState()

View File

@ -341,6 +341,78 @@ namespace osu.Game.Rulesets.Difficulty
public double GetMostCommonBeatLength() => baseBeatmap.GetMostCommonBeatLength(); public double GetMostCommonBeatLength() => baseBeatmap.GetMostCommonBeatLength();
public IBeatmap Clone() => new ProgressiveCalculationBeatmap(baseBeatmap.Clone()); public IBeatmap Clone() => new ProgressiveCalculationBeatmap(baseBeatmap.Clone());
public double AudioLeadIn
{
get => baseBeatmap.AudioLeadIn;
set => baseBeatmap.AudioLeadIn = value;
}
public float StackLeniency
{
get => baseBeatmap.StackLeniency;
set => baseBeatmap.StackLeniency = value;
}
public bool SpecialStyle
{
get => baseBeatmap.SpecialStyle;
set => baseBeatmap.SpecialStyle = value;
}
public bool LetterboxInBreaks
{
get => baseBeatmap.LetterboxInBreaks;
set => baseBeatmap.LetterboxInBreaks = value;
}
public bool WidescreenStoryboard
{
get => baseBeatmap.WidescreenStoryboard;
set => baseBeatmap.WidescreenStoryboard = value;
}
public bool EpilepsyWarning
{
get => baseBeatmap.EpilepsyWarning;
set => baseBeatmap.EpilepsyWarning = value;
}
public bool SamplesMatchPlaybackRate
{
get => baseBeatmap.SamplesMatchPlaybackRate;
set => baseBeatmap.SamplesMatchPlaybackRate = value;
}
public double DistanceSpacing
{
get => baseBeatmap.DistanceSpacing;
set => baseBeatmap.DistanceSpacing = value;
}
public int GridSize
{
get => baseBeatmap.GridSize;
set => baseBeatmap.GridSize = value;
}
public double TimelineZoom
{
get => baseBeatmap.TimelineZoom;
set => baseBeatmap.TimelineZoom = value;
}
public CountdownType Countdown
{
get => baseBeatmap.Countdown;
set => baseBeatmap.Countdown = value;
}
public int CountdownOffset
{
get => baseBeatmap.CountdownOffset;
set => baseBeatmap.CountdownOffset = value;
}
#endregion #endregion
} }
} }

View File

@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Edit
} }
}); });
DistanceSpacingMultiplier.Value = editorBeatmap.BeatmapInfo.DistanceSpacing; DistanceSpacingMultiplier.Value = editorBeatmap.DistanceSpacing;
DistanceSpacingMultiplier.BindValueChanged(multiplier => DistanceSpacingMultiplier.BindValueChanged(multiplier =>
{ {
distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})"; distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})";
@ -109,7 +109,7 @@ namespace osu.Game.Rulesets.Edit
if (multiplier.NewValue != multiplier.OldValue) if (multiplier.NewValue != multiplier.OldValue)
onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier)); onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier));
editorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue; editorBeatmap.DistanceSpacing = multiplier.NewValue;
}, true); }, true);
DistanceSpacingMultiplier.BindDisabledChanged(disabled => distanceSpacingSlider.Alpha = disabled ? 0 : 1, true); DistanceSpacingMultiplier.BindDisabledChanged(disabled => distanceSpacingSlider.Alpha = disabled ? 0 : 1, true);

View File

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

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Edit
/// A multiplier which changes the ratio of distance travelled per time unit. /// A multiplier which changes the ratio of distance travelled per time unit.
/// Importantly, this is provided for manual usage, and not multiplied into any of the methods exposed by this interface. /// Importantly, this is provided for manual usage, and not multiplied into any of the methods exposed by this interface.
/// </summary> /// </summary>
/// <seealso cref="BeatmapInfo.DistanceSpacing"/> /// <seealso cref="IBeatmap.DistanceSpacing"/>
Bindable<double> DistanceSpacingMultiplier { get; } Bindable<double> DistanceSpacingMultiplier { get; }
/// <summary> /// <summary>

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.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osuTK; 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> /// <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> /// </summary>
public static void SnapTo<THitObject>(this THitObject hitObject, IDistanceSnapProvider? snapProvider) public Vector2 PositionalOffset { get; private set; }
where THitObject : HitObject, IHasPath
{ private readonly SliderPath sliderPath;
hitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(hitObject, (float)hitObject.Path.CalculatedDistance, DistanceSnapTarget.Start) ?? hitObject.Path.CalculatedDistance;
}
/// <summary> /// <summary>
/// Reverse the direction of this path. /// Reverse the direction of this path.
/// </summary> /// </summary>
/// <param name="sliderPath">The <see cref="SliderPath"/>.</param> /// <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 ReverseSliderPathChange(SliderPath sliderPath)
public static void Reverse(this SliderPath sliderPath, out Vector2 positionalOffset) {
this.sliderPath = sliderPath;
}
protected override void SubmitChanges()
{ {
var controlPoints = sliderPath.ControlPoints; 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, // 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. // 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(); double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray();
@ -46,11 +51,11 @@ namespace osu.Game.Rulesets.Objects
segmentEnds = segmentEnds[..^1]; segmentEnds = segmentEnds[..^1];
} }
controlPoints.RemoveAt(controlPoints.Count - 1); Submit(new RemovePathControlPointChange(controlPoints, controlPoints.Count - 1));
} }
// Restore original control point types. // 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. // 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()) 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>(); var circleArcPath = new List<Vector2>();
sliderPath.GetPathToProgress(circleArcPath, lastSegmentStart / lastSegmentEnd, 1); 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> private void reverseControlPoints()
/// 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)
{ {
var points = sliderPath.ControlPoints.ToArray(); 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; PathType? lastType = null;
for (int i = 0; i < points.Length; i++) for (int i = 0; i < points.Length; i++)
{ {
var p = points[i]; var p = new PathControlPoint(points[i].Position, points[i].Type);
p.Position -= positionalOffset; p.Position -= PositionalOffset;
// propagate types forwards to last null type // propagate types forwards to last null type
if (i == points.Length - 1) if (i == points.Length - 1)
@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Objects
else if (p.Type != null) else if (p.Type != null)
(p.Type, lastType) = (lastType, p.Type); (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; } private IPositionSnapProvider snapProvider { get; set; }
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; } private NewBeatmapEditorChangeHandler changeHandler { get; set; }
protected readonly BindableList<T> SelectedItems = new BindableList<T>(); protected readonly BindableList<T> SelectedItems = new BindableList<T>();
@ -433,7 +433,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// Finishes the current blueprint selection. /// Finishes the current blueprint selection.
/// </summary> /// </summary>
/// <param name="e">The mouse event which triggered end of selection.</param> /// <param name="e">The mouse event which triggered end of selection.</param>
/// <returns>Whether a click selection was active.</returns> /// <returns>
/// Whether the mouse event is considered to be fully handled.
/// If the return value is <see langword="false"/>, the standard click / mouse up action will follow.
/// </returns>
private bool endClickSelection(MouseButtonEvent e) private bool endClickSelection(MouseButtonEvent e)
{ {
// If already handled a selection, double-click, or drag, we don't want to perform a mouse up / click action. // If already handled a selection, double-click, or drag, we don't want to perform a mouse up / click action.
@ -443,14 +446,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (e.ControlPressed) if (e.ControlPressed)
{ {
// if a selection didn't occur, we may want to trigger a deselection.
// Iterate from the top of the input stack (blueprints closest to the front of the screen first). // Iterate from the top of the input stack (blueprints closest to the front of the screen first).
// Priority is given to already-selected blueprints. // Priority is given to already-selected blueprints.
foreach (SelectionBlueprint<T> blueprint in SelectionBlueprints.AliveChildren.Where(b => b.IsHovered).OrderByDescending(b => b.IsSelected)) foreach (SelectionBlueprint<T> blueprint in SelectionBlueprints.AliveChildren.Where(b => b.IsHovered).OrderByDescending(b => b.IsSelected))
return clickSelectionHandled = SelectionHandler.MouseUpSelectionRequested(blueprint, e); return clickSelectionHandled = SelectionHandler.MouseUpSelectionRequested(blueprint, e);
return false; // can only be reached if there are no hovered blueprints.
// in that case, we still want to suppress mouse up / click handling, because when control is pressed,
// it is presumed we want to add to existing selection, not remove from it
// (unless explicitly control-clicking a selected object, which is handled above).
return true;
} }
if (selectedBlueprintAlreadySelectedOnMouseDown && SelectedItems.Count == 1) if (selectedBlueprintAlreadySelectedOnMouseDown && SelectedItems.Count == 1)

View File

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

View File

@ -157,7 +157,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
Scheduler.AddOnce(applyVisualOffset, beatmap); Scheduler.AddOnce(applyVisualOffset, beatmap);
}, true); }, true);
Zoom = (float)(defaultTimelineZoom * editorBeatmap.BeatmapInfo.TimelineZoom); Zoom = (float)(defaultTimelineZoom * editorBeatmap.TimelineZoom);
} }
private void applyVisualOffset(IBindable<WorkingBeatmap> beatmap) private void applyVisualOffset(IBindable<WorkingBeatmap> beatmap)
@ -212,7 +212,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
float minimumZoom = getZoomLevelForVisibleMilliseconds(10000); float minimumZoom = getZoomLevelForVisibleMilliseconds(10000);
float maximumZoom = getZoomLevelForVisibleMilliseconds(500); float maximumZoom = getZoomLevelForVisibleMilliseconds(500);
float initialZoom = (float)Math.Clamp(defaultTimelineZoom * (editorBeatmap.BeatmapInfo.TimelineZoom == 0 ? 1 : editorBeatmap.BeatmapInfo.TimelineZoom), minimumZoom, maximumZoom); float initialZoom = (float)Math.Clamp(defaultTimelineZoom * (editorBeatmap.TimelineZoom == 0 ? 1 : editorBeatmap.TimelineZoom), minimumZoom, maximumZoom);
SetupZoom(initialZoom, minimumZoom, maximumZoom); SetupZoom(initialZoom, minimumZoom, maximumZoom);
@ -234,7 +234,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override void OnZoomChanged() protected override void OnZoomChanged()
{ {
base.OnZoomChanged(); base.OnZoomChanged();
editorBeatmap.BeatmapInfo.TimelineZoom = Zoom / defaultTimelineZoom; editorBeatmap.TimelineZoom = Zoom / defaultTimelineZoom;
} }
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()

View File

@ -80,8 +80,6 @@ namespace osu.Game.Screens.Edit
public override float BackgroundParallaxAmount => 0.1f; public override float BackgroundParallaxAmount => 0.1f;
public override bool AllowBackButton => false;
public override bool HideOverlaysOnEnter => true; public override bool HideOverlaysOnEnter => true;
public override bool DisallowExternalBeatmapRulesetChanges => true; public override bool DisallowExternalBeatmapRulesetChanges => true;
@ -179,6 +177,8 @@ namespace osu.Game.Screens.Edit
[CanBeNull] // Should be non-null once it can support custom rulesets. [CanBeNull] // Should be non-null once it can support custom rulesets.
private EditorChangeHandler changeHandler; private EditorChangeHandler changeHandler;
private NewBeatmapEditorChangeHandler newChangeHandler;
private DependencyContainer dependencies; private DependencyContainer dependencies;
private bool isNewBeatmap; private bool isNewBeatmap;
@ -194,6 +194,8 @@ namespace osu.Game.Screens.Edit
} }
} }
protected override bool InitialBackButtonVisibility => false;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); => dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
@ -302,6 +304,9 @@ namespace osu.Game.Screens.Edit
dependencies.CacheAs<IEditorChangeHandler>(changeHandler); dependencies.CacheAs<IEditorChangeHandler>(changeHandler);
} }
newChangeHandler = new NewBeatmapEditorChangeHandler(editorBeatmap);
dependencies.CacheAs(newChangeHandler);
beatDivisor.SetArbitraryDivisor(editorBeatmap.BeatmapInfo.BeatDivisor); beatDivisor.SetArbitraryDivisor(editorBeatmap.BeatmapInfo.BeatDivisor);
beatDivisor.BindValueChanged(divisor => editorBeatmap.BeatmapInfo.BeatDivisor = divisor.NewValue); 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); newChangeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
changeHandler?.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true); newChangeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
editorBackgroundDim.BindValueChanged(_ => dimBackground()); editorBackgroundDim.BindValueChanged(_ => dimBackground());
} }
@ -760,11 +765,6 @@ namespace osu.Game.Screens.Edit
switch (e.Action) switch (e.Action)
{ {
case GlobalAction.Back:
// as we don't want to display the back button, manual handling of exit action is required.
this.Exit();
return true;
case GlobalAction.EditorCloneSelection: case GlobalAction.EditorCloneSelection:
Clone(); Clone();
return true; return true;
@ -976,9 +976,9 @@ namespace osu.Game.Screens.Edit
#endregion #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() protected void SetPreviewPointToCurrentTime()
{ {

View File

@ -198,6 +198,78 @@ namespace osu.Game.Screens.Edit
public double GetMostCommonBeatLength() => PlayableBeatmap.GetMostCommonBeatLength(); public double GetMostCommonBeatLength() => PlayableBeatmap.GetMostCommonBeatLength();
public double AudioLeadIn
{
get => PlayableBeatmap.AudioLeadIn;
set => PlayableBeatmap.AudioLeadIn = value;
}
public float StackLeniency
{
get => PlayableBeatmap.StackLeniency;
set => PlayableBeatmap.StackLeniency = value;
}
public bool SpecialStyle
{
get => PlayableBeatmap.SpecialStyle;
set => PlayableBeatmap.SpecialStyle = value;
}
public bool LetterboxInBreaks
{
get => PlayableBeatmap.LetterboxInBreaks;
set => PlayableBeatmap.LetterboxInBreaks = value;
}
public bool WidescreenStoryboard
{
get => PlayableBeatmap.WidescreenStoryboard;
set => PlayableBeatmap.WidescreenStoryboard = value;
}
public bool EpilepsyWarning
{
get => PlayableBeatmap.EpilepsyWarning;
set => PlayableBeatmap.EpilepsyWarning = value;
}
public bool SamplesMatchPlaybackRate
{
get => PlayableBeatmap.SamplesMatchPlaybackRate;
set => PlayableBeatmap.SamplesMatchPlaybackRate = value;
}
public double DistanceSpacing
{
get => PlayableBeatmap.DistanceSpacing;
set => PlayableBeatmap.DistanceSpacing = value;
}
public int GridSize
{
get => PlayableBeatmap.GridSize;
set => PlayableBeatmap.GridSize = value;
}
public double TimelineZoom
{
get => PlayableBeatmap.TimelineZoom;
set => PlayableBeatmap.TimelineZoom = value;
}
public CountdownType Countdown
{
get => PlayableBeatmap.Countdown;
set => PlayableBeatmap.Countdown = value;
}
public int CountdownOffset
{
get => PlayableBeatmap.CountdownOffset;
set => PlayableBeatmap.CountdownOffset = value;
}
public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone(); public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone();
private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects;

View File

@ -36,7 +36,7 @@ namespace osu.Game.Screens.Edit
public override float BackgroundParallaxAmount => 0.1f; public override float BackgroundParallaxAmount => 0.1f;
public override bool AllowBackButton => false; public override bool AllowUserExit => false;
public override bool HideOverlaysOnEnter => true; public override bool HideOverlaysOnEnter => true;

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

@ -39,7 +39,7 @@ namespace osu.Game.Screens.Edit.Setup
{ {
Caption = EditorSetupStrings.EnableCountdown, Caption = EditorSetupStrings.EnableCountdown,
HintText = EditorSetupStrings.CountdownDescription, HintText = EditorSetupStrings.CountdownDescription,
Current = { Value = Beatmap.BeatmapInfo.Countdown != CountdownType.None }, Current = { Value = Beatmap.Countdown != CountdownType.None },
}, },
CountdownSettings = new FillFlowContainer CountdownSettings = new FillFlowContainer
{ {
@ -52,14 +52,14 @@ namespace osu.Game.Screens.Edit.Setup
CountdownSpeed = new FormEnumDropdown<CountdownType> CountdownSpeed = new FormEnumDropdown<CountdownType>
{ {
Caption = EditorSetupStrings.CountdownSpeed, Caption = EditorSetupStrings.CountdownSpeed,
Current = { Value = Beatmap.BeatmapInfo.Countdown != CountdownType.None ? Beatmap.BeatmapInfo.Countdown : CountdownType.Normal }, Current = { Value = Beatmap.Countdown != CountdownType.None ? Beatmap.Countdown : CountdownType.Normal },
Items = Enum.GetValues<CountdownType>().Where(type => type != CountdownType.None) Items = Enum.GetValues<CountdownType>().Where(type => type != CountdownType.None)
}, },
CountdownOffset = new FormNumberBox CountdownOffset = new FormNumberBox
{ {
Caption = EditorSetupStrings.CountdownOffset, Caption = EditorSetupStrings.CountdownOffset,
HintText = EditorSetupStrings.CountdownOffsetDescription, HintText = EditorSetupStrings.CountdownOffsetDescription,
Current = { Value = Beatmap.BeatmapInfo.CountdownOffset.ToString() }, Current = { Value = Beatmap.CountdownOffset.ToString() },
TabbableContentContainer = this, TabbableContentContainer = this,
} }
} }
@ -68,25 +68,25 @@ namespace osu.Game.Screens.Edit.Setup
{ {
Caption = EditorSetupStrings.WidescreenSupport, Caption = EditorSetupStrings.WidescreenSupport,
HintText = EditorSetupStrings.WidescreenSupportDescription, HintText = EditorSetupStrings.WidescreenSupportDescription,
Current = { Value = Beatmap.BeatmapInfo.WidescreenStoryboard } Current = { Value = Beatmap.WidescreenStoryboard }
}, },
epilepsyWarning = new FormCheckBox epilepsyWarning = new FormCheckBox
{ {
Caption = EditorSetupStrings.EpilepsyWarning, Caption = EditorSetupStrings.EpilepsyWarning,
HintText = EditorSetupStrings.EpilepsyWarningDescription, HintText = EditorSetupStrings.EpilepsyWarningDescription,
Current = { Value = Beatmap.BeatmapInfo.EpilepsyWarning } Current = { Value = Beatmap.EpilepsyWarning }
}, },
letterboxDuringBreaks = new FormCheckBox letterboxDuringBreaks = new FormCheckBox
{ {
Caption = EditorSetupStrings.LetterboxDuringBreaks, Caption = EditorSetupStrings.LetterboxDuringBreaks,
HintText = EditorSetupStrings.LetterboxDuringBreaksDescription, HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
Current = { Value = Beatmap.BeatmapInfo.LetterboxInBreaks } Current = { Value = Beatmap.LetterboxInBreaks }
}, },
samplesMatchPlaybackRate = new FormCheckBox samplesMatchPlaybackRate = new FormCheckBox
{ {
Caption = EditorSetupStrings.SamplesMatchPlaybackRate, Caption = EditorSetupStrings.SamplesMatchPlaybackRate,
HintText = EditorSetupStrings.SamplesMatchPlaybackRateDescription, HintText = EditorSetupStrings.SamplesMatchPlaybackRateDescription,
Current = { Value = Beatmap.BeatmapInfo.SamplesMatchPlaybackRate } Current = { Value = Beatmap.SamplesMatchPlaybackRate }
} }
}; };
} }
@ -113,18 +113,18 @@ namespace osu.Game.Screens.Edit.Setup
{ {
updateBeatmap(); updateBeatmap();
// update displayed text to ensure parsed value matches display (i.e. if empty string was provided). // update displayed text to ensure parsed value matches display (i.e. if empty string was provided).
CountdownOffset.Current.Value = Beatmap.BeatmapInfo.CountdownOffset.ToString(CultureInfo.InvariantCulture); CountdownOffset.Current.Value = Beatmap.CountdownOffset.ToString(CultureInfo.InvariantCulture);
} }
private void updateBeatmap() private void updateBeatmap()
{ {
Beatmap.BeatmapInfo.Countdown = EnableCountdown.Current.Value ? CountdownSpeed.Current.Value : CountdownType.None; Beatmap.Countdown = EnableCountdown.Current.Value ? CountdownSpeed.Current.Value : CountdownType.None;
Beatmap.BeatmapInfo.CountdownOffset = int.TryParse(CountdownOffset.Current.Value, NumberStyles.None, CultureInfo.InvariantCulture, out int offset) ? offset : 0; Beatmap.CountdownOffset = int.TryParse(CountdownOffset.Current.Value, NumberStyles.None, CultureInfo.InvariantCulture, out int offset) ? offset : 0;
Beatmap.BeatmapInfo.WidescreenStoryboard = widescreenSupport.Current.Value; Beatmap.WidescreenStoryboard = widescreenSupport.Current.Value;
Beatmap.BeatmapInfo.EpilepsyWarning = epilepsyWarning.Current.Value; Beatmap.EpilepsyWarning = epilepsyWarning.Current.Value;
Beatmap.BeatmapInfo.LetterboxInBreaks = letterboxDuringBreaks.Current.Value; Beatmap.LetterboxInBreaks = letterboxDuringBreaks.Current.Value;
Beatmap.BeatmapInfo.SamplesMatchPlaybackRate = samplesMatchPlaybackRate.Current.Value; Beatmap.SamplesMatchPlaybackRate = samplesMatchPlaybackRate.Current.Value;
Beatmap.SaveState(); Beatmap.SaveState();
} }

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Input.Bindings;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Screens.Footer; using osu.Game.Screens.Footer;
@ -21,15 +22,21 @@ namespace osu.Game.Screens
bool DisallowExternalBeatmapRulesetChanges { get; } bool DisallowExternalBeatmapRulesetChanges { get; }
/// <summary> /// <summary>
/// Whether the user can exit this <see cref="IOsuScreen"/> by pressing the back button. /// Whether the user can exit this <see cref="IOsuScreen"/>.
/// </summary> /// </summary>
bool AllowBackButton { get; } /// <remarks>
/// When overriden to <c>false</c>,
/// the user is blocked from exiting the screen via the <see cref="GlobalAction.Back"/> action,
/// and the back button is hidden from this screen by the initial state of <see cref="BackButtonVisibility"/> being set to hidden.
/// </remarks>
bool AllowUserExit { get; }
/// <summary> /// <summary>
/// Whether a footer (and a back button) should be displayed underneath the screen. /// Whether a footer (and a back button) should be displayed underneath the screen.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Temporarily, the back button is shown regardless of whether <see cref="AllowBackButton"/> is true. /// Temporarily, the footer's own back button is shown regardless of whether <see cref="BackButtonVisibility"/> is set to hidden.
/// This will be corrected as the footer becomes used more commonly.
/// </remarks> /// </remarks>
bool ShowFooter { get; } bool ShowFooter { get; }
@ -59,6 +66,11 @@ namespace osu.Game.Screens
/// </summary> /// </summary>
IBindable<OverlayActivation> OverlayActivationMode { get; } IBindable<OverlayActivation> OverlayActivationMode { get; }
/// <summary>
/// Whether the back button should be displayed in this screen.
/// </summary>
IBindable<bool> BackButtonVisibility { get; }
/// <summary> /// <summary>
/// The current <see cref="UserActivity"/> for this screen. /// The current <see cref="UserActivity"/> for this screen.
/// </summary> /// </summary>

View File

@ -48,7 +48,7 @@ namespace osu.Game.Screens.Menu
public override bool HideOverlaysOnEnter => Buttons == null || Buttons.State == ButtonSystemState.Initial; public override bool HideOverlaysOnEnter => Buttons == null || Buttons.State == ButtonSystemState.Initial;
public override bool AllowBackButton => false; public override bool AllowUserExit => false;
public override bool AllowExternalScreenChange => true; public override bool AllowExternalScreenChange => true;

View File

@ -126,7 +126,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
syncManager = new SpectatorSyncManager(masterClockContainer) syncManager = new SpectatorSyncManager(masterClockContainer)
{ {
ReadyToStart = performInitialSeek, ReadyToStart = performInitialSeek,
} },
new PlayerSettingsOverlay()
}; };
for (int i = 0; i < Users.Count; i++) for (int i = 0; i < Users.Count; i++)

View File

@ -180,7 +180,7 @@ namespace osu.Game.Screens.OnlinePlay
if (!(screenStack.CurrentScreen is IOnlinePlaySubScreen onlineSubScreen)) if (!(screenStack.CurrentScreen is IOnlinePlaySubScreen onlineSubScreen))
return false; return false;
if (((Drawable)onlineSubScreen).IsLoaded && onlineSubScreen.AllowBackButton && onlineSubScreen.OnBackButton()) if (((Drawable)onlineSubScreen).IsLoaded && onlineSubScreen.AllowUserExit && onlineSubScreen.OnBackButton())
return true; return true;
if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen)) if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen))

View File

@ -37,7 +37,7 @@ namespace osu.Game.Screens
public string Description => Title; public string Description => Title;
public virtual bool AllowBackButton => true; public virtual bool AllowUserExit => true;
public virtual bool ShowFooter => false; public virtual bool ShowFooter => false;
@ -56,6 +56,15 @@ namespace osu.Game.Screens
IBindable<OverlayActivation> IOsuScreen.OverlayActivationMode => OverlayActivationMode; IBindable<OverlayActivation> IOsuScreen.OverlayActivationMode => OverlayActivationMode;
/// <summary>
/// The initial visibility state of the back button when this screen is entered for the first time.
/// </summary>
protected virtual bool InitialBackButtonVisibility => AllowUserExit;
public readonly Bindable<bool> BackButtonVisibility;
IBindable<bool> IOsuScreen.BackButtonVisibility => BackButtonVisibility;
public virtual bool CursorVisible => true; public virtual bool CursorVisible => true;
protected new OsuGameBase Game => base.Game as OsuGameBase; protected new OsuGameBase Game => base.Game as OsuGameBase;
@ -154,6 +163,7 @@ namespace osu.Game.Screens
Origin = Anchor.Centre; Origin = Anchor.Centre;
OverlayActivationMode = new Bindable<OverlayActivation>(InitialOverlayActivationMode); OverlayActivationMode = new Bindable<OverlayActivation>(InitialOverlayActivationMode);
BackButtonVisibility = new Bindable<bool>(InitialBackButtonVisibility);
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]

View File

@ -1,46 +1,102 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osuTK; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Play.PlayerSettings;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public partial class PlayerSettingsOverlay : VisibilityContainer public partial class PlayerSettingsOverlay : ExpandingContainer
{ {
public VisualSettings VisualSettings { get; private set; }
private const float padding = 10;
public const float EXPANDED_WIDTH = player_settings_width + padding * 2;
private const float player_settings_width = 270;
private const int fade_duration = 200; private const int fade_duration = 200;
public readonly VisualSettings VisualSettings; public override void Show() => this.FadeIn(fade_duration);
public override void Hide() => this.FadeOut(fade_duration);
// we'll handle this ourselves because we have slightly custom logic.
protected override bool ExpandOnHover => false;
protected override Container<Drawable> Content => content; protected override Container<Drawable> Content => content;
private readonly FillFlowContainer content; private readonly FillFlowContainer content;
public PlayerSettingsOverlay() private readonly IconButton button;
{
Anchor = Anchor.TopRight;
Origin = Anchor.TopRight;
AutoSizeAxes = Axes.Both;
InternalChild = content = new FillFlowContainer private InputManager inputManager = null!;
public PlayerSettingsOverlay()
: base(0, EXPANDED_WIDTH)
{
Origin = Anchor.TopRight;
Anchor = Anchor.TopRight;
base.Content.Add(content = new FillFlowContainer
{ {
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 20), Spacing = new Vector2(0, 20),
Margin = new MarginPadding(padding),
Children = new PlayerSettingsGroup[] Children = new PlayerSettingsGroup[]
{ {
VisualSettings = new VisualSettings { Expanded = { Value = false } }, VisualSettings = new VisualSettings { Expanded = { Value = false } },
new AudioSettings { Expanded = { Value = false } } new AudioSettings { Expanded = { Value = false } }
} }
}; });
AddInternal(button = new IconButton
{
Icon = FontAwesome.Solid.Cog,
Origin = Anchor.TopRight,
Anchor = Anchor.TopLeft,
Margin = new MarginPadding(5),
Action = () => Expanded.Toggle()
});
AddInternal(new Box
{
Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0), Color4.Black.Opacity(0.8f)),
Depth = float.MaxValue,
RelativeSizeAxes = Axes.Both,
});
} }
protected override void PopIn() => this.FadeIn(fade_duration); protected override void LoadComplete()
protected override void PopOut() => this.FadeOut(fade_duration); {
base.LoadComplete();
inputManager = GetContainingInputManager()!;
}
protected override void Update()
{
base.Update();
Expanded.Value = inputManager.CurrentState.Mouse.Position.X >= button.ScreenSpaceDrawQuad.TopLeft.X;
}
protected override void OnHoverLost(HoverLostEvent e)
{
// handle un-expanding manually because our children do weird hover blocking stuff.
}
public void AddAtStart(PlayerSettingsGroup drawable) => content.Insert(-1, drawable); public void AddAtStart(PlayerSettingsGroup drawable) => content.Insert(-1, drawable);
} }

View File

@ -115,6 +115,8 @@ namespace osu.Game.Screens.Play
public HUDOverlay([CanBeNull] DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods, bool alwaysShowLeaderboard = true) public HUDOverlay([CanBeNull] DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods, bool alwaysShowLeaderboard = true)
{ {
Container rightSettings;
this.drawableRuleset = drawableRuleset; this.drawableRuleset = drawableRuleset;
this.mods = mods; this.mods = mods;
@ -146,7 +148,6 @@ namespace osu.Game.Screens.Play
Children = new Drawable[] Children = new Drawable[]
{ {
ModDisplay = CreateModsContainer(), ModDisplay = CreateModsContainer(),
PlayerSettingsOverlay = CreatePlayerSettingsOverlay(),
} }
}, },
bottomRightElements = new FillFlowContainer bottomRightElements = new FillFlowContainer
@ -164,6 +165,14 @@ namespace osu.Game.Screens.Play
HoldToQuit = CreateHoldForMenuButton(), HoldToQuit = CreateHoldForMenuButton(),
} }
}, },
rightSettings = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
PlayerSettingsOverlay = new PlayerSettingsOverlay(),
}
},
LeaderboardFlow = new FillFlowContainer LeaderboardFlow = new FillFlowContainer
{ {
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
@ -173,7 +182,7 @@ namespace osu.Game.Screens.Play
}, },
}; };
hideTargets = new List<Drawable> { mainComponents, topRightElements }; hideTargets = new List<Drawable> { mainComponents, topRightElements, rightSettings };
if (rulesetComponents != null) if (rulesetComponents != null)
hideTargets.Add(rulesetComponents); hideTargets.Add(rulesetComponents);
@ -389,8 +398,6 @@ namespace osu.Game.Screens.Play
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
}; };
protected PlayerSettingsOverlay CreatePlayerSettingsOverlay() => new PlayerSettingsOverlay();
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{ {
if (e.Repeat) if (e.Repeat)

View File

@ -95,8 +95,8 @@ namespace osu.Game.Screens.Play
// some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available. // some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available.
// this is not available as an option in the live editor but can still be applied via .osu editing. // this is not available as an option in the live editor but can still be applied via .osu editing.
double firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime; double firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime;
if (beatmap.BeatmapInfo.AudioLeadIn > 0) if (beatmap.Beatmap.AudioLeadIn > 0)
time = Math.Min(time, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn); time = Math.Min(time, firstHitObjectTime - beatmap.Beatmap.AudioLeadIn);
return time; return time;
} }

View File

@ -57,7 +57,7 @@ namespace osu.Game.Screens.Play
/// </summary> /// </summary>
public event Action OnGameplayStarted; public event Action OnGameplayStarted;
public override bool AllowBackButton => false; // handled by HoldForMenuButton public override bool AllowUserExit => false; // handled by HoldForMenuButton
protected override bool PlayExitSound => !isRestarting; protected override bool PlayExitSound => !isRestarting;
@ -457,7 +457,7 @@ namespace osu.Game.Screens.Play
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre Origin = Anchor.Centre
}, },
BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) BreakOverlay = new BreakOverlay(working.Beatmap.LetterboxInBreaks, ScoreProcessor)
{ {
Clock = DrawableRuleset.FrameStableClock, Clock = DrawableRuleset.FrameStableClock,
ProcessCustomClock = false, ProcessCustomClock = false,

View File

@ -242,7 +242,7 @@ namespace osu.Game.Screens.Play
sampleRestart = new SkinnableSound(new SampleInfo(@"Gameplay/restart", @"pause-retry-click")) sampleRestart = new SkinnableSound(new SampleInfo(@"Gameplay/restart", @"pause-retry-click"))
}; };
if (Beatmap.Value.BeatmapInfo.EpilepsyWarning) if (Beatmap.Value.Beatmap.EpilepsyWarning)
{ {
disclaimers.Add(epilepsyWarning = new PlayerLoaderDisclaimer(PlayerLoaderStrings.EpilepsyWarningTitle, PlayerLoaderStrings.EpilepsyWarningContent)); disclaimers.Add(epilepsyWarning = new PlayerLoaderDisclaimer(PlayerLoaderStrings.EpilepsyWarningTitle, PlayerLoaderStrings.EpilepsyWarningContent));
} }
@ -485,6 +485,8 @@ namespace osu.Game.Screens.Play
if (quickRestart) if (quickRestart)
{ {
BackButtonVisibility.Value = false;
// A quick restart starts by triggering a fade to black // A quick restart starts by triggering a fade to black
AddInternal(quickRestartBlackLayer = new Box AddInternal(quickRestartBlackLayer = new Box
{ {
@ -503,6 +505,8 @@ namespace osu.Game.Screens.Play
.Delay(quick_restart_initial_delay) .Delay(quick_restart_initial_delay)
.ScaleTo(1) .ScaleTo(1)
.FadeInFromZero(500, Easing.OutQuint); .FadeInFromZero(500, Easing.OutQuint);
this.Delay(quick_restart_initial_delay).Schedule(() => BackButtonVisibility.Value = true);
} }
else else
{ {

View File

@ -10,7 +10,7 @@ namespace osu.Game.Screens
/// </summary> /// </summary>
public abstract partial class StartupScreen : OsuScreen public abstract partial class StartupScreen : OsuScreen
{ {
public override bool AllowBackButton => false; public override bool AllowUserExit => false;
public override bool HideOverlaysOnEnter => true; public override bool HideOverlaysOnEnter => true;

View File

@ -67,7 +67,7 @@ namespace osu.Game.Storyboards.Drawables
bool onlyHasVideoElements = Storyboard.Layers.SelectMany(l => l.Elements).All(e => e is StoryboardVideo); bool onlyHasVideoElements = Storyboard.Layers.SelectMany(l => l.Elements).All(e => e is StoryboardVideo);
Width = Height * (storyboard.BeatmapInfo.WidescreenStoryboard || onlyHasVideoElements ? 16 / 9f : 4 / 3f); Width = Height * (storyboard.Beatmap.WidescreenStoryboard || onlyHasVideoElements ? 16 / 9f : 4 / 3f);
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
Origin = Anchor.Centre; Origin = Anchor.Centre;

View File

@ -17,6 +17,7 @@ namespace osu.Game.Storyboards
public IEnumerable<StoryboardLayer> Layers => layers.Values; public IEnumerable<StoryboardLayer> Layers => layers.Values;
public BeatmapInfo BeatmapInfo = new BeatmapInfo(); public BeatmapInfo BeatmapInfo = new BeatmapInfo();
public IBeatmap Beatmap { get; set; } = new Beatmap();
/// <summary> /// <summary>
/// Whether the storyboard should prefer textures from the current skin before using local storyboard textures. /// Whether the storyboard should prefer textures from the current skin before using local storyboard textures.

View File

@ -27,6 +27,17 @@ namespace osu.Game.Tests.Beatmaps
BeatmapInfo = baseBeatmap.BeatmapInfo; BeatmapInfo = baseBeatmap.BeatmapInfo;
ControlPointInfo = baseBeatmap.ControlPointInfo; ControlPointInfo = baseBeatmap.ControlPointInfo;
UnhandledEventLines = baseBeatmap.UnhandledEventLines; UnhandledEventLines = baseBeatmap.UnhandledEventLines;
AudioLeadIn = baseBeatmap.AudioLeadIn;
StackLeniency = baseBeatmap.StackLeniency;
SpecialStyle = baseBeatmap.SpecialStyle;
LetterboxInBreaks = baseBeatmap.LetterboxInBreaks;
WidescreenStoryboard = baseBeatmap.WidescreenStoryboard;
EpilepsyWarning = baseBeatmap.EpilepsyWarning;
SamplesMatchPlaybackRate = baseBeatmap.SamplesMatchPlaybackRate;
DistanceSpacing = baseBeatmap.DistanceSpacing;
GridSize = baseBeatmap.GridSize;
TimelineZoom = baseBeatmap.TimelineZoom;
CountdownOffset = baseBeatmap.CountdownOffset;
if (withHitObjects) if (withHitObjects)
{ {