1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-25 15:50:14 +08:00

Compare commits

..

33 Commits

18 changed files with 460 additions and 63 deletions
@@ -10,7 +10,9 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
@@ -261,6 +263,163 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("selection preserved", () => EditorBeatmap.SelectedHitObjects.Count, () => Is.EqualTo(1));
}
[Test]
public void TestQuickDeleteOnUnselectedControlPointOnlyRemovesThatControlPoint()
{
var slider = new Slider
{
StartTime = 0,
Position = new Vector2(100, 100),
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint { Type = PathType.LINEAR },
new PathControlPoint(new Vector2(100, 0)),
new PathControlPoint(new Vector2(100)),
new PathControlPoint(new Vector2(0, 100))
}
}
};
AddStep("add slider", () => EditorBeatmap.Add(slider));
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("select second node", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1));
InputManager.Click(MouseButton.Left);
});
AddStep("also select third node", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(2));
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddStep("quick-delete fourth node", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(3));
InputManager.Click(MouseButton.Middle);
});
AddUntilStep("slider not deleted", () => EditorBeatmap.HitObjects.OfType<Slider>().Count(), () => Is.EqualTo(1));
AddUntilStep("slider path has 3 nodes", () => EditorBeatmap.HitObjects.OfType<Slider>().Single().Path.ControlPoints.Count, () => Is.EqualTo(3));
}
[Test]
public void TestQuickDeleteOnSelectedControlPointRemovesEntireSelection()
{
var slider = new Slider
{
StartTime = 0,
Position = new Vector2(100, 100),
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint { Type = PathType.LINEAR },
new PathControlPoint(new Vector2(100, 0)),
new PathControlPoint(new Vector2(100)),
new PathControlPoint(new Vector2(0, 100))
}
}
};
AddStep("add slider", () => EditorBeatmap.Add(slider));
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("select second node", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1));
InputManager.Click(MouseButton.Left);
});
AddStep("also select third node", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(2));
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddStep("quick-delete second node", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().ElementAt(1));
InputManager.Click(MouseButton.Middle);
});
AddUntilStep("slider not deleted", () => EditorBeatmap.HitObjects.OfType<Slider>().Count(), () => Is.EqualTo(1));
AddUntilStep("slider path has 2 nodes", () => EditorBeatmap.HitObjects.OfType<Slider>().Single().Path.ControlPoints.Count, () => Is.EqualTo(2));
}
[Test]
public void TestSliderDragMarkerDoesNotBlockControlPointContextMenu()
{
var slider = new Slider
{
StartTime = 0,
Position = new Vector2(100, 100),
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint { Type = PathType.LINEAR },
new PathControlPoint(new Vector2(50, 100)),
new PathControlPoint(new Vector2(145, 100)),
},
ExpectedDistance = { Value = 162.62 }
},
};
AddStep("add slider", () => EditorBeatmap.Add(slider));
AddStep("select slider", () => EditorBeatmap.SelectedHitObjects.Add(slider));
AddStep("select last node", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<PathControlPointPiece<Slider>>().Last());
InputManager.Click(MouseButton.Left);
});
AddStep("right click node", () => InputManager.Click(MouseButton.Right));
AddUntilStep("context menu open", () => this.ChildrenOfType<ContextMenuContainer>().Single().ChildrenOfType<Menu>().All(m => m.State == MenuState.Open));
}
[Test]
public void TestSliderDragMarkerBlocksSelectionOfObjectsUnderneath()
{
var firstSlider = new Slider
{
StartTime = 0,
Position = new Vector2(10, 50),
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(),
new PathControlPoint(new Vector2(100))
}
}
};
var secondSlider = new Slider
{
StartTime = 500,
Position = new Vector2(200, 0),
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(),
new PathControlPoint(new Vector2(-100, 100))
}
}
};
AddStep("add objects", () => EditorBeatmap.AddRange(new HitObject[] { firstSlider, secondSlider }));
AddStep("select second slider", () => EditorBeatmap.SelectedHitObjects.Add(secondSlider));
AddStep("move to marker", () =>
{
var marker = this.ChildrenOfType<SliderEndDragMarker>().First();
var position = (marker.ScreenSpaceDrawQuad.TopRight + marker.ScreenSpaceDrawQuad.BottomRight) / 2;
InputManager.MoveMouseTo(position);
});
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("second slider still selected", () => EditorBeatmap.SelectedHitObjects.Single(), () => Is.EqualTo(secondSlider));
}
private ComposeBlueprintContainer blueprintContainer
=> Editor.ChildrenOfType<ComposeBlueprintContainer>().First();
@@ -0,0 +1,100 @@
// 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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModRelax : OsuModTestScene
{
private readonly HitCircle hitObject;
private readonly HitWindows hitWindows = new OsuHitWindows();
public TestSceneOsuModRelax()
{
hitWindows.SetDifficulty(9);
hitObject = new HitCircle
{
StartTime = 1000,
Position = new Vector2(100, 100),
HitWindows = hitWindows
};
}
protected override TestPlayer CreateModPlayer(Ruleset ruleset) => new ModRelaxTestPlayer(CurrentTestData, AllowFail);
[Test]
public void TestRelax() => CreateModTest(new ModTestData
{
Mod = new OsuModRelax(),
Autoplay = false,
CreateBeatmap = () => new Beatmap
{
HitObjects = new List<HitObject> { hitObject }
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(0, new Vector2()),
new OsuReplayFrame(hitObject.StartTime, hitObject.Position),
},
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1
});
[Test]
public void TestRelaxLeniency() => CreateModTest(new ModTestData
{
Mod = new OsuModRelax(),
Autoplay = false,
CreateBeatmap = () => new Beatmap
{
HitObjects = new List<HitObject> { hitObject }
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(0, new Vector2(hitObject.X - 22, hitObject.Y - 22)), // must be an edge hit for the cursor to not stay on the object for too long
new OsuReplayFrame(hitObject.StartTime - OsuModRelax.RELAX_LENIENCY, new Vector2(hitObject.X - 22, hitObject.Y - 22)),
new OsuReplayFrame(hitObject.StartTime, new Vector2(0)),
},
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1
});
protected partial class ModRelaxTestPlayer : ModTestPlayer
{
private readonly ModTestData currentTestData;
public ModRelaxTestPlayer(ModTestData data, bool allowFail)
: base(data, allowFail)
{
currentTestData = data;
}
protected override void PrepareReplay()
{
// We need to set IsLegacyScore to true otherwise the mod assumes that presses are already embedded into the replay
DrawableRuleset?.SetReplayScore(new Score
{
Replay = new Replay { Frames = currentTestData.ReplayFrames! },
ScoreInfo = new ScoreInfo { User = new APIUser { Username = @"Test" }, IsLegacyScore = true, Mods = new Mod[] { new OsuModRelax() } },
});
DrawableRuleset?.SetRecordTarget(Score);
}
}
}
}
@@ -137,11 +137,27 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
/// <summary>
/// Delete all visually selected <see cref="PathControlPoint"/>s.
/// </summary>
/// <returns></returns>
/// <returns>Whether any change actually took place.</returns>
public bool DeleteSelected()
{
List<PathControlPoint> toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => p.ControlPoint).ToList();
if (!Delete(toRemove))
return false;
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
foreach (var piece in Pieces)
piece.IsSelected.Value = false;
return true;
}
/// <summary>
/// Delete the specified <see cref="PathControlPoint"/>s.
/// </summary>
/// <returns>Whether any change actually took place.</returns>
public bool Delete(List<PathControlPoint> toRemove)
{
// Ensure that there are any points to be deleted
if (toRemove.Count == 0)
return false;
@@ -149,11 +165,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
changeHandler?.BeginChange();
RemoveControlPointsRequested?.Invoke(toRemove);
changeHandler?.EndChange();
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
foreach (var piece in Pieces)
piece.IsSelected.Value = false;
return true;
}
@@ -10,6 +10,7 @@ using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
@@ -76,9 +77,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
base.OnDragEnd(e);
}
protected override bool OnMouseDown(MouseDownEvent e) => true;
protected override bool OnMouseDown(MouseDownEvent e) => e.Button == MouseButton.Left;
protected override bool OnClick(ClickEvent e) => true;
protected override bool OnClick(ClickEvent e) => e.Button == MouseButton.Left;
private void updateState()
{
@@ -140,8 +140,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
if (hoveredControlPoint == null)
return false;
hoveredControlPoint.IsSelected.Value = true;
ControlPointVisualiser?.DeleteSelected();
if (hoveredControlPoint.IsSelected.Value)
ControlPointVisualiser?.DeleteSelected();
else
ControlPointVisualiser?.Delete([hoveredControlPoint.ControlPoint]);
return true;
}
+2 -2
View File
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Mods
/// <summary>
/// How early before a hitobject's start time to trigger a hit.
/// </summary>
private const float relax_leniency = 3;
public const float RELAX_LENIENCY = 12;
private bool isDownState;
private bool wasLeft;
@@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Mods
foreach (var h in playfield.HitObjectContainer.AliveObjects.OfType<DrawableOsuHitObject>())
{
// we are not yet close enough to the object.
if (time < h.HitObject.StartTime - relax_leniency)
if (time < h.HitObject.StartTime - RELAX_LENIENCY)
break;
// already hit or beyond the hittable end time.
@@ -177,6 +177,7 @@ namespace osu.Game.Tests.Visual.Editing
// bit of a hack to ensure this test can be ran multiple times without running into UNIQUE constraint failures
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = Guid.NewGuid().ToString());
AddStep("start playing track", () => InputManager.Key(Key.Space));
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
@@ -185,11 +186,13 @@ namespace osu.Game.Tests.Visual.Editing
InputManager.Click(MouseButton.Left);
});
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog);
AddAssert("track stopped", () => !Beatmap.Value.Track.IsRunning);
AddStep("save changes", () => DialogOverlay.CurrentDialog!.PerformOkAction());
EditorPlayer editorPlayer = null;
AddUntilStep("player pushed", () => (editorPlayer = Stack.CurrentScreen as EditorPlayer) != null);
AddUntilStep("track playing", () => Beatmap.Value.Track.IsRunning);
AddAssert("beatmap has 1 object", () => editorPlayer.Beatmap.Value.Beatmap.HitObjects.Count == 1);
AddUntilStep("wait for return to editor", () => Stack.CurrentScreen is Editor);
+18 -1
View File
@@ -96,8 +96,9 @@ namespace osu.Game.Database
/// 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.
/// 45 2024-12-23 Change beat snap divisor adjust defaults to be Ctrl+Scroll instead of Ctrl+Shift+Scroll, if not already changed by user.
/// 46 2024-12-26 Change beat snap divisor bindings to match stable directionality ¯\_(ツ)_/¯.
/// </summary>
private const int schema_version = 45;
private const int schema_version = 46;
/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
@@ -1222,6 +1223,22 @@ namespace osu.Game.Database
break;
}
case 46:
{
// Stable direction didn't match.
var keyBindings = migration.NewRealm.All<RealmKeyBinding>();
var nextBeatSnapBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.EditorCycleNextBeatSnapDivisor);
if (nextBeatSnapBinding != null && nextBeatSnapBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Control, InputKey.MouseWheelDown }))
migration.NewRealm.Remove(nextBeatSnapBinding);
var previousBeatSnapBinding = keyBindings.FirstOrDefault(k => k.ActionInt == (int)GlobalAction.EditorCyclePreviousBeatSnapDivisor);
if (previousBeatSnapBinding != null && previousBeatSnapBinding.KeyCombination.Keys.SequenceEqual(new[] { InputKey.Control, InputKey.MouseWheelUp }))
migration.NewRealm.Remove(previousBeatSnapBinding);
break;
}
}
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
@@ -142,8 +142,8 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.J }, GlobalAction.EditorFlipVertically),
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelDown }, GlobalAction.EditorDecreaseDistanceSpacing),
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelUp }, GlobalAction.EditorIncreaseDistanceSpacing),
new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelUp }, GlobalAction.EditorCyclePreviousBeatSnapDivisor),
new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelDown }, GlobalAction.EditorCycleNextBeatSnapDivisor),
new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelDown }, GlobalAction.EditorCyclePreviousBeatSnapDivisor),
new KeyBinding(new[] { InputKey.Control, InputKey.MouseWheelUp }, GlobalAction.EditorCycleNextBeatSnapDivisor),
new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl),
new KeyBinding(new[] { InputKey.Control, InputKey.E }, GlobalAction.EditorToggleScaleControl),
new KeyBinding(new[] { InputKey.Control, InputKey.Left }, GlobalAction.EditorSeekToPreviousHitObject),
+10 -9
View File
@@ -1428,24 +1428,25 @@ namespace osu.Game
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat)
return false;
if (introScreen == null) return false;
switch (e.Action)
{
case GlobalAction.DecreaseVolume:
case GlobalAction.IncreaseVolume:
return volume.Adjust(e.Action);
}
// All actions below this point don't allow key repeat.
if (e.Repeat)
return false;
// Wait until we're loaded at least to the intro before allowing various interactions.
if (introScreen == null) return false;
switch (e.Action)
{
case GlobalAction.ToggleMute:
case GlobalAction.NextVolumeMeter:
case GlobalAction.PreviousVolumeMeter:
if (e.Repeat)
return true;
return volume.Adjust(e.Action);
case GlobalAction.ToggleFPSDisplay:
@@ -121,9 +121,11 @@ namespace osu.Game.Overlays.Login
codeTextBox.Current.BindValueChanged(code =>
{
if (code.NewValue.Length == 8)
string trimmedCode = code.NewValue.Trim();
if (trimmedCode.Length == 8)
{
api.AuthenticateSecondFactor(code.NewValue);
api.AuthenticateSecondFactor(trimmedCode);
codeTextBox.Current.Disabled = true;
}
});
@@ -16,6 +16,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
@@ -131,7 +132,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void updateSamplePointContractedState()
{
const double minimum_gap = 28;
const double absolute_minimum_gap = 31; // assumes single letter bank name for default banks
double minimumGap = absolute_minimum_gap;
if (timeline == null || editorClock == null)
return;
@@ -153,9 +155,23 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (hitObject.GetEndTime() < editorClock.CurrentTime - timeline.VisibleRange / 2)
break;
foreach (var sample in hitObject.Samples)
{
if (!HitSampleInfo.AllBanks.Contains(sample.Bank))
minimumGap = Math.Max(minimumGap, absolute_minimum_gap + sample.Bank.Length * 3);
}
if (hitObject is IHasRepeats hasRepeats)
{
smallestTimeGap = Math.Min(smallestTimeGap, hasRepeats.Duration / hasRepeats.SpanCount() / 2);
foreach (var sample in hasRepeats.NodeSamples.SelectMany(s => s))
{
if (!HitSampleInfo.AllBanks.Contains(sample.Bank))
minimumGap = Math.Max(minimumGap, absolute_minimum_gap + sample.Bank.Length * 3);
}
}
double gap = lastTime - hitObject.GetEndTime();
// If the gap is less than 1ms, we can assume that the objects are stacked on top of each other
@@ -167,7 +183,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
double smallestAbsoluteGap = ((TimelineSelectionBlueprintContainer)SelectionBlueprints).ContentRelativeToAbsoluteFactor.X * smallestTimeGap;
SamplePointContracted.Value = smallestAbsoluteGap < minimum_gap;
SamplePointContracted.Value = smallestAbsoluteGap < minimumGap;
}
private readonly Stack<HitObject> currentConcurrentObjects = new Stack<HitObject>();
+2
View File
@@ -523,6 +523,8 @@ namespace osu.Game.Screens.Edit
public void TestGameplay()
{
clock.Stop();
if (HasUnsavedChanges)
{
dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() =>
+36 -19
View File
@@ -28,33 +28,31 @@ namespace osu.Game.Screens.Edit.Setup
public override LocalisableString Title => EditorSetupStrings.MetadataHeader;
[BackgroundDependencyLoader]
private void load()
private void load(SetupScreen? setupScreen)
{
var metadata = Beatmap.Metadata;
Children = new[]
{
ArtistTextBox = createTextBox<FormTextBox>(EditorSetupStrings.Artist,
!string.IsNullOrEmpty(metadata.ArtistUnicode) ? metadata.ArtistUnicode : metadata.Artist),
RomanisedArtistTextBox = createTextBox<FormRomanisedTextBox>(EditorSetupStrings.RomanisedArtist,
!string.IsNullOrEmpty(metadata.Artist) ? metadata.Artist : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)),
TitleTextBox = createTextBox<FormTextBox>(EditorSetupStrings.Title,
!string.IsNullOrEmpty(metadata.TitleUnicode) ? metadata.TitleUnicode : metadata.Title),
RomanisedTitleTextBox = createTextBox<FormRomanisedTextBox>(EditorSetupStrings.RomanisedTitle,
!string.IsNullOrEmpty(metadata.Title) ? metadata.Title : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)),
creatorTextBox = createTextBox<FormTextBox>(EditorSetupStrings.Creator, metadata.Author.Username),
difficultyTextBox = createTextBox<FormTextBox>(EditorSetupStrings.DifficultyName, Beatmap.BeatmapInfo.DifficultyName),
sourceTextBox = createTextBox<FormTextBox>(BeatmapsetsStrings.ShowInfoSource, metadata.Source),
tagsTextBox = createTextBox<FormTextBox>(BeatmapsetsStrings.ShowInfoTags, metadata.Tags)
ArtistTextBox = createTextBox<FormTextBox>(EditorSetupStrings.Artist),
RomanisedArtistTextBox = createTextBox<FormRomanisedTextBox>(EditorSetupStrings.RomanisedArtist),
TitleTextBox = createTextBox<FormTextBox>(EditorSetupStrings.Title),
RomanisedTitleTextBox = createTextBox<FormRomanisedTextBox>(EditorSetupStrings.RomanisedTitle),
creatorTextBox = createTextBox<FormTextBox>(EditorSetupStrings.Creator),
difficultyTextBox = createTextBox<FormTextBox>(EditorSetupStrings.DifficultyName),
sourceTextBox = createTextBox<FormTextBox>(BeatmapsetsStrings.ShowInfoSource),
tagsTextBox = createTextBox<FormTextBox>(BeatmapsetsStrings.ShowInfoTags)
};
if (setupScreen != null)
setupScreen.MetadataChanged += reloadMetadata;
reloadMetadata();
}
private TTextBox createTextBox<TTextBox>(LocalisableString label, string initialValue)
private TTextBox createTextBox<TTextBox>(LocalisableString label)
where TTextBox : FormTextBox, new()
=> new TTextBox
{
Caption = label,
Current = { Value = initialValue },
TabbableContentContainer = this
};
@@ -94,10 +92,29 @@ namespace osu.Game.Screens.Edit.Setup
// for now, update on commit rather than making BeatmapMetadata bindables.
// after switching database engines we can reconsider if switching to bindables is a good direction.
updateMetadata();
setMetadata();
}
private void updateMetadata()
private void reloadMetadata()
{
var metadata = Beatmap.Metadata;
RomanisedArtistTextBox.ReadOnly = false;
RomanisedTitleTextBox.ReadOnly = false;
ArtistTextBox.Current.Value = !string.IsNullOrEmpty(metadata.ArtistUnicode) ? metadata.ArtistUnicode : metadata.Artist;
RomanisedArtistTextBox.Current.Value = !string.IsNullOrEmpty(metadata.Artist) ? metadata.Artist : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode);
TitleTextBox.Current.Value = !string.IsNullOrEmpty(metadata.TitleUnicode) ? metadata.TitleUnicode : metadata.Title;
RomanisedTitleTextBox.Current.Value = !string.IsNullOrEmpty(metadata.Title) ? metadata.Title : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode);
creatorTextBox.Current.Value = metadata.Author.Username;
difficultyTextBox.Current.Value = Beatmap.BeatmapInfo.DifficultyName;
sourceTextBox.Current.Value = metadata.Source;
tagsTextBox.Current.Value = metadata.Tags;
updateReadOnlyState();
}
private void setMetadata()
{
Beatmap.Metadata.ArtistUnicode = ArtistTextBox.Current.Value;
Beatmap.Metadata.Artist = RomanisedArtistTextBox.Current.Value;
@@ -35,6 +35,9 @@ namespace osu.Game.Screens.Edit.Setup
[Resolved]
private Editor? editor { get; set; }
[Resolved]
private SetupScreen setupScreen { get; set; } = null!;
private SetupScreenHeaderBackground headerBackground = null!;
[BackgroundDependencyLoader]
@@ -93,15 +96,37 @@ namespace osu.Game.Screens.Edit.Setup
if (!source.Exists)
return false;
var tagSource = TagLib.File.Create(source.FullName);
changeResource(source, applyToAllDifficulties, @"audio",
metadata => metadata.AudioFile,
(metadata, name) => metadata.AudioFile = name);
(metadata, name) =>
{
metadata.AudioFile = name;
string artist = tagSource.Tag.JoinedAlbumArtists;
if (!string.IsNullOrWhiteSpace(artist))
{
metadata.ArtistUnicode = artist;
metadata.Artist = MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode);
}
string title = tagSource.Tag.Title;
if (!string.IsNullOrEmpty(title))
{
metadata.TitleUnicode = title;
metadata.Title = MetadataUtils.StripNonRomanisedCharacters(metadata.TitleUnicode);
}
});
music.ReloadCurrentTrack();
setupScreen.MetadataChanged?.Invoke();
return true;
}
private void changeResource(FileInfo source, bool applyToAllDifficulties, string baseFilename, Func<BeatmapMetadata, string> readFilename, Action<BeatmapMetadata, string> writeFilename)
private void changeResource(FileInfo source, bool applyToAllDifficulties, string baseFilename, Func<BeatmapMetadata, string> readFilename, Action<BeatmapMetadata, string> writeMetadata)
{
var set = working.Value.BeatmapSetInfo;
var beatmap = working.Value.BeatmapInfo;
@@ -148,10 +173,7 @@ namespace osu.Game.Screens.Edit.Setup
{
foreach (var b in otherBeatmaps)
{
// This operation is quite expensive, so only perform it if required.
if (readFilename(b.Metadata) == newFilename) continue;
writeFilename(b.Metadata, newFilename);
writeMetadata(b.Metadata, newFilename);
// save the difficulty to re-encode the .osu file, updating any reference of the old filename.
//
@@ -162,7 +184,7 @@ namespace osu.Game.Screens.Edit.Setup
}
}
writeFilename(beatmap.Metadata, newFilename);
writeMetadata(beatmap.Metadata, newFilename);
// editor change handler cannot be aware of any file changes or other difficulties having their metadata modified.
// for simplicity's sake, trigger a save when changing any resource to ensure the change is correctly saved.
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -13,12 +14,15 @@ using osuTK;
namespace osu.Game.Screens.Edit.Setup
{
[Cached]
public partial class SetupScreen : EditorScreen
{
public const float COLUMN_WIDTH = 450;
public const float SPACING = 28;
public const float MAX_WIDTH = 2 * COLUMN_WIDTH + SPACING;
public Action? MetadataChanged { get; set; }
public SetupScreen()
: base(EditorScreenMode.SongSetup)
{
@@ -12,6 +12,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Framework.Timing;
using osu.Framework.Utils;
@@ -41,6 +42,9 @@ namespace osu.Game.Screens.Edit.Timing
[Resolved]
private OverlayColourProvider overlayColourProvider { get; set; } = null!;
[Resolved]
private BindableBeatDivisor beatDivisor { get; set; } = null!;
public bool EnableClicking
{
get => metronomeTick.EnableClicking;
@@ -222,7 +226,7 @@ namespace osu.Game.Screens.Edit.Timing
Clock = new FramedClock(metronomeClock = new StopwatchClock(true));
}
private double beatLength;
private double effectiveBeatLength;
private TimingControlPoint timingPoint = null!;
@@ -232,11 +236,26 @@ namespace osu.Game.Screens.Edit.Timing
private ScheduledDelegate? latchDelegate;
private bool spedUp;
private int computeSpedUpDivisor()
{
if (!spedUp)
return 1;
if (beatDivisor.Value % 3 == 0)
return 3;
if (beatDivisor.Value % 2 == 0)
return 2;
return 1;
}
protected override void LoadComplete()
{
base.LoadComplete();
interpolatedBpm.BindValueChanged(bpm => bpmText.Text = bpm.NewValue.ToLocalisableString());
interpolatedBpm.BindValueChanged(_ => bpmText.Text = interpolatedBpm.Value.ToLocalisableString());
}
protected override void Update()
@@ -250,16 +269,20 @@ namespace osu.Game.Screens.Edit.Timing
timingPoint = BeatSyncSource.ControlPoints.TimingPointAt(BeatSyncSource.Clock.CurrentTime);
if (beatLength != timingPoint.BeatLength)
Divisor = metronomeTick.Divisor = computeSpedUpDivisor();
if (effectiveBeatLength != timingPoint.BeatLength / Divisor)
{
beatLength = timingPoint.BeatLength;
effectiveBeatLength = timingPoint.BeatLength / Divisor;
EarlyActivationMilliseconds = timingPoint.BeatLength / 2;
float bpmRatio = (float)Interpolation.ApplyEasing(Easing.OutQuad, Math.Clamp((timingPoint.BPM - 30) / 480, 0, 1));
double effectiveBpm = 60000 / effectiveBeatLength;
float bpmRatio = (float)Interpolation.ApplyEasing(Easing.OutQuad, Math.Clamp((effectiveBpm - 30) / 480, 0, 1));
weight.MoveToY((float)Interpolation.Lerp(0.1f, 0.83f, bpmRatio), 600, Easing.OutQuint);
this.TransformBindableTo(interpolatedBpm, (int)Math.Round(timingPoint.BPM), 600, Easing.OutQuint);
this.TransformBindableTo(interpolatedBpm, (int)Math.Round(effectiveBpm), 600, Easing.OutQuint);
}
if (!BeatSyncSource.Clock.IsRunning && isSwinging)
@@ -305,7 +328,7 @@ namespace osu.Game.Screens.Edit.Timing
float currentAngle = swing.Rotation;
float targetAngle = currentAngle > 0 ? -angle : angle;
swing.RotateTo(targetAngle, beatLength, Easing.InOutQuad);
swing.RotateTo(targetAngle, effectiveBeatLength, Easing.InOutQuad);
}
private void onTickPlayed()
@@ -313,9 +336,25 @@ namespace osu.Game.Screens.Edit.Timing
// Originally, this flash only occurred when the pendulum correctly passess the centre.
// Mappers weren't happy with the metronome tick not playing immediately after starting playback
// so now this matches the actual tick sample.
stick.FlashColour(overlayColourProvider.Content1, beatLength, Easing.OutQuint);
stick.FlashColour(overlayColourProvider.Content1, effectiveBeatLength, Easing.OutQuint);
}
protected override bool OnKeyDown(KeyDownEvent e)
{
updateDivisorFromKey(e);
return base.OnKeyDown(e);
}
protected override void OnKeyUp(KeyUpEvent e)
{
base.OnKeyUp(e);
updateDivisorFromKey(e);
}
private void updateDivisorFromKey(UIEvent e) => spedUp = e.ControlPressed;
private partial class MetronomeTick : BeatSyncedContainer
{
public bool EnableClicking;
+1 -1
View File
@@ -11,7 +11,7 @@ namespace osu.Game.Seasonal
/// </summary>
public static class SeasonalUIConfig
{
public static readonly bool ENABLED = true;
public static readonly bool ENABLED = false;
public static readonly Color4 PRIMARY_COLOUR_1 = Color4Extensions.FromHex(@"D32F2F");