1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-26 18:52:55 +08:00

Merge branch 'master' into formats-nrt

This commit is contained in:
Bartłomiej Dach 2023-08-21 08:17:34 +02:00 committed by GitHub
commit c27f06c0a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 880 additions and 332 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

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{
private const double individual_decay_base = 0.125;
private const double overall_decay_base = 0.30;
private const double release_threshold = 24;
private const double release_threshold = 30;
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 1;
@ -50,10 +50,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
for (int i = 0; i < endTimes.Length; ++i)
{
// The current note is overlapped if a previous note or end is overlapping the current note body
isOverlapping |= Precision.DefinitelyBigger(endTimes[i], startTime, 1) && Precision.DefinitelyBigger(endTime, endTimes[i], 1);
isOverlapping |= Precision.DefinitelyBigger(endTimes[i], startTime, 1) &&
Precision.DefinitelyBigger(endTime, endTimes[i], 1) &&
Precision.DefinitelyBigger(startTime, startTimes[i], 1);
// We give a slight bonus to everything if something is held meanwhile
if (Precision.DefinitelyBigger(endTimes[i], endTime, 1))
if (Precision.DefinitelyBigger(endTimes[i], endTime, 1) &&
Precision.DefinitelyBigger(startTime, startTimes[i], 1))
holdFactor = 1.25;
closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - endTimes[i]));
@ -70,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
// 0.0 +--------+-+---------------> Release Difference / ms
// release_threshold
if (isOverlapping)
holdAddition = 1 / (1 + Math.Exp(0.5 * (release_threshold - closestEndTime)));
holdAddition = 1 / (1 + Math.Exp(0.27 * (release_threshold - closestEndTime)));
// Decay and increase individualStrains in own column
individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base);

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

@ -0,0 +1,130 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneSliderWithTextBoxInput : OsuManualInputManagerTestScene
{
private SliderWithTextBoxInput<float> sliderWithTextBoxInput = null!;
private OsuSliderBar<float> slider => sliderWithTextBoxInput.ChildrenOfType<OsuSliderBar<float>>().Single();
private Nub nub => sliderWithTextBoxInput.ChildrenOfType<Nub>().Single();
private OsuTextBox textBox => sliderWithTextBoxInput.ChildrenOfType<OsuTextBox>().Single();
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create slider", () => Child = sliderWithTextBoxInput = new SliderWithTextBoxInput<float>("Test Slider")
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
Current = new BindableFloat
{
MinValue = -5,
MaxValue = 5,
Precision = 0.2f
}
});
}
[Test]
public void TestNonInstantaneousMode()
{
AddStep("set instantaneous to false", () => sliderWithTextBoxInput.Instantaneous = false);
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
AddStep("change text", () => textBox.Text = "3");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.Zero);
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.Zero);
AddStep("commit text", () => InputManager.Key(Key.Enter));
AddAssert("slider moved", () => slider.Current.Value, () => Is.EqualTo(3));
AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3));
AddStep("move mouse to nub", () => InputManager.MoveMouseTo(nub));
AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left));
AddStep("move mouse to minimum", () => InputManager.MoveMouseTo(sliderWithTextBoxInput.ScreenSpaceDrawQuad.BottomLeft));
AddAssert("textbox not changed", () => textBox.Current.Value, () => Is.EqualTo("3"));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3));
AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("textbox changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("commit text", () => InputManager.Key(Key.Enter));
AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("lose focus", () => InputManager.ChangeFocus(null));
AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
}
[Test]
public void TestInstantaneousMode()
{
AddStep("set instantaneous to true", () => sliderWithTextBoxInput.Instantaneous = true);
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
AddStep("change text", () => textBox.Text = "3");
AddAssert("slider moved", () => slider.Current.Value, () => Is.EqualTo(3));
AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3));
AddStep("commit text", () => InputManager.Key(Key.Enter));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(3));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3));
AddStep("move mouse to nub", () => InputManager.MoveMouseTo(nub));
AddStep("hold left mouse", () => InputManager.PressButton(MouseButton.Left));
AddStep("move mouse to minimum", () => InputManager.MoveMouseTo(sliderWithTextBoxInput.ScreenSpaceDrawQuad.BottomLeft));
AddAssert("textbox changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("release left mouse", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("textbox not changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("commit text", () => InputManager.Key(Key.Enter));
AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("lose focus", () => InputManager.ChangeFocus(null));
AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
}
}
}

