1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 08:27:49 +08:00

Merge branch 'master' into fix-slider-reversing

This commit is contained in:
Bartłomiej Dach 2023-08-21 09:29:46 +02:00 committed by GitHub
commit 1d657a8844
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 558 additions and 65 deletions

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.815.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.817.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="sh.ppy.osulazer" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" />
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!" android:icon="@drawable/lazer" />
<!-- for editor usage -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
</manifest>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- using a different name because package name cannot contain 'catch' -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Catch_Tests.Android" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" />
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!catch Test" />
</manifest>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Mania.Tests.Android" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" />
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!mania Test" />
</manifest>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Osu.Tests.Android" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" />
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!standard Test" />
</manifest>

View File

@ -9,6 +9,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
@ -70,12 +71,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
base.Content.Children = new Drawable[]
{
editorClock = new EditorClock(editorBeatmap),
snapProvider,
new PopoverContainer { Child = snapProvider },
Content
};
}
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
protected override Container<Drawable> Content { get; } = new PopoverContainer { RelativeSizeAxes = Axes.Both };
[SetUp]
public void Setup() => Schedule(() =>

View File

@ -0,0 +1,95 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Components.RadioButtons;
using osu.Game.Tests.Beatmaps;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public partial class TestScenePreciseRotation : TestSceneOsuEditor
{
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset);
[Test]
public void TestHotkeyHandling()
{
AddStep("select single circle", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType<HitCircle>().First()));
AddStep("press rotate hotkey", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("no popover present", () => this.ChildrenOfType<PreciseRotationPopover>().Count(), () => Is.Zero);
AddStep("select first three objects", () =>
{
EditorBeatmap.SelectedHitObjects.Clear();
EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects.Take(3));
});
AddStep("press rotate hotkey", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("popover present", () => this.ChildrenOfType<PreciseRotationPopover>().Count(), () => Is.EqualTo(1));
AddStep("press rotate hotkey", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("no popover present", () => this.ChildrenOfType<PreciseRotationPopover>().Count(), () => Is.Zero);
}
[Test]
public void TestRotateCorrectness()
{
AddStep("replace objects", () =>
{
EditorBeatmap.Clear();
EditorBeatmap.AddRange(new HitObject[]
{
new HitCircle { Position = new Vector2(100) },
new HitCircle { Position = new Vector2(200) },
});
});
AddStep("select both circles", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects));
AddStep("press rotate hotkey", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("popover present", getPopover, () => Is.Not.Null);
AddStep("rotate by 180deg", () => getPopover().ChildrenOfType<TextBox>().Single().Current.Value = "180");
AddAssert("first object rotated 180deg around playfield centre",
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(0).Position,
() => Is.EqualTo(OsuPlayfield.BASE_SIZE - new Vector2(100)));
AddAssert("second object rotated 180deg around playfield centre",
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(1).Position,
() => Is.EqualTo(OsuPlayfield.BASE_SIZE - new Vector2(200)));
AddStep("change rotation origin", () => getPopover().ChildrenOfType<EditorRadioButton>().ElementAt(1).TriggerClick());
AddAssert("first object rotated 90deg around selection centre",
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(0).Position, () => Is.EqualTo(new Vector2(200, 200)));
AddAssert("second object rotated 90deg around selection centre",
() => EditorBeatmap.HitObjects.OfType<HitCircle>().ElementAt(1).Position, () => Is.EqualTo(new Vector2(100, 100)));
PreciseRotationPopover? getPopover() => this.ChildrenOfType<PreciseRotationPopover>().SingleOrDefault();
}
}
}

View File

@ -85,6 +85,11 @@ namespace osu.Game.Rulesets.Osu.Edit
// we may be entering the screen with a selection already active
updateDistanceSnapGrid();
RightToolbox.Add(new TransformToolboxGroup
{
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler
});
}
protected override ComposeBlueprintContainer CreateBlueprintContainer()

View File

