1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-18 05:42:56 +08:00

Merge branch 'master' into mania-hp-bar-position

This commit is contained in:
Dean Herbert 2025-01-02 14:33:55 +09:00 committed by GitHub
commit 216c9040c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 567 additions and 78 deletions

View File

@ -14,7 +14,16 @@ namespace osu.Game.Rulesets.EmptyFreeform.Objects
public Vector2 Position { get; set; }
public float X => Position.X;
public float Y => Position.Y;
public float X
{
get => Position.X;
set => Position = new Vector2(value, Y);
}
public float Y
{
get => Position.Y;
set => Position = new Vector2(X, value);
}
}
}

View File

@ -14,7 +14,16 @@ namespace osu.Game.Rulesets.Pippidon.Objects
public Vector2 Position { get; set; }
public float X => Position.X;
public float Y => Position.Y;
public float X
{
get => Position.X;
set => Position = new Vector2(value, Y);
}
public float Y
{
get => Position.Y;
set => Position = new Vector2(X, value);
}
}
}

View File

@ -210,11 +210,27 @@ namespace osu.Game.Rulesets.Catch.Objects
/// </summary>
public float LegacyConvertedY { get; set; } = DEFAULT_LEGACY_CONVERT_Y;
float IHasXPosition.X => OriginalX;
float IHasXPosition.X
{
get => OriginalX;
set => OriginalX = value;
}
float IHasYPosition.Y => LegacyConvertedY;
float IHasYPosition.Y
{
get => LegacyConvertedY;
set => LegacyConvertedY = value;
}
Vector2 IHasPosition.Position => new Vector2(OriginalX, LegacyConvertedY);
Vector2 IHasPosition.Position
{
get => new Vector2(OriginalX, LegacyConvertedY);
set
{
((IHasXPosition)this).X = value.X;
((IHasYPosition)this).Y = value.Y;
}
}
#endregion
}

View File

@ -25,7 +25,11 @@ namespace osu.Game.Rulesets.Mania.Objects
#region LegacyBeatmapEncoder
float IHasXPosition.X => Column;
float IHasXPosition.X
{
get => Column;
set => Column = (int)value;
}
#endregion
}

View File

@ -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();

View File

@ -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);
}
}
}
}

View File

@ -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;
}

View File

@ -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()
{

View File

@ -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;
}

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.

View File

@ -59,8 +59,17 @@ namespace osu.Game.Rulesets.Osu.Objects
set => position.Value = value;
}
public float X => Position.X;
public float Y => Position.Y;
public float X
{
get => Position.X;
set => Position = new Vector2(value, Position.Y);
}
public float Y
{
get => Position.Y;
set => Position = new Vector2(Position.X, value);
}
public Vector2 StackedPosition => Position + StackOffset;

View File

@ -11,6 +11,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO.Archives;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual;
using MemoryStream = System.IO.MemoryStream;
@ -50,6 +51,29 @@ namespace osu.Game.Tests.Beatmaps.IO
AddAssert("hit object is snapped", () => beatmap.Beatmap.HitObjects[0].StartTime, () => Is.EqualTo(28519).Within(0.001));
}
[Test]
public void TestFractionalObjectCoordinatesRounded()
{
IWorkingBeatmap beatmap = null!;
MemoryStream outStream = null!;
// Ensure importer encoding is correct
AddStep("import beatmap", () => beatmap = importBeatmapFromArchives(@"fractional-coordinates.olz"));
AddAssert("hit object has fractional position", () => ((IHasYPosition)beatmap.Beatmap.HitObjects[1]).Y, () => Is.EqualTo(383.99997).Within(0.00001));
// Ensure exporter legacy conversion is correct
AddStep("export", () =>
{
outStream = new MemoryStream();
new LegacyBeatmapExporter(LocalStorage)
.ExportToStream((BeatmapSetInfo)beatmap.BeatmapInfo.BeatmapSet!, outStream, null);
});
AddStep("import beatmap again", () => beatmap = importBeatmapFromStream(outStream));
AddAssert("hit object is snapped", () => ((IHasYPosition)beatmap.Beatmap.HitObjects[1]).Y, () => Is.EqualTo(384).Within(0.00001));
}
[Test]
public void TestExportStability()
{

View File

@ -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);

View File

@ -42,7 +42,10 @@ namespace osu.Game.Database
return null;
using var contentStreamReader = new LineBufferedReader(contentStream);
var beatmapContent = new LegacyBeatmapDecoder().Decode(contentStreamReader);
// FIRST_LAZER_VERSION is specified here to avoid flooring object coordinates on decode via `(int)` casts.
// we will be making integers out of them lower down, but in a slightly different manner (rounding rather than truncating)
var beatmapContent = new LegacyBeatmapDecoder(LegacyBeatmapEncoder.FIRST_LAZER_VERSION).Decode(contentStreamReader);
var workingBeatmap = new FlatWorkingBeatmap(beatmapContent);
var playableBeatmap = workingBeatmap.GetPlayableBeatmap(beatmapInfo.Ruleset);
@ -93,6 +96,12 @@ namespace osu.Game.Database
hitObject.StartTime = Math.Floor(hitObject.StartTime);
if (hitObject is IHasXPosition hasXPosition)
hasXPosition.X = MathF.Round(hasXPosition.X);
if (hitObject is IHasYPosition hasYPosition)
hasYPosition.Y = MathF.Round(hasYPosition.Y);
if (hitObject is not IHasPath hasPath) continue;
// stable's hit object parsing expects the entire slider to use only one type of curve,

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");

View File

@ -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),

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:

View File

@ -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;
}
});

View File

@ -21,9 +21,17 @@ namespace osu.Game.Rulesets.Objects.Legacy
public int ComboOffset { get; set; }
public float X => Position.X;
public float X
{
get => Position.X;
set => Position = new Vector2(value, Position.Y);
}
public float Y => Position.Y;
public float Y
{
get => Position.Y;
set => Position = new Vector2(Position.X, value);
}
public Vector2 Position { get; set; }

View File

@ -13,6 +13,6 @@ namespace osu.Game.Rulesets.Objects.Types
/// <summary>
/// The starting position of the HitObject.
/// </summary>
Vector2 Position { get; }
Vector2 Position { get; set; }
}
}

View File

@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Objects.Types
/// <summary>
/// The starting X-position of this HitObject.
/// </summary>
float X { get; }
float X { get; set; }
}
}

View File

@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Objects.Types
/// <summary>
/// The starting Y-position of this HitObject.
/// </summary>
float Y { get; }
float Y { get; set; }
}
}

View File

@ -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>();

View File

@ -523,6 +523,8 @@ namespace osu.Game.Screens.Edit
public void TestGameplay()
{
clock.Stop();
if (HasUnsavedChanges)
{
dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() =>

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;

View File

@ -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.

View File

@ -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)
{

View File

@ -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;

View File

@ -157,5 +157,9 @@
<string>public.app-category.music-games</string>
<key>LSSupportsOpeningDocumentsInPlace</key>
<false/>
<!-- Game mode is supposed to be automatically enabled by the app category specification above,
but for some reason it needs to be explicitly enabled with this key. -->
<key>GCSupportsGameMode</key>
<true/>
</dict>
</plist>