View File

@ -14,7 +14,6 @@ using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Rulesets;
using osu.Game.Screens.Menu;
using osu.Game.Tournament.Models;
using osuTK;
using osuTK.Graphics;
@ -22,14 +21,14 @@ namespace osu.Game.Tournament.Components
{
public partial class SongBar : CompositeDrawable
{
private TournamentBeatmap? beatmap;
private IBeatmapInfo? beatmap;
public const float HEIGHT = 145 / 2f;
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
public TournamentBeatmap? Beatmap
public IBeatmapInfo? Beatmap
{
set
{
@ -37,7 +36,7 @@ namespace osu.Game.Tournament.Components
return;
beatmap = value;
update();
refreshContent();
}
}
@ -49,7 +48,7 @@ namespace osu.Game.Tournament.Components
set
{
mods = value;
update();
refreshContent();
}
}
@ -71,19 +70,25 @@ namespace osu.Game.Tournament.Components
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
[BackgroundDependencyLoader]
private void load()
private void load(OsuColour colours)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Masking = true;
CornerRadius = 5;
InternalChildren = new Drawable[]
{
new Box
{
Colour = colours.Gray3,
RelativeSizeAxes = Axes.Both,
},
flow = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
LayoutDuration = 500,
LayoutEasing = Easing.OutQuint,
Direction = FillDirection.Full,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
@ -93,7 +98,7 @@ namespace osu.Game.Tournament.Components
Expanded = true;
}
private void update()
private void refreshContent()
{
if (beatmap == null)
{
@ -229,7 +234,7 @@ namespace osu.Game.Tournament.Components
}
}
},
new TournamentBeatmapPanel(beatmap)
new UnmaskedTournamentBeatmapPanel(beatmap)
{
RelativeSizeAxes = Axes.X,
Width = 0.5f,
@ -272,4 +277,18 @@ namespace osu.Game.Tournament.Components
}
}
}
internal partial class UnmaskedTournamentBeatmapPanel : TournamentBeatmapPanel
{
public UnmaskedTournamentBeatmapPanel(IBeatmapInfo? beatmap, string mod = "")
: base(beatmap, mod)
{
}
[BackgroundDependencyLoader]
private void load()
{
Masking = false;
}
}
}

View File