@ -0,0 +1,107 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Components.RadioButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class PreciseRotationPopover : OsuPopover
{
private readonly SelectionRotationHandler rotationHandler;
private readonly Bindable<PreciseRotationInfo> rotationInfo = new Bindable<PreciseRotationInfo>(new PreciseRotationInfo(0, RotationOrigin.PlayfieldCentre));
private SliderWithTextBoxInput<float> angleInput = null!;
private EditorRadioButtonCollection rotationOrigin = null!;
public PreciseRotationPopover(SelectionRotationHandler rotationHandler)
{
this.rotationHandler = rotationHandler;
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
}
[BackgroundDependencyLoader]
private void load()
{
Child = new FillFlowContainer
{
Width = 220,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(20),
Children = new Drawable[]
{
angleInput = new SliderWithTextBoxInput<float>("Angle (degrees):")
{
Current = new BindableNumber<float>
{
MinValue = -360,
MaxValue = 360,
Precision = 1
},
Instantaneous = true
},
rotationOrigin = new EditorRadioButtonCollection
{
RelativeSizeAxes = Axes.X,
Items = new[]
{
new RadioButton("Playfield centre",
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre },
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
new RadioButton("Selection centre",
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.SelectionCentre },
() => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare })
}
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
ScheduleAfterChildren(() => angleInput.TakeFocus());
angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue });
rotationOrigin.Items.First().Select();
rotationInfo.BindValueChanged(rotation =>
{
rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null);
});
}
protected override void PopIn()
{
base.PopIn();
rotationHandler.Begin();
}
protected override void PopOut()
{
base.PopOut();
if (IsLoaded)
rotationHandler.Commit();
}
}
public enum RotationOrigin
{
PlayfieldCentre,
SelectionCentre
}
public record PreciseRotationInfo(float Degrees, RotationOrigin Origin);
}

View File

@ -0,0 +1,80 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class TransformToolboxGroup : EditorToolboxGroup, IKeyBindingHandler<GlobalAction>
{
private readonly Bindable<bool> canRotate = new BindableBool();
private EditorToolButton rotateButton = null!;
public SelectionRotationHandler RotationHandler { get; init; } = null!;
public TransformToolboxGroup()
: base("transform")
{
}
[BackgroundDependencyLoader]
private void load()
{
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(5),
Children = new Drawable[]
{
rotateButton = new EditorToolButton("Rotate",
() => new SpriteIcon { Icon = FontAwesome.Solid.Undo },
() => new PreciseRotationPopover(RotationHandler)),
// TODO: scale
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
// bindings to `Enabled` on the buttons are decoupled on purpose
// due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set.
canRotate.BindTo(RotationHandler.CanRotate);
canRotate.BindValueChanged(_ => rotateButton.Enabled.Value = canRotate.Value, true);
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat) return false;
switch (e.Action)
{
case GlobalAction.EditorToggleRotateControl:
{
rotateButton.TriggerClick();
return true;
}
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
}
}

View File

@ -257,7 +257,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
texture.Bind();
for (int i = 0; i < points.Count; i++)
drawPointQuad(points[i], textureRect, i + firstVisiblePointIndex);
drawPointQuad(renderer, points[i], textureRect, i + firstVisiblePointIndex);
UnbindTextureShader(renderer);
renderer.PopLocalMatrix();
@ -325,7 +325,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
private float getRotation(int index) => max_rotation * (StatelessRNG.NextSingle(rotationSeed, index) * 2 - 1);
private void drawPointQuad(SmokePoint point, RectangleF textureRect, int index)
private void drawPointQuad(IRenderer renderer, SmokePoint point, RectangleF textureRect, int index)
{
Debug.Assert(quadBatch != null);
@ -347,25 +347,25 @@ namespace osu.Game.Rulesets.Osu.Skinning
var localBotLeft = point.Position + ortho - dir;
var localBotRight = point.Position + ortho + dir;
quadBatch.Add(new TexturedVertex2D
quadBatch.Add(new TexturedVertex2D(renderer)
{
Position = localTopLeft,
TexturePosition = textureRect.TopLeft,
Colour = Color4Extensions.Multiply(ColourAtPosition(localTopLeft), colour),
});
quadBatch.Add(new TexturedVertex2D
quadBatch.Add(new TexturedVertex2D(renderer)
{
Position = localTopRight,
TexturePosition = textureRect.TopRight,
Colour = Color4Extensions.Multiply(ColourAtPosition(localTopRight), colour),
});
quadBatch.Add(new TexturedVertex2D
quadBatch.Add(new TexturedVertex2D(renderer)
{
Position = localBotRight,
TexturePosition = textureRect.BottomRight,
Colour = Color4Extensions.Multiply(ColourAtPosition(localBotRight), colour),
});
quadBatch.Add(new TexturedVertex2D
quadBatch.Add(new TexturedVertex2D(renderer)
{
Position = localBotLeft,
TexturePosition = textureRect.BottomLeft,

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Rulesets.Taiko.Tests.Android" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" />
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!taiko Test" />
</manifest>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="osu.Game.Tests.Android" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" />
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!visual Test" />
</manifest>

View File

@ -1,19 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Database
{
[TestFixture]
public class LegacyBeatmapImporterTest
public class LegacyBeatmapImporterTest : RealmTest
{
private readonly TestLegacyBeatmapImporter importer = new TestLegacyBeatmapImporter();
@ -60,6 +64,33 @@ namespace osu.Game.Tests.Database
}
}
[Test]
public void TestStableDateAddedApplied()
{
RunTestWithRealmAsync(async (realm, storage) =>
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
using (var tmpStorage = new TemporaryNativeStorage("stable-songs-folder"))
{
var stableStorage = new StableStorage(tmpStorage.GetFullPath(""), host);
var songsStorage = stableStorage.GetStorageForDirectory(StableStorage.STABLE_DEFAULT_SONGS_PATH);
ZipFile.ExtractToDirectory(TestResources.GetQuickTestBeatmapForImport(), songsStorage.GetFullPath("renatus"));
string[] beatmaps = Directory.GetFiles(songsStorage.GetFullPath("renatus"), "*.osu", SearchOption.TopDirectoryOnly);
File.SetLastWriteTimeUtc(beatmaps[beatmaps.Length / 2], new DateTime(2000, 1, 1, 12, 0, 0));
await new LegacyBeatmapImporter(new BeatmapImporter(storage, realm)).ImportFromStableAsync(stableStorage);
var importedSet = realm.Realm.All<BeatmapSetInfo>().Single();
Assert.NotNull(importedSet);
Assert.AreEqual(new DateTimeOffset(new DateTime(2000, 1, 1, 12, 0, 0, DateTimeKind.Utc)), importedSet.DateAdded);
}
});
}
private class TestLegacyBeatmapImporter : LegacyBeatmapImporter
{
public TestLegacyBeatmapImporter()

View File

@ -6,6 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
@ -29,7 +30,7 @@ namespace osu.Game.Tests.Editing
[Cached(typeof(IBeatSnapProvider))]
private readonly EditorBeatmap editorBeatmap;
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
protected override Container<Drawable> Content { get; } = new PopoverContainer { RelativeSizeAxes = Axes.Both };
public TestSceneHitObjectComposerDistanceSnapping()
{

View File

@ -8,7 +8,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@ -178,7 +178,7 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("distance spacing increased by 0.5", () => editorBeatmap.BeatmapInfo.DistanceSpacing == originalSpacing + 0.5);
}
public partial class EditorBeatmapContainer : Container
public partial class EditorBeatmapContainer : PopoverContainer
{
private readonly IWorkingBeatmap working;

View File

@ -198,14 +198,17 @@ namespace osu.Game.Tests.Visual.Gameplay
double[] distances = { 100d, 200d, 300d };
AddStep("create path", () => path.ControlPoints.AddRange(positions.Select(p => new PathControlPoint(p, PathType.Linear))));
AddAssert("segment ends are correct", () => path.GetSegmentEnds().SequenceEqual(distances.Select(d => d / 300)));
AddAssert("segment end positions recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)).SequenceEqual(positions.Skip(1)));
AddAssert("segment ends are correct", () => path.GetSegmentEnds(), () => Is.EqualTo(distances.Select(d => d / 300)));
AddAssert("segment end positions recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(positions.Skip(1)));
AddStep("lengthen last segment", () => path.ExpectedDistance.Value = 400);
AddAssert("segment ends are correct", () => path.GetSegmentEnds().SequenceEqual(distances.Select(d => d / 400)));
AddAssert("segment end positions recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)).SequenceEqual(positions.Skip(1)));
AddAssert("segment ends are correct", () => path.GetSegmentEnds(), () => Is.EqualTo(distances.Select(d => d / 400)));
AddAssert("segment end positions recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(positions.Skip(1)));
AddStep("shorten last segment", () => path.ExpectedDistance.Value = 150);
AddAssert("segment ends are correct", () => path.GetSegmentEnds().SequenceEqual(distances.Select(d => d / 150)));
AddAssert("segment end positions not recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)).SequenceEqual(new[]
AddAssert("segment ends are correct", () => path.GetSegmentEnds(), () => Is.EqualTo(distances.Select(d => d / 150)));
// see remarks in `GetSegmentEnds()` xmldoc (`SliderPath.PositionAt()` clamps progress to [0,1]).
AddAssert("segment end positions not recovered", () => path.GetSegmentEnds().Select(p => path.PositionAt(p)), () => Is.EqualTo(new[]
{
positions[1],
new Vector2(100, 50),

View File

@ -152,6 +152,8 @@ namespace osu.Game.Beatmaps
if (archive != null)
beatmapSet.Beatmaps.AddRange(createBeatmapDifficulties(beatmapSet, realm));
beatmapSet.DateAdded = getDateAdded(archive);
foreach (BeatmapInfo b in beatmapSet.Beatmaps)
{
b.BeatmapSet = beatmapSet;
@ -305,11 +307,36 @@ namespace osu.Game.Beatmaps
return new BeatmapSetInfo
{
OnlineID = beatmap.BeatmapInfo.BeatmapSet?.OnlineID ?? -1,
// Metadata = beatmap.Metadata,
DateAdded = DateTimeOffset.UtcNow
};
}
/// <summary>
/// Determine the date a given beatmapset has been added to the game.
/// For legacy imports, we can use the oldest file write time for any `.osu` file in the directory.
/// For any other import types, use "now".
/// </summary>
private DateTimeOffset getDateAdded(ArchiveReader? reader)
{
DateTimeOffset dateAdded = DateTimeOffset.UtcNow;
if (reader is LegacyDirectoryArchiveReader legacyReader)
{
var beatmaps = reader.Filenames.Where(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase));
dateAdded = File.GetLastWriteTimeUtc(legacyReader.GetFullPath(beatmaps.First()));
foreach (string beatmapName in beatmaps)
{
var currentDateAdded = File.GetLastWriteTimeUtc(legacyReader.GetFullPath(beatmapName));
if (currentDateAdded < dateAdded)
dateAdded = currentDateAdded;
}
}
return dateAdded;
}
/// <summary>
/// Create all required <see cref="BeatmapInfo"/>s for the provided archive.
/// </summary>

View File

@ -1,13 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using JetBrains.Annotations;
using osu.Game.IO;
using osu.Game.Rulesets;
@ -45,7 +42,7 @@ namespace osu.Game.Beatmaps.Formats
/// Register dependencies for use with static decoder classes.
/// </summary>
/// <param name="rulesets">A store containing all available rulesets (used by <see cref="LegacyBeatmapDecoder"/>).</param>
public static void RegisterDependencies([NotNull] RulesetStore rulesets)
public static void RegisterDependencies(RulesetStore rulesets)
{
LegacyBeatmapDecoder.RulesetStore = rulesets ?? throw new ArgumentNullException(nameof(rulesets));
}
@ -63,7 +60,7 @@ namespace osu.Game.Beatmaps.Formats
throw new IOException(@"Unknown decoder type");
// start off with the first line of the file
string line = stream.PeekLine()?.Trim();
string? line = stream.PeekLine()?.Trim();
while (line != null && line.Length == 0)
{

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic;
using osuTK.Graphics;
@ -13,7 +11,7 @@ namespace osu.Game.Beatmaps.Formats
/// <summary>
/// Retrieves the list of combo colours for presentation only.
/// </summary>
IReadOnlyList<Color4> ComboColours { get; }
IReadOnlyList<Color4>? ComboColours { get; }
/// <summary>
/// The list of custom combo colours.

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
#pragma warning disable 618
using System;
@ -36,11 +34,11 @@ namespace osu.Game.Beatmaps.Formats
/// </summary>
private const double control_point_leniency = 1;
internal static RulesetStore RulesetStore;
internal static RulesetStore? RulesetStore;
private Beatmap beatmap;
private Beatmap beatmap = null!;
private ConvertHitObjectParser parser;
private ConvertHitObjectParser? parser;
private LegacySampleBank defaultSampleBank;
private int defaultSampleVolume = 100;
@ -222,7 +220,7 @@ namespace osu.Game.Beatmaps.Formats
case @"Mode":
int rulesetID = Parsing.ParseInt(pair.Value);
beatmap.BeatmapInfo.Ruleset = RulesetStore.GetRuleset(rulesetID) ?? throw new ArgumentException("Ruleset is not available locally.");
beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(rulesetID) ?? throw new ArgumentException("Ruleset is not available locally.");
switch (rulesetID)
{

View File

@ -1,15 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using JetBrains.Annotations;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy;
@ -34,8 +31,7 @@ namespace osu.Game.Beatmaps.Formats
private readonly IBeatmap beatmap;
[CanBeNull]
private readonly ISkin skin;
private readonly ISkin? skin;
private readonly int onlineRulesetID;
@ -44,7 +40,7 @@ namespace osu.Game.Beatmaps.Formats
/// </summary>
/// <param name="beatmap">The beatmap to encode.</param>
/// <param name="skin">The beatmap's skin, used for encoding combo colours.</param>
public LegacyBeatmapEncoder(IBeatmap beatmap, [CanBeNull] ISkin skin)
public LegacyBeatmapEncoder(IBeatmap beatmap, ISkin? skin)
{
this.beatmap = beatmap;
this.skin = skin;
@ -180,8 +176,8 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine("[TimingPoints]");
SampleControlPoint lastRelevantSamplePoint = null;
DifficultyControlPoint lastRelevantDifficultyPoint = null;
SampleControlPoint? lastRelevantSamplePoint = null;
DifficultyControlPoint? lastRelevantDifficultyPoint = null;
// In osu!taiko and osu!mania, a scroll speed is stored as "slider velocity" in legacy formats.
// In that case, a scrolling speed change is a global effect and per-hit object difficulty control points are ignored.
@ -585,7 +581,7 @@ namespace osu.Game.Beatmaps.Formats
return type;
}
private LegacySampleBank toLegacySampleBank(string sampleBank)
private LegacySampleBank toLegacySampleBank(string? sampleBank)
{
switch (sampleBank?.ToLowerInvariant())
{
@ -603,7 +599,7 @@ namespace osu.Game.Beatmaps.Formats
}
}
private int toLegacyCustomSampleBank(HitSampleInfo hitSampleInfo)
private int toLegacyCustomSampleBank(HitSampleInfo? hitSampleInfo)
{
if (hitSampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy)
return legacy.CustomSampleBank;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.IO;
@ -19,10 +17,10 @@ namespace osu.Game.Beatmaps.Formats
{
public class LegacyStoryboardDecoder : LegacyDecoder<Storyboard>
{
private StoryboardSprite storyboardSprite;
private CommandTimelineGroup timelineGroup;
private StoryboardSprite? storyboardSprite;
private CommandTimelineGroup? timelineGroup;
private Storyboard storyboard;
private Storyboard storyboard = null!;
private readonly Dictionary<string, string> variables = new Dictionary<string, string>();

View File

@ -70,7 +70,22 @@ namespace osu.Game.Database
hitObject.StartTime = Math.Floor(hitObject.StartTime);
if (hitObject is not IHasPath hasPath || BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue;
if (hitObject is not IHasPath hasPath) continue;
// stable's hit object parsing expects the entire slider to use only one type of curve,
// and happens to use the last non-empty curve type read for the entire slider.
// this clear of the last control point type handles an edge case
// wherein the last control point of an otherwise-single-segment slider path has a different type than previous,
// which would lead to sliders being mangled when exported back to stable.
// normally, that would be handled by the `BezierConverter.ConvertToModernBezier()` call below,
// which outputs a slider path containing only Bezier control points,
// but a non-inherited last control point is (rightly) not considered to be starting a new segment,
// therefore it would fail to clear the `CountSegments() <= 1` check.
// by clearing explicitly we both fix the issue and avoid unnecessary conversions to Bezier.
if (hasPath.Path.ControlPoints.Count > 1)
hasPath.Path.ControlPoints[^1].Type = null;
if (BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue;
var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints);

View File

@ -35,6 +35,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
public string Text
{
get => Component.Text;
set => Component.Text = value;
}

View File

@ -85,6 +85,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
Current.BindValueChanged(updateTextBoxFromSlider, true);
}
public bool TakeFocus() => GetContainingInputManager().ChangeFocus(textBox);
private bool updatingFromTextBox;
private void textChanged(ValueChangedEvent<string> change)

View File

@ -21,7 +21,9 @@ namespace osu.Game.IO.Archives
this.path = Path.GetFullPath(path);
}
public override Stream GetStream(string name) => File.OpenRead(Path.Combine(path, name));
public override Stream GetStream(string name) => File.OpenRead(GetFullPath(name));
public string GetFullPath(string filename) => Path.Combine(path, filename);
public override void Dispose()
{

View File

@ -105,6 +105,7 @@ namespace osu.Game.Input.Bindings
// See https://github.com/ppy/osu-framework/blob/master/osu.Framework/Input/StateChanges/MouseScrollRelativeInput.cs#L37-L38.
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor),
new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl),
};
public IEnumerable<KeyBinding> InGameKeyBindings => new[]
@ -378,5 +379,8 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleInGameLeaderboard))]
ToggleInGameLeaderboard,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))]
EditorToggleRotateControl,
}
}

View File

@ -344,6 +344,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString ExportReplay => new TranslatableString(getKey(@"export_replay"), @"Export replay");
/// <summary>
/// "Toggle rotate control"
/// </summary>
public static LocalisableString EditorToggleRotateControl => new TranslatableString(getKey(@"editor_toggle_rotate_control"), @"Toggle rotate control");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -198,8 +198,23 @@ namespace osu.Game.Rulesets.Objects
}
/// <summary>
/// Returns the progress values at which segments of the path end.
/// Returns the progress values at which (control point) segments of the path end.
/// Ranges from 0 (beginning of the path) to 1 (end of the path) to infinity (beyond the end of the path).
/// </summary>
/// <remarks>
/// <see cref="PositionAt"/> truncates the progression values to [0,1],
/// so you can't use this method in conjunction with that one to retrieve the positions of segment ends beyond the end of the path.
/// </remarks>
/// <example>
/// <para>
/// In case <see cref="Distance"/> is less than <see cref="CalculatedDistance"/>,
/// the last segment ends after the end of the path, hence it returns a value greater than 1.
/// </para>
/// <para>
/// In case <see cref="Distance"/> is greater than <see cref="CalculatedDistance"/>,
/// the last segment ends before the end of the path, hence it returns a value less than 1.
/// </para>
/// </example>
public IEnumerable<double> GetSegmentEnds()
{
ensureValid();
@ -254,8 +269,10 @@ namespace osu.Game.Rulesets.Objects
}
if (i > 0)
{
// Remember the index of the segment end
segmentEnds.Add(calculatedPath.Count - 1);
}
// Start the new segment at the current vertex
start = i;

View File

@ -0,0 +1,107 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Components
{
public partial class EditorToolButton : OsuButton, IHasPopover
{
public BindableBool Selected { get; } = new BindableBool();
private readonly Func<Drawable> createIcon;
private readonly Func<Popover?> createPopover;
private Color4 defaultBackgroundColour;
private Color4 defaultIconColour;
private Color4 selectedBackgroundColour;
private Color4 selectedIconColour;
private Drawable icon = null!;
public EditorToolButton(LocalisableString text, Func<Drawable> createIcon, Func<Popover?> createPopover)
{
Text = text;
this.createIcon = createIcon;
this.createPopover = createPopover;
RelativeSizeAxes = Axes.X;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
defaultBackgroundColour = colourProvider.Background3;
selectedBackgroundColour = colourProvider.Background1;
defaultIconColour = defaultBackgroundColour.Darken(0.5f);
selectedIconColour = selectedBackgroundColour.Lighten(0.5f);
Add(icon = createIcon().With(b =>
{
b.Blending = BlendingParameters.Additive;
b.Anchor = Anchor.CentreLeft;
b.Origin = Anchor.CentreLeft;
b.Size = new Vector2(20);
b.X = 10;
}));
Action = Selected.Toggle;
}
protected override void LoadComplete()
{
base.LoadComplete();
Selected.BindValueChanged(_ => updateSelectionState(), true);
}
private void updateSelectionState()
{
if (!IsLoaded)
return;
BackgroundColour = Selected.Value ? selectedBackgroundColour : defaultBackgroundColour;
icon.Colour = Selected.Value ? selectedIconColour : defaultIconColour;
if (Selected.Value)
this.ShowPopover();
else
this.HidePopover();
}
protected override SpriteText CreateText() => new OsuSpriteText
{
Depth = -1,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
X = 40f
};
public Popover? GetPopover() => Enabled.Value
? createPopover()?.With(p =>
{
p.State.BindValueChanged(state =>
{
if (state.NewValue == Visibility.Hidden)
Selected.Value = false;
});
})
: null;
}
}

View File

@ -34,7 +34,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
public Container<SelectionBlueprint<T>> SelectionBlueprints { get; private set; }
protected SelectionHandler<T> SelectionHandler { get; private set; }
public SelectionHandler<T> SelectionHandler { get; private set; }
private readonly Dictionary<T, SelectionBlueprint<T>> blueprintMap = new Dictionary<T, SelectionBlueprint<T>>();

View File

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

View File

@ -232,6 +232,6 @@ namespace osu.Game.Skinning
}
private static Color4 getComboColour(IHasComboColours source, int colourIndex)
=> source.ComboColours[colourIndex % source.ComboColours.Count];
=> source.ComboColours![colourIndex % source.ComboColours.Count];
}
}

View File

@ -203,6 +203,6 @@ namespace osu.Game.Skinning
}
private static Color4 getComboColour(IHasComboColours source, int colourIndex)
=> source.ComboColours[colourIndex % source.ComboColours.Count];
=> source.ComboColours![colourIndex % source.ComboColours.Count];
}
}

View File

@ -36,7 +36,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="11.1.2" />
<PackageReference Include="ppy.osu.Framework" Version="2023.815.0" />
<PackageReference Include="ppy.osu.Framework" Version="2023.817.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.719.0" />
<PackageReference Include="Sentry" Version="3.28.1" />
<PackageReference Include="SharpCompress" Version="0.32.2" />

View File

@ -23,6 +23,6 @@
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.815.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.817.0" />
</ItemGroup>
</Project>