@ -20,7 +20,7 @@ namespace osu.Game.Tournament.Components
{
public partial class TournamentBeatmapPanel : CompositeDrawable
{
public readonly TournamentBeatmap? Beatmap;
public readonly IBeatmapInfo? Beatmap;
private readonly string mod;
@ -30,7 +30,7 @@ namespace osu.Game.Tournament.Components
private Box flash = null!;
public TournamentBeatmapPanel(TournamentBeatmap? beatmap, string mod = "")
public TournamentBeatmapPanel(IBeatmapInfo? beatmap, string mod = "")
{
Beatmap = beatmap;
this.mod = mod;
@ -58,7 +58,7 @@ namespace osu.Game.Tournament.Components
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(0.5f),
OnlineInfo = Beatmap,
OnlineInfo = (Beatmap as IBeatmapSetOnlineInfo),
},
new FillFlowContainer
{

View File

@ -14,7 +14,7 @@ namespace osu.Game.Tournament.IPC
public Bindable<LegacyMods> Mods { get; } = new Bindable<LegacyMods>();
public Bindable<TourneyState> State { get; } = new Bindable<TourneyState>();
public Bindable<string> ChatChannel { get; } = new Bindable<string>();
public BindableInt Score1 { get; } = new BindableInt();
public BindableInt Score2 { get; } = new BindableInt();
public BindableLong Score1 { get; } = new BindableLong();
public BindableLong Score2 { get; } = new BindableLong();
}
}

View File

@ -1,181 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Play.HUD;
using osu.Game.Tournament.IPC;
using osuTK;
namespace osu.Game.Tournament.Screens.Gameplay.Components
{
// TODO: Update to derive from osu-side class?
public partial class TournamentMatchScoreDisplay : CompositeDrawable
public partial class TournamentMatchScoreDisplay : MatchScoreDisplay
{
private const float bar_height = 18;
private readonly BindableInt score1 = new BindableInt();
private readonly BindableInt score2 = new BindableInt();
private readonly MatchScoreCounter score1Text;
private readonly MatchScoreCounter score2Text;
private readonly MatchScoreDiffCounter scoreDiffText;
private readonly Drawable score1Bar;
private readonly Drawable score2Bar;
public TournamentMatchScoreDisplay()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChildren = new[]
{
new Box
{
Name = "top bar red (static)",
RelativeSizeAxes = Axes.X,
Height = bar_height / 4,
Width = 0.5f,
Colour = TournamentGame.COLOUR_RED,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopRight
},
new Box
{
Name = "top bar blue (static)",
RelativeSizeAxes = Axes.X,
Height = bar_height / 4,
Width = 0.5f,
Colour = TournamentGame.COLOUR_BLUE,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopLeft
},
score1Bar = new Box
{
Name = "top bar red",
RelativeSizeAxes = Axes.X,
Height = bar_height,
Width = 0,
Colour = TournamentGame.COLOUR_RED,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopRight
},
score1Text = new MatchScoreCounter
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
},
score2Bar = new Box
{
Name = "top bar blue",
RelativeSizeAxes = Axes.X,
Height = bar_height,
Width = 0,
Colour = TournamentGame.COLOUR_BLUE,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopLeft
},
score2Text = new MatchScoreCounter
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
},
scoreDiffText = new MatchScoreDiffCounter
{
Anchor = Anchor.TopCentre,
Margin = new MarginPadding
{
Top = bar_height / 4,
Horizontal = 8
},
Alpha = 0
}
};
}
[BackgroundDependencyLoader]
private void load(MatchIPCInfo ipc)
{
score1.BindValueChanged(_ => updateScores());
score1.BindTo(ipc.Score1);
score2.BindValueChanged(_ => updateScores());
score2.BindTo(ipc.Score2);
}
private void updateScores()
{
score1Text.Current.Value = score1.Value;
score2Text.Current.Value = score2.Value;
var winningText = score1.Value > score2.Value ? score1Text : score2Text;
var losingText = score1.Value <= score2.Value ? score1Text : score2Text;
winningText.Winning = true;
losingText.Winning = false;
var winningBar = score1.Value > score2.Value ? score1Bar : score2Bar;
var losingBar = score1.Value <= score2.Value ? score1Bar : score2Bar;
int diff = Math.Max(score1.Value, score2.Value) - Math.Min(score1.Value, score2.Value);
losingBar.ResizeWidthTo(0, 400, Easing.OutQuint);
winningBar.ResizeWidthTo(Math.Min(0.4f, MathF.Pow(diff / 1500000f, 0.5f) / 2), 400, Easing.OutQuint);
scoreDiffText.Alpha = diff != 0 ? 1 : 0;
scoreDiffText.Current.Value = -diff;
scoreDiffText.Origin = score1.Value > score2.Value ? Anchor.TopLeft : Anchor.TopRight;
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
score1Text.X = -Math.Max(5 + score1Text.DrawWidth / 2, score1Bar.DrawWidth);
score2Text.X = Math.Max(5 + score2Text.DrawWidth / 2, score2Bar.DrawWidth);
}
private partial class MatchScoreCounter : CommaSeparatedScoreCounter
{
private OsuSpriteText displayedSpriteText = null!;
public MatchScoreCounter()
{
Margin = new MarginPadding { Top = bar_height, Horizontal = 10 };
}
public bool Winning
{
set => updateFont(value);
}
protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s =>
{
displayedSpriteText = s;
displayedSpriteText.Spacing = new Vector2(-6);
updateFont(false);
});
private void updateFont(bool winning)
=> displayedSpriteText.Font = winning
? OsuFont.Torus.With(weight: FontWeight.Bold, size: 50, fixedWidth: true)
: OsuFont.Torus.With(weight: FontWeight.Regular, size: 40, fixedWidth: true);
}
private partial class MatchScoreDiffCounter : CommaSeparatedScoreCounter
{
protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s =>
{
s.Spacing = new Vector2(-2);
s.Font = OsuFont.Torus.With(weight: FontWeight.Regular, size: bar_height, fixedWidth: true);
});
Team1Score.BindTo(ipc.Score1);
Team2Score.BindTo(ipc.Score2);
}
}
}

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

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

@ -0,0 +1,145 @@
// 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.Globalization;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Overlays.Settings;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Graphics.UserInterfaceV2
{
public partial class SliderWithTextBoxInput<T> : CompositeDrawable, IHasCurrentValue<T>
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
{
/// <summary>
/// A custom step value for each key press which actuates a change on this control.
/// </summary>
public float KeyboardStep
{
get => slider.KeyboardStep;
set => slider.KeyboardStep = value;
}
public Bindable<T> Current
{
get => slider.Current;
set => slider.Current = value;
}
private bool instantaneous;
/// <summary>
/// Whether changes to the slider should instantaneously transfer to the text box (and vice versa).
/// If <see langword="false"/>, the transfer will happen on text box commit (explicit, or implicit via focus loss), or on slider drag end.
/// </summary>
public bool Instantaneous
{
get => instantaneous;
set
{
instantaneous = value;
slider.TransferValueOnCommit = !instantaneous;
}
}
private readonly SettingsSlider<T> slider;
private readonly LabelledTextBox textBox;
public SliderWithTextBoxInput(LocalisableString labelText)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(20),
Children = new Drawable[]
{
textBox = new LabelledTextBox
{
Label = labelText,
},
slider = new SettingsSlider<T>
{
TransferValueOnCommit = true,
RelativeSizeAxes = Axes.X,
}
}
},
};
textBox.OnCommit += textCommitted;
textBox.Current.BindValueChanged(textChanged);
Current.BindValueChanged(updateTextBoxFromSlider, true);
}
public bool TakeFocus() => GetContainingInputManager().ChangeFocus(textBox);
private bool updatingFromTextBox;
private void textChanged(ValueChangedEvent<string> change)
{
if (!instantaneous) return;
tryUpdateSliderFromTextBox();
}
private void textCommitted(TextBox t, bool isNew)
{
tryUpdateSliderFromTextBox();
// If the attempted update above failed, restore text box to match the slider.
Current.TriggerChange();
}
private void tryUpdateSliderFromTextBox()
{
updatingFromTextBox = true;
try
{
switch (slider.Current)
{
case Bindable<int> bindableInt:
bindableInt.Value = int.Parse(textBox.Current.Value);
break;
case Bindable<double> bindableDouble:
bindableDouble.Value = double.Parse(textBox.Current.Value);
break;
default:
slider.Current.Parse(textBox.Current.Value);
break;
}
}
catch
{
// ignore parsing failures.
// sane state will eventually be restored by a commit (either explicit, or implicit via focus loss).
}
updatingFromTextBox = false;
}
private void updateTextBoxFromSlider(ValueChangedEvent<T> _)
{
if (updatingFromTextBox) return;
decimal decimalValue = slider.Current.Value.ToDecimal(NumberFormatInfo.InvariantInfo);
textBox.Text = decimalValue.ToString($@"N{FormatUtils.FindPrecision(decimalValue)}");
}
}
}

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

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

@ -147,13 +147,25 @@ namespace osu.Game.Screens.Edit.Timing
trackedType = null;
else
{
// If the selected group only has one control point, update the tracking type.
if (selectedGroup.Value.ControlPoints.Count == 1)
trackedType = selectedGroup.Value?.ControlPoints.Single().GetType();
// If the selected group has more than one control point, choose the first as the tracking type
// if we don't already have a singular tracked type.
else if (trackedType == null)
trackedType = selectedGroup.Value?.ControlPoints.FirstOrDefault()?.GetType();
switch (selectedGroup.Value.ControlPoints.Count)
{
// If the selected group has no control points, clear the tracked type.
// Otherwise the user will be unable to select a group with no control points.
case 0:
trackedType = null;
break;
// If the selected group only has one control point, update the tracking type.
case 1:
trackedType = selectedGroup.Value?.ControlPoints.Single().GetType();
break;
// If the selected group has more than one control point, choose the first as the tracking type
// if we don't already have a singular tracked type.
default:
trackedType ??= selectedGroup.Value?.ControlPoints.FirstOrDefault()?.GetType();
break;
}
}
if (trackedType != null)

View File

@ -1,106 +0,0 @@
// 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.Globalization;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays.Settings;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Screens.Edit.Timing
{
public partial class SliderWithTextBoxInput<T> : CompositeDrawable, IHasCurrentValue<T>
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
{
private readonly SettingsSlider<T> slider;
public SliderWithTextBoxInput(LocalisableString labelText)
{
LabelledTextBox textBox;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(20),
Children = new Drawable[]
{
textBox = new LabelledTextBox
{
Label = labelText,
},
slider = new SettingsSlider<T>
{
TransferValueOnCommit = true,
RelativeSizeAxes = Axes.X,
}
}
},
};
textBox.OnCommit += (t, isNew) =>
{
if (!isNew) return;
try
{
switch (slider.Current)
{
case Bindable<int> bindableInt:
bindableInt.Value = int.Parse(t.Text);
break;
case Bindable<double> bindableDouble:
bindableDouble.Value = double.Parse(t.Text);
break;
default:
slider.Current.Parse(t.Text);
break;
}
}
catch
{
// TriggerChange below will restore the previous text value on failure.
}
// This is run regardless of parsing success as the parsed number may not actually trigger a change
// due to bindable clamping. Even in such a case we want to update the textbox to a sane visual state.
Current.TriggerChange();
};
Current.BindValueChanged(_ =>
{
decimal decimalValue = slider.Current.Value.ToDecimal(NumberFormatInfo.InvariantInfo);
textBox.Text = decimalValue.ToString($@"N{FormatUtils.FindPrecision(decimalValue)}");
}, true);
}
/// <summary>
/// A custom step value for each key press which actuates a change on this control.
/// </summary>
public float KeyboardStep
{
get => slider.KeyboardStep;
set => slider.KeyboardStep = value;
}
public Bindable<T> Current
{
get => slider.Current;
set => slider.Current = value;
}
}
}

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 osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -24,11 +22,13 @@ namespace osu.Game.Screens.Play.HUD
public BindableLong Team1Score = new BindableLong();
public BindableLong Team2Score = new BindableLong();
protected MatchScoreCounter Score1Text;
protected MatchScoreCounter Score2Text;
protected MatchScoreCounter Score1Text = null!;
protected MatchScoreCounter Score2Text = null!;
private Drawable score1Bar;
private Drawable score2Bar;
private Drawable score1Bar = null!;
private Drawable score2Bar = null!;
private MatchScoreDiffCounter scoreDiffText = null!;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
@ -98,6 +98,16 @@ namespace osu.Game.Screens.Play.HUD
},
}
},
scoreDiffText = new MatchScoreDiffCounter
{
Anchor = Anchor.TopCentre,
Margin = new MarginPadding
{
Top = bar_height / 4,
Horizontal = 8
},
Alpha = 0
}
};
}
@ -139,6 +149,10 @@ namespace osu.Game.Screens.Play.HUD
losingBar.ResizeWidthTo(0, 400, Easing.OutQuint);
winningBar.ResizeWidthTo(Math.Min(0.4f, MathF.Pow(diff / 1500000f, 0.5f) / 2), 400, Easing.OutQuint);
scoreDiffText.Alpha = diff != 0 ? 1 : 0;
scoreDiffText.Current.Value = -diff;
scoreDiffText.Origin = Team1Score.Value > Team2Score.Value ? Anchor.TopLeft : Anchor.TopRight;
}
protected override void UpdateAfterChildren()
@ -150,7 +164,7 @@ namespace osu.Game.Screens.Play.HUD
protected partial class MatchScoreCounter : CommaSeparatedScoreCounter
{
private OsuSpriteText displayedSpriteText;
private OsuSpriteText displayedSpriteText = null!;
public MatchScoreCounter()
{
@ -174,5 +188,14 @@ namespace osu.Game.Screens.Play.HUD
? OsuFont.Torus.With(weight: FontWeight.Bold, size: font_size, fixedWidth: true)
: OsuFont.Torus.With(weight: FontWeight.Regular, size: font_size * 0.8f, fixedWidth: true);
}
private partial class MatchScoreDiffCounter : CommaSeparatedScoreCounter
{
protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s =>
{
s.Spacing = new Vector2(-2);
s.Font = OsuFont.Torus.With(weight: FontWeight.Regular, size: bar_height, fixedWidth: true);
});
}
}
}

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>