mirror of
https://github.com/ppy/osu.git
synced 2025-03-06 02:22:58 +08:00
Merge branch 'master' into ux/notifications/flash-on-message
This commit is contained in:
commit
71b5be2568
@ -10,7 +10,7 @@
|
|||||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.815.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.817.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-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">
|
<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" />
|
<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>
|
</manifest>
|
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- using a different name because package name cannot contain 'catch' -->
|
<!-- 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">
|
<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" />
|
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!catch Test" />
|
||||||
</manifest>
|
</manifest>
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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">
|
<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" />
|
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!mania Test" />
|
||||||
</manifest>
|
</manifest>
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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">
|
<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" />
|
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!standard Test" />
|
||||||
</manifest>
|
</manifest>
|
@ -9,6 +9,7 @@ using NUnit.Framework;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Cursor;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
@ -70,12 +71,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
base.Content.Children = new Drawable[]
|
base.Content.Children = new Drawable[]
|
||||||
{
|
{
|
||||||
editorClock = new EditorClock(editorBeatmap),
|
editorClock = new EditorClock(editorBeatmap),
|
||||||
snapProvider,
|
new PopoverContainer { Child = snapProvider },
|
||||||
Content
|
Content
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
protected override Container<Drawable> Content { get; } = new PopoverContainer { RelativeSizeAxes = Axes.Both };
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup() => Schedule(() =>
|
public void Setup() => Schedule(() =>
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
110
osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs
Normal file
110
osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderReversal.cs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
// 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.Utils;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||||
|
{
|
||||||
|
public partial class TestSceneSliderReversal : TestSceneOsuEditor
|
||||||
|
{
|
||||||
|
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(Ruleset.Value, false);
|
||||||
|
|
||||||
|
private readonly PathControlPoint[][] paths =
|
||||||
|
{
|
||||||
|
createPathSegment(
|
||||||
|
PathType.PerfectCurve,
|
||||||
|
new Vector2(200, -50),
|
||||||
|
new Vector2(250, 0)
|
||||||
|
),
|
||||||
|
createPathSegment(
|
||||||
|
PathType.Linear,
|
||||||
|
new Vector2(100, 0),
|
||||||
|
new Vector2(100, 100)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
private static PathControlPoint[] createPathSegment(PathType type, params Vector2[] positions)
|
||||||
|
{
|
||||||
|
return positions.Select(p => new PathControlPoint
|
||||||
|
{
|
||||||
|
Position = p
|
||||||
|
}).Prepend(new PathControlPoint
|
||||||
|
{
|
||||||
|
Type = type
|
||||||
|
}).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Slider selectedSlider => (Slider)EditorBeatmap.SelectedHitObjects[0];
|
||||||
|
|
||||||
|
[TestCase(0, 250)]
|
||||||
|
[TestCase(0, 200)]
|
||||||
|
[TestCase(1, 120)]
|
||||||
|
[TestCase(1, 80)]
|
||||||
|
public void TestSliderReversal(int pathIndex, double length)
|
||||||
|
{
|
||||||
|
var controlPoints = paths[pathIndex];
|
||||||
|
|
||||||
|
Vector2 oldStartPos = default;
|
||||||
|
Vector2 oldEndPos = default;
|
||||||
|
double oldDistance = default;
|
||||||
|
var oldControlPointTypes = controlPoints.Select(p => p.Type);
|
||||||
|
|
||||||
|
AddStep("Add slider", () =>
|
||||||
|
{
|
||||||
|
var slider = new Slider
|
||||||
|
{
|
||||||
|
Position = new Vector2(OsuPlayfield.BASE_SIZE.X / 2, OsuPlayfield.BASE_SIZE.Y / 2),
|
||||||
|
Path = new SliderPath(controlPoints)
|
||||||
|
{
|
||||||
|
ExpectedDistance = { Value = length }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
EditorBeatmap.Add(slider);
|
||||||
|
|
||||||
|
oldStartPos = slider.Position;
|
||||||
|
oldEndPos = slider.EndPosition;
|
||||||
|
oldDistance = slider.Path.Distance;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("Select slider", () =>
|
||||||
|
{
|
||||||
|
var slider = (Slider)EditorBeatmap.HitObjects[0];
|
||||||
|
EditorBeatmap.SelectedHitObjects.Add(slider);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("Reverse slider", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.LControl);
|
||||||
|
InputManager.Key(Key.G);
|
||||||
|
InputManager.ReleaseKey(Key.LControl);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("Slider has correct length", () =>
|
||||||
|
Precision.AlmostEquals(selectedSlider.Path.Distance, oldDistance));
|
||||||
|
|
||||||
|
AddAssert("Slider has correct start position", () =>
|
||||||
|
Vector2.Distance(selectedSlider.Position, oldEndPos) < 1);
|
||||||
|
|
||||||
|
AddAssert("Slider has correct end position", () =>
|
||||||
|
Vector2.Distance(selectedSlider.EndPosition, oldStartPos) < 1);
|
||||||
|
|
||||||
|
AddAssert("Control points have correct types", () =>
|
||||||
|
{
|
||||||
|
var newControlPointTypes = selectedSlider.Path.ControlPoints.Select(p => p.Type).ToArray();
|
||||||
|
|
||||||
|
return oldControlPointTypes.Take(newControlPointTypes.Length).SequenceEqual(newControlPointTypes);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -85,6 +85,11 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
// we may be entering the screen with a selection already active
|
// we may be entering the screen with a selection already active
|
||||||
updateDistanceSnapGrid();
|
updateDistanceSnapGrid();
|
||||||
|
|
||||||
|
RightToolbox.Add(new TransformToolboxGroup
|
||||||
|
{
|
||||||
|
RotationHandler = BlueprintContainer.SelectionHandler.RotationHandler
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override ComposeBlueprintContainer CreateBlueprintContainer()
|
protected override ComposeBlueprintContainer CreateBlueprintContainer()
|
||||||
|
107
osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs
Normal file
107
osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs
Normal 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);
|
||||||
|
}
|
80
osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs
Normal file
80
osu.Game.Rulesets.Osu/Edit/TransformToolboxGroup.cs
Normal 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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -257,7 +257,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
texture.Bind();
|
texture.Bind();
|
||||||
|
|
||||||
for (int i = 0; i < points.Count; i++)
|
for (int i = 0; i < points.Count; i++)
|
||||||
drawPointQuad(points[i], textureRect, i + firstVisiblePointIndex);
|
drawPointQuad(renderer, points[i], textureRect, i + firstVisiblePointIndex);
|
||||||
|
|
||||||
UnbindTextureShader(renderer);
|
UnbindTextureShader(renderer);
|
||||||
renderer.PopLocalMatrix();
|
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 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);
|
Debug.Assert(quadBatch != null);
|
||||||
|
|
||||||
@ -347,25 +347,25 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
var localBotLeft = point.Position + ortho - dir;
|
var localBotLeft = point.Position + ortho - dir;
|
||||||
var localBotRight = point.Position + ortho + dir;
|
var localBotRight = point.Position + ortho + dir;
|
||||||
|
|
||||||
quadBatch.Add(new TexturedVertex2D
|
quadBatch.Add(new TexturedVertex2D(renderer)
|
||||||
{
|
{
|
||||||
Position = localTopLeft,
|
Position = localTopLeft,
|
||||||
TexturePosition = textureRect.TopLeft,
|
TexturePosition = textureRect.TopLeft,
|
||||||
Colour = Color4Extensions.Multiply(ColourAtPosition(localTopLeft), colour),
|
Colour = Color4Extensions.Multiply(ColourAtPosition(localTopLeft), colour),
|
||||||
});
|
});
|
||||||
quadBatch.Add(new TexturedVertex2D
|
quadBatch.Add(new TexturedVertex2D(renderer)
|
||||||
{
|
{
|
||||||
Position = localTopRight,
|
Position = localTopRight,
|
||||||
TexturePosition = textureRect.TopRight,
|
TexturePosition = textureRect.TopRight,
|
||||||
Colour = Color4Extensions.Multiply(ColourAtPosition(localTopRight), colour),
|
Colour = Color4Extensions.Multiply(ColourAtPosition(localTopRight), colour),
|
||||||
});
|
});
|
||||||
quadBatch.Add(new TexturedVertex2D
|
quadBatch.Add(new TexturedVertex2D(renderer)
|
||||||
{
|
{
|
||||||
Position = localBotRight,
|
Position = localBotRight,
|
||||||
TexturePosition = textureRect.BottomRight,
|
TexturePosition = textureRect.BottomRight,
|
||||||
Colour = Color4Extensions.Multiply(ColourAtPosition(localBotRight), colour),
|
Colour = Color4Extensions.Multiply(ColourAtPosition(localBotRight), colour),
|
||||||
});
|
});
|
||||||
quadBatch.Add(new TexturedVertex2D
|
quadBatch.Add(new TexturedVertex2D(renderer)
|
||||||
{
|
{
|
||||||
Position = localBotLeft,
|
Position = localBotLeft,
|
||||||
TexturePosition = textureRect.BottomLeft,
|
TexturePosition = textureRect.BottomLeft,
|
||||||
|
@ -286,7 +286,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
if (time - part.Time >= 1)
|
if (time - part.Time >= 1)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
vertexBatch.Add(new TexturedTrailVertex
|
vertexBatch.Add(new TexturedTrailVertex(renderer)
|
||||||
{
|
{
|
||||||
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)),
|
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)),
|
||||||
TexturePosition = textureRect.BottomLeft,
|
TexturePosition = textureRect.BottomLeft,
|
||||||
@ -295,7 +295,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
Time = part.Time
|
Time = part.Time
|
||||||
});
|
});
|
||||||
|
|
||||||
vertexBatch.Add(new TexturedTrailVertex
|
vertexBatch.Add(new TexturedTrailVertex(renderer)
|
||||||
{
|
{
|
||||||
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)),
|
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)),
|
||||||
TexturePosition = textureRect.BottomRight,
|
TexturePosition = textureRect.BottomRight,
|
||||||
@ -304,7 +304,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
Time = part.Time
|
Time = part.Time
|
||||||
});
|
});
|
||||||
|
|
||||||
vertexBatch.Add(new TexturedTrailVertex
|
vertexBatch.Add(new TexturedTrailVertex(renderer)
|
||||||
{
|
{
|
||||||
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y),
|
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y),
|
||||||
TexturePosition = textureRect.TopRight,
|
TexturePosition = textureRect.TopRight,
|
||||||
@ -313,7 +313,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
Time = part.Time
|
Time = part.Time
|
||||||
});
|
});
|
||||||
|
|
||||||
vertexBatch.Add(new TexturedTrailVertex
|
vertexBatch.Add(new TexturedTrailVertex(renderer)
|
||||||
{
|
{
|
||||||
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y),
|
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y),
|
||||||
TexturePosition = textureRect.TopLeft,
|
TexturePosition = textureRect.TopLeft,
|
||||||
@ -362,12 +362,22 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
[VertexMember(1, VertexAttribPointerType.Float)]
|
[VertexMember(1, VertexAttribPointerType.Float)]
|
||||||
public float Time;
|
public float Time;
|
||||||
|
|
||||||
|
[VertexMember(1, VertexAttribPointerType.Int)]
|
||||||
|
private readonly int maskingIndex;
|
||||||
|
|
||||||
|
public TexturedTrailVertex(IRenderer renderer)
|
||||||
|
{
|
||||||
|
this = default;
|
||||||
|
maskingIndex = renderer.CurrentMaskingIndex;
|
||||||
|
}
|
||||||
|
|
||||||
public bool Equals(TexturedTrailVertex other)
|
public bool Equals(TexturedTrailVertex other)
|
||||||
{
|
{
|
||||||
return Position.Equals(other.Position)
|
return Position.Equals(other.Position)
|
||||||
&& TexturePosition.Equals(other.TexturePosition)
|
&& TexturePosition.Equals(other.TexturePosition)
|
||||||
&& Colour.Equals(other.Colour)
|
&& Colour.Equals(other.Colour)
|
||||||
&& Time.Equals(other.Time);
|
&& Time.Equals(other.Time)
|
||||||
|
&& maskingIndex == other.maskingIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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">
|
<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" />
|
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!taiko Test" />
|
||||||
</manifest>
|
</manifest>
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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">
|
<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" />
|
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!visual Test" />
|
||||||
</manifest>
|
</manifest>
|
@ -8,6 +8,9 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Scoring.Legacy;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Tests.Beatmaps.IO;
|
using osu.Game.Tests.Beatmaps.IO;
|
||||||
using osu.Game.Tests.Visual;
|
using osu.Game.Tests.Visual;
|
||||||
@ -15,7 +18,7 @@ using osu.Game.Tests.Visual;
|
|||||||
namespace osu.Game.Tests.Database
|
namespace osu.Game.Tests.Database
|
||||||
{
|
{
|
||||||
[HeadlessTest]
|
[HeadlessTest]
|
||||||
public partial class BackgroundBeatmapProcessorTests : OsuTestScene, ILocalUserPlayInfo
|
public partial class BackgroundDataStoreProcessorTests : OsuTestScene, ILocalUserPlayInfo
|
||||||
{
|
{
|
||||||
public IBindable<bool> IsPlaying => isPlaying;
|
public IBindable<bool> IsPlaying => isPlaying;
|
||||||
|
|
||||||
@ -59,7 +62,7 @@ namespace osu.Game.Tests.Database
|
|||||||
|
|
||||||
AddStep("Run background processor", () =>
|
AddStep("Run background processor", () =>
|
||||||
{
|
{
|
||||||
Add(new TestBackgroundBeatmapProcessor());
|
Add(new TestBackgroundDataStoreProcessor());
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("wait for difficulties repopulated", () =>
|
AddUntilStep("wait for difficulties repopulated", () =>
|
||||||
@ -98,7 +101,7 @@ namespace osu.Game.Tests.Database
|
|||||||
|
|
||||||
AddStep("Run background processor", () =>
|
AddStep("Run background processor", () =>
|
||||||
{
|
{
|
||||||
Add(new TestBackgroundBeatmapProcessor());
|
Add(new TestBackgroundDataStoreProcessor());
|
||||||
});
|
});
|
||||||
|
|
||||||
AddWaitStep("wait some", 500);
|
AddWaitStep("wait some", 500);
|
||||||
@ -124,7 +127,58 @@ namespace osu.Game.Tests.Database
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class TestBackgroundBeatmapProcessor : BackgroundBeatmapProcessor
|
[Test]
|
||||||
|
public void TestScoreUpgradeSuccess()
|
||||||
|
{
|
||||||
|
ScoreInfo scoreInfo = null!;
|
||||||
|
|
||||||
|
AddStep("Add score which requires upgrade (and has beatmap)", () =>
|
||||||
|
{
|
||||||
|
Realm.Write(r =>
|
||||||
|
{
|
||||||
|
r.Add(scoreInfo = new ScoreInfo(ruleset: r.All<RulesetInfo>().First(), beatmap: r.All<BeatmapInfo>().First())
|
||||||
|
{
|
||||||
|
TotalScoreVersion = 30000002,
|
||||||
|
LegacyTotalScore = 123456,
|
||||||
|
IsLegacyScore = true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor()));
|
||||||
|
|
||||||
|
AddUntilStep("Score version upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(LegacyScoreEncoder.LATEST_VERSION));
|
||||||
|
AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestScoreUpgradeFailed()
|
||||||
|
{
|
||||||
|
ScoreInfo scoreInfo = null!;
|
||||||
|
|
||||||
|
AddStep("Add score which requires upgrade (but has no beatmap)", () =>
|
||||||
|
{
|
||||||
|
Realm.Write(r =>
|
||||||
|
{
|
||||||
|
r.Add(scoreInfo = new ScoreInfo(ruleset: r.All<RulesetInfo>().First(), beatmap: new BeatmapInfo
|
||||||
|
{
|
||||||
|
BeatmapSet = new BeatmapSetInfo(),
|
||||||
|
Ruleset = r.All<RulesetInfo>().First(),
|
||||||
|
})
|
||||||
|
{
|
||||||
|
TotalScoreVersion = 30000002,
|
||||||
|
IsLegacyScore = true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor()));
|
||||||
|
|
||||||
|
AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True);
|
||||||
|
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000002));
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class TestBackgroundDataStoreProcessor : BackgroundDataStoreProcessor
|
||||||
{
|
{
|
||||||
protected override int TimeToSleepDuringGameplay => 10;
|
protected override int TimeToSleepDuringGameplay => 10;
|
||||||
}
|
}
|
@ -1,19 +1,23 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.IO;
|
using osu.Game.IO;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Database
|
namespace osu.Game.Tests.Database
|
||||||
{
|
{
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class LegacyBeatmapImporterTest
|
public class LegacyBeatmapImporterTest : RealmTest
|
||||||
{
|
{
|
||||||
private readonly TestLegacyBeatmapImporter importer = new TestLegacyBeatmapImporter();
|
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
|
private class TestLegacyBeatmapImporter : LegacyBeatmapImporter
|
||||||
{
|
{
|
||||||
public TestLegacyBeatmapImporter()
|
public TestLegacyBeatmapImporter()
|
||||||
|
@ -6,6 +6,7 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Cursor;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
@ -29,7 +30,7 @@ namespace osu.Game.Tests.Editing
|
|||||||
[Cached(typeof(IBeatSnapProvider))]
|
[Cached(typeof(IBeatSnapProvider))]
|
||||||
private readonly EditorBeatmap editorBeatmap;
|
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()
|
public TestSceneHitObjectComposerDistanceSnapping()
|
||||||
{
|
{
|
||||||
|
@ -13,7 +13,7 @@ layout(location = 4) out mediump vec2 v_BlendRange;
|
|||||||
void main(void)
|
void main(void)
|
||||||
{
|
{
|
||||||
// Transform from screen space to masking space.
|
// Transform from screen space to masking space.
|
||||||
highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0);
|
highp vec3 maskingPos = g_MaskingInfo.ToMaskingSpace * vec3(m_Position, 1.0);
|
||||||
v_MaskingPosition = maskingPos.xy / maskingPos.z;
|
v_MaskingPosition = maskingPos.xy / maskingPos.z;
|
||||||
|
|
||||||
v_Colour = m_Colour;
|
v_Colour = m_Colour;
|
||||||
|
@ -8,7 +8,7 @@ using System.Linq;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Cursor;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
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);
|
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;
|
private readonly IWorkingBeatmap working;
|
||||||
|
|
||||||
|
@ -185,6 +185,37 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddStep("shorten to -10 length", () => path.ExpectedDistance.Value = -10);
|
AddStep("shorten to -10 length", () => path.ExpectedDistance.Value = -10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestGetSegmentEnds()
|
||||||
|
{
|
||||||
|
var positions = new[]
|
||||||
|
{
|
||||||
|
Vector2.Zero,
|
||||||
|
new Vector2(100, 0),
|
||||||
|
new Vector2(100),
|
||||||
|
new Vector2(200, 100),
|
||||||
|
};
|
||||||
|
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(), () => 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(), () => 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(), () => 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),
|
||||||
|
new Vector2(100, 50),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
private List<PathControlPoint> createSegment(PathType type, params Vector2[] controlPoints)
|
private List<PathControlPoint> createSegment(PathType type, params Vector2[] controlPoints)
|
||||||
{
|
{
|
||||||
var points = controlPoints.Select(p => new PathControlPoint { Position = p }).ToList();
|
var points = controlPoints.Select(p => new PathControlPoint { Position = p }).ToList();
|
||||||
|
@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
foreach (var fountain in Children.OfType<StarFountain>())
|
foreach (var fountain in Children.OfType<StarFountain>())
|
||||||
{
|
{
|
||||||
if (RNG.NextSingle() > 0.8f)
|
if (RNG.NextSingle() > 0.8f)
|
||||||
fountain.Shoot();
|
fountain.Shoot(RNG.Next(-1, 2));
|
||||||
}
|
}
|
||||||
}, 150);
|
}, 150);
|
||||||
}
|
}
|
||||||
|
@ -2,27 +2,36 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps.Legacy;
|
using osu.Game.Beatmaps.Legacy;
|
||||||
using osu.Game.Tests.Visual;
|
|
||||||
using osu.Game.Tournament.Components;
|
using osu.Game.Tournament.Components;
|
||||||
using osu.Game.Tournament.Models;
|
using osu.Game.Tournament.Models;
|
||||||
|
|
||||||
namespace osu.Game.Tournament.Tests.Components
|
namespace osu.Game.Tournament.Tests.Components
|
||||||
{
|
{
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public partial class TestSceneSongBar : OsuTestScene
|
public partial class TestSceneSongBar : TournamentTestScene
|
||||||
{
|
{
|
||||||
[Cached]
|
|
||||||
private readonly LadderInfo ladder = new LadderInfo();
|
|
||||||
|
|
||||||
private SongBar songBar = null!;
|
private SongBar songBar = null!;
|
||||||
|
private TournamentBeatmap ladderBeatmap = null!;
|
||||||
|
|
||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public void SetUpSteps()
|
public override void SetUpSteps()
|
||||||
{
|
{
|
||||||
|
base.SetUpSteps();
|
||||||
|
|
||||||
|
AddStep("setup picks bans", () =>
|
||||||
|
{
|
||||||
|
ladderBeatmap = CreateSampleBeatmap();
|
||||||
|
Ladder.CurrentMatch.Value!.PicksBans.Add(new BeatmapChoice
|
||||||
|
{
|
||||||
|
BeatmapID = ladderBeatmap.OnlineID,
|
||||||
|
Team = TeamColour.Red,
|
||||||
|
Type = ChoiceType.Pick,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
AddStep("create bar", () => Child = songBar = new SongBar
|
AddStep("create bar", () => Child = songBar = new SongBar
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
@ -38,12 +47,14 @@ namespace osu.Game.Tournament.Tests.Components
|
|||||||
AddStep("set beatmap", () =>
|
AddStep("set beatmap", () =>
|
||||||
{
|
{
|
||||||
var beatmap = CreateAPIBeatmap(Ruleset.Value);
|
var beatmap = CreateAPIBeatmap(Ruleset.Value);
|
||||||
|
|
||||||
beatmap.CircleSize = 3.4f;
|
beatmap.CircleSize = 3.4f;
|
||||||
beatmap.ApproachRate = 6.8f;
|
beatmap.ApproachRate = 6.8f;
|
||||||
beatmap.OverallDifficulty = 5.5f;
|
beatmap.OverallDifficulty = 5.5f;
|
||||||
beatmap.StarRating = 4.56f;
|
beatmap.StarRating = 4.56f;
|
||||||
beatmap.Length = 123456;
|
beatmap.Length = 123456;
|
||||||
beatmap.BPM = 133;
|
beatmap.BPM = 133;
|
||||||
|
beatmap.OnlineID = ladderBeatmap.OnlineID;
|
||||||
|
|
||||||
songBar.Beatmap = new TournamentBeatmap(beatmap);
|
songBar.Beatmap = new TournamentBeatmap(beatmap);
|
||||||
});
|
});
|
||||||
|
@ -234,7 +234,7 @@ namespace osu.Game.Tournament.Components
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
new UnmaskedTournamentBeatmapPanel(beatmap)
|
new TournamentBeatmapPanel(beatmap)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Width = 0.5f,
|
Width = 0.5f,
|
||||||
@ -277,18 +277,4 @@ 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ namespace osu.Game.Tournament.Models
|
|||||||
|
|
||||||
public Bindable<int> LastYearPlacing = new BindableInt
|
public Bindable<int> LastYearPlacing = new BindableInt
|
||||||
{
|
{
|
||||||
MinValue = 1,
|
MinValue = 0,
|
||||||
MaxValue = 256
|
MaxValue = 256
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -10,7 +10,9 @@ using osu.Framework.Extensions;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Settings;
|
using osu.Game.Overlays.Settings;
|
||||||
using osu.Game.Tournament.Models;
|
using osu.Game.Tournament.Models;
|
||||||
@ -128,7 +130,7 @@ namespace osu.Game.Tournament.Screens.Editors
|
|||||||
Width = 0.2f,
|
Width = 0.2f,
|
||||||
Current = Model.Seed
|
Current = Model.Seed
|
||||||
},
|
},
|
||||||
new SettingsSlider<int>
|
new SettingsSlider<int, LastYearPlacementSlider>
|
||||||
{
|
{
|
||||||
LabelText = "Last Year Placement",
|
LabelText = "Last Year Placement",
|
||||||
Width = 0.33f,
|
Width = 0.33f,
|
||||||
@ -175,6 +177,11 @@ namespace osu.Game.Tournament.Screens.Editors
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private partial class LastYearPlacementSlider : RoundedSliderBar<int>
|
||||||
|
{
|
||||||
|
public override LocalisableString TooltipText => Current.Value == 0 ? "N/A" : base.TooltipText;
|
||||||
|
}
|
||||||
|
|
||||||
public partial class PlayerEditor : CompositeDrawable
|
public partial class PlayerEditor : CompositeDrawable
|
||||||
{
|
{
|
||||||
private readonly TournamentTeam team;
|
private readonly TournamentTeam team;
|
||||||
|
@ -274,7 +274,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
|
|||||||
new TeamDisplay(team) { Margin = new MarginPadding { Bottom = 30 } },
|
new TeamDisplay(team) { Margin = new MarginPadding { Bottom = 30 } },
|
||||||
new RowDisplay("Average Rank:", $"#{team.AverageRank:#,0}"),
|
new RowDisplay("Average Rank:", $"#{team.AverageRank:#,0}"),
|
||||||
new RowDisplay("Seed:", team.Seed.Value),
|
new RowDisplay("Seed:", team.Seed.Value),
|
||||||
new RowDisplay("Last year's placing:", team.LastYearPlacing.Value > 0 ? $"#{team.LastYearPlacing:#,0}" : "0"),
|
new RowDisplay("Last year's placing:", team.LastYearPlacing.Value > 0 ? $"#{team.LastYearPlacing:#,0}" : "N/A"),
|
||||||
new Container { Margin = new MarginPadding { Bottom = 30 } },
|
new Container { Margin = new MarginPadding { Bottom = 30 } },
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -24,7 +24,10 @@ using osu.Game.Screens.Play;
|
|||||||
|
|
||||||
namespace osu.Game
|
namespace osu.Game
|
||||||
{
|
{
|
||||||
public partial class BackgroundBeatmapProcessor : Component
|
/// <summary>
|
||||||
|
/// Performs background updating of data stores at startup.
|
||||||
|
/// </summary>
|
||||||
|
public partial class BackgroundDataStoreProcessor : Component
|
||||||
{
|
{
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private RulesetStore rulesetStore { get; set; } = null!;
|
private RulesetStore rulesetStore { get; set; } = null!;
|
||||||
@ -61,7 +64,8 @@ namespace osu.Game
|
|||||||
|
|
||||||
Task.Factory.StartNew(() =>
|
Task.Factory.StartNew(() =>
|
||||||
{
|
{
|
||||||
Logger.Log("Beginning background beatmap processing..");
|
Logger.Log("Beginning background data store processing..");
|
||||||
|
|
||||||
checkForOutdatedStarRatings();
|
checkForOutdatedStarRatings();
|
||||||
processBeatmapSetsWithMissingMetrics();
|
processBeatmapSetsWithMissingMetrics();
|
||||||
processScoresWithMissingStatistics();
|
processScoresWithMissingStatistics();
|
||||||
@ -74,7 +78,7 @@ namespace osu.Game
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Log("Finished background beatmap processing!");
|
Logger.Log("Finished background data store processing!");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,7 +186,7 @@ namespace osu.Game
|
|||||||
|
|
||||||
realmAccess.Run(r =>
|
realmAccess.Run(r =>
|
||||||
{
|
{
|
||||||
foreach (var score in r.All<ScoreInfo>())
|
foreach (var score in r.All<ScoreInfo>().Where(s => !s.BackgroundReprocessingFailed))
|
||||||
{
|
{
|
||||||
if (score.BeatmapInfo != null
|
if (score.BeatmapInfo != null
|
||||||
&& score.Statistics.Sum(kvp => kvp.Value) > 0
|
&& score.Statistics.Sum(kvp => kvp.Value) > 0
|
||||||
@ -221,6 +225,7 @@ namespace osu.Game
|
|||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}");
|
Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}");
|
||||||
|
realmAccess.Write(r => r.Find<ScoreInfo>(id)!.BackgroundReprocessingFailed = true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -230,7 +235,7 @@ namespace osu.Game
|
|||||||
Logger.Log("Querying for scores that need total score conversion...");
|
Logger.Log("Querying for scores that need total score conversion...");
|
||||||
|
|
||||||
HashSet<Guid> scoreIds = realmAccess.Run(r => new HashSet<Guid>(r.All<ScoreInfo>()
|
HashSet<Guid> scoreIds = realmAccess.Run(r => new HashSet<Guid>(r.All<ScoreInfo>()
|
||||||
.Where(s => s.BeatmapInfo != null && s.TotalScoreVersion == 30000002)
|
.Where(s => !s.BackgroundReprocessingFailed && s.BeatmapInfo != null && s.TotalScoreVersion == 30000002)
|
||||||
.AsEnumerable().Select(s => s.ID)));
|
.AsEnumerable().Select(s => s.ID)));
|
||||||
|
|
||||||
Logger.Log($"Found {scoreIds.Count} scores which require total score conversion.");
|
Logger.Log($"Found {scoreIds.Count} scores which require total score conversion.");
|
||||||
@ -279,6 +284,7 @@ namespace osu.Game
|
|||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Logger.Log($"Failed to convert total score for {id}: {e}");
|
Logger.Log($"Failed to convert total score for {id}: {e}");
|
||||||
|
realmAccess.Write(r => r.Find<ScoreInfo>(id)!.BackgroundReprocessingFailed = true);
|
||||||
++failedCount;
|
++failedCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -152,6 +152,8 @@ namespace osu.Game.Beatmaps
|
|||||||
if (archive != null)
|
if (archive != null)
|
||||||
beatmapSet.Beatmaps.AddRange(createBeatmapDifficulties(beatmapSet, realm));
|
beatmapSet.Beatmaps.AddRange(createBeatmapDifficulties(beatmapSet, realm));
|
||||||
|
|
||||||
|
beatmapSet.DateAdded = getDateAdded(archive);
|
||||||
|
|
||||||
foreach (BeatmapInfo b in beatmapSet.Beatmaps)
|
foreach (BeatmapInfo b in beatmapSet.Beatmaps)
|
||||||
{
|
{
|
||||||
b.BeatmapSet = beatmapSet;
|
b.BeatmapSet = beatmapSet;
|
||||||
@ -305,11 +307,36 @@ namespace osu.Game.Beatmaps
|
|||||||
return new BeatmapSetInfo
|
return new BeatmapSetInfo
|
||||||
{
|
{
|
||||||
OnlineID = beatmap.BeatmapInfo.BeatmapSet?.OnlineID ?? -1,
|
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>
|
/// <summary>
|
||||||
/// Create all required <see cref="BeatmapInfo"/>s for the provided archive.
|
/// Create all required <see cref="BeatmapInfo"/>s for the provided archive.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
|
||||||
using osu.Game.IO;
|
using osu.Game.IO;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
|
|
||||||
@ -45,7 +42,7 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
/// Register dependencies for use with static decoder classes.
|
/// Register dependencies for use with static decoder classes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="rulesets">A store containing all available rulesets (used by <see cref="LegacyBeatmapDecoder"/>).</param>
|
/// <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));
|
LegacyBeatmapDecoder.RulesetStore = rulesets ?? throw new ArgumentNullException(nameof(rulesets));
|
||||||
}
|
}
|
||||||
@ -63,7 +60,7 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
throw new IOException(@"Unknown decoder type");
|
throw new IOException(@"Unknown decoder type");
|
||||||
|
|
||||||
// start off with the first line of the file
|
// 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)
|
while (line != null && line.Length == 0)
|
||||||
{
|
{
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
@ -13,7 +11,7 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves the list of combo colours for presentation only.
|
/// Retrieves the list of combo colours for presentation only.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IReadOnlyList<Color4> ComboColours { get; }
|
IReadOnlyList<Color4>? ComboColours { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The list of custom combo colours.
|
/// The list of custom combo colours.
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable 618
|
#pragma warning disable 618
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@ -36,11 +34,11 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private const double control_point_leniency = 1;
|
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 LegacySampleBank defaultSampleBank;
|
||||||
private int defaultSampleVolume = 100;
|
private int defaultSampleVolume = 100;
|
||||||
@ -222,7 +220,7 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
case @"Mode":
|
case @"Mode":
|
||||||
int rulesetID = Parsing.ParseInt(pair.Value);
|
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)
|
switch (rulesetID)
|
||||||
{
|
{
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using JetBrains.Annotations;
|
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Beatmaps.Legacy;
|
using osu.Game.Beatmaps.Legacy;
|
||||||
@ -34,8 +31,7 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
|
|
||||||
private readonly IBeatmap beatmap;
|
private readonly IBeatmap beatmap;
|
||||||
|
|
||||||
[CanBeNull]
|
private readonly ISkin? skin;
|
||||||
private readonly ISkin skin;
|
|
||||||
|
|
||||||
private readonly int onlineRulesetID;
|
private readonly int onlineRulesetID;
|
||||||
|
|
||||||
@ -44,7 +40,7 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="beatmap">The beatmap to encode.</param>
|
/// <param name="beatmap">The beatmap to encode.</param>
|
||||||
/// <param name="skin">The beatmap's skin, used for encoding combo colours.</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.beatmap = beatmap;
|
||||||
this.skin = skin;
|
this.skin = skin;
|
||||||
@ -180,8 +176,8 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
|
|
||||||
writer.WriteLine("[TimingPoints]");
|
writer.WriteLine("[TimingPoints]");
|
||||||
|
|
||||||
SampleControlPoint lastRelevantSamplePoint = null;
|
SampleControlPoint? lastRelevantSamplePoint = null;
|
||||||
DifficultyControlPoint lastRelevantDifficultyPoint = null;
|
DifficultyControlPoint? lastRelevantDifficultyPoint = null;
|
||||||
|
|
||||||
// In osu!taiko and osu!mania, a scroll speed is stored as "slider velocity" in legacy formats.
|
// 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.
|
// 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;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
private LegacySampleBank toLegacySampleBank(string sampleBank)
|
private LegacySampleBank toLegacySampleBank(string? sampleBank)
|
||||||
{
|
{
|
||||||
switch (sampleBank?.ToLowerInvariant())
|
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)
|
if (hitSampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy)
|
||||||
return legacy.CustomSampleBank;
|
return legacy.CustomSampleBank;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@ -19,10 +17,10 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
{
|
{
|
||||||
public class LegacyStoryboardDecoder : LegacyDecoder<Storyboard>
|
public class LegacyStoryboardDecoder : LegacyDecoder<Storyboard>
|
||||||
{
|
{
|
||||||
private StoryboardSprite storyboardSprite;
|
private StoryboardSprite? storyboardSprite;
|
||||||
private CommandTimelineGroup timelineGroup;
|
private CommandTimelineGroup? timelineGroup;
|
||||||
|
|
||||||
private Storyboard storyboard;
|
private Storyboard storyboard = null!;
|
||||||
|
|
||||||
private readonly Dictionary<string, string> variables = new Dictionary<string, string>();
|
private readonly Dictionary<string, string> variables = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
@ -70,7 +70,22 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
hitObject.StartTime = Math.Floor(hitObject.StartTime);
|
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);
|
var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints);
|
||||||
|
|
||||||
|
@ -83,8 +83,9 @@ namespace osu.Game.Database
|
|||||||
/// 31 2023-06-26 Add Version and LegacyTotalScore to ScoreInfo, set Version to 30000002 and copy TotalScore into LegacyTotalScore for legacy scores.
|
/// 31 2023-06-26 Add Version and LegacyTotalScore to ScoreInfo, set Version to 30000002 and copy TotalScore into LegacyTotalScore for legacy scores.
|
||||||
/// 32 2023-07-09 Populate legacy scores with the ScoreV2 mod (and restore TotalScore to the legacy total for such scores) using replay files.
|
/// 32 2023-07-09 Populate legacy scores with the ScoreV2 mod (and restore TotalScore to the legacy total for such scores) using replay files.
|
||||||
/// 33 2023-08-16 Reset default chat toggle key binding to avoid conflict with newly added leaderboard toggle key binding.
|
/// 33 2023-08-16 Reset default chat toggle key binding to avoid conflict with newly added leaderboard toggle key binding.
|
||||||
|
/// 34 2023-08-21 Add BackgroundReprocessingFailed flag to ScoreInfo to track upgrade failures.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const int schema_version = 33;
|
private const int schema_version = 34;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
||||||
|
@ -40,8 +40,14 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
AddInternal(hoverClickSounds = new HoverClickSounds());
|
AddInternal(hoverClickSounds = new HoverClickSounds());
|
||||||
|
|
||||||
updateTextColour();
|
updateTextColour();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
Item.Action.BindDisabledChanged(_ => updateState(), true);
|
Item.Action.BindDisabledChanged(_ => updateState(), true);
|
||||||
|
FinishTransforms();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTextColour()
|
private void updateTextColour()
|
||||||
|
@ -35,6 +35,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
|||||||
|
|
||||||
public string Text
|
public string Text
|
||||||
{
|
{
|
||||||
|
get => Component.Text;
|
||||||
set => Component.Text = value;
|
set => Component.Text = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,6 +85,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
|||||||
Current.BindValueChanged(updateTextBoxFromSlider, true);
|
Current.BindValueChanged(updateTextBoxFromSlider, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TakeFocus() => GetContainingInputManager().ChangeFocus(textBox);
|
||||||
|
|
||||||
private bool updatingFromTextBox;
|
private bool updatingFromTextBox;
|
||||||
|
|
||||||
private void textChanged(ValueChangedEvent<string> change)
|
private void textChanged(ValueChangedEvent<string> change)
|
||||||
|
@ -21,7 +21,9 @@ namespace osu.Game.IO.Archives
|
|||||||
this.path = Path.GetFullPath(path);
|
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()
|
public override void Dispose()
|
||||||
{
|
{
|
||||||
|
@ -3,33 +3,26 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Localisation;
|
using osu.Game.Localisation;
|
||||||
|
|
||||||
namespace osu.Game.Input.Bindings
|
namespace osu.Game.Input.Bindings
|
||||||
{
|
{
|
||||||
public partial class GlobalActionContainer : DatabasedKeyBindingContainer<GlobalAction>, IHandleGlobalKeyboardInput
|
public partial class GlobalActionContainer : DatabasedKeyBindingContainer<GlobalAction>, IHandleGlobalKeyboardInput, IKeyBindingHandler<GlobalAction>
|
||||||
{
|
{
|
||||||
private readonly Drawable? handler;
|
private readonly IKeyBindingHandler<GlobalAction>? handler;
|
||||||
|
|
||||||
private InputManager? parentInputManager;
|
|
||||||
|
|
||||||
public GlobalActionContainer(OsuGameBase? game)
|
public GlobalActionContainer(OsuGameBase? game)
|
||||||
: base(matchingMode: KeyCombinationMatchingMode.Modifiers)
|
: base(matchingMode: KeyCombinationMatchingMode.Modifiers)
|
||||||
{
|
{
|
||||||
if (game is IKeyBindingHandler<GlobalAction>)
|
if (game is IKeyBindingHandler<GlobalAction> h)
|
||||||
handler = game;
|
handler = h;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override bool Prioritised => true;
|
||||||
{
|
|
||||||
base.LoadComplete();
|
|
||||||
|
|
||||||
parentInputManager = GetContainingInputManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
// IMPORTANT: Take care when changing order of the items in the enumerable.
|
// IMPORTANT: Take care when changing order of the items in the enumerable.
|
||||||
// It is used to decide the order of precedence, with the earlier items having higher precedence.
|
// It is used to decide the order of precedence, with the earlier items having higher precedence.
|
||||||
@ -105,6 +98,7 @@ namespace osu.Game.Input.Bindings
|
|||||||
// See https://github.com/ppy/osu-framework/blob/master/osu.Framework/Input/StateChanges/MouseScrollRelativeInput.cs#L37-L38.
|
// 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.MouseWheelRight }, GlobalAction.EditorCyclePreviousBeatSnapDivisor),
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.MouseWheelLeft }, GlobalAction.EditorCycleNextBeatSnapDivisor),
|
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[]
|
public IEnumerable<KeyBinding> InGameKeyBindings => new[]
|
||||||
@ -160,20 +154,9 @@ namespace osu.Game.Input.Bindings
|
|||||||
new KeyBinding(InputKey.F3, GlobalAction.MusicPlay)
|
new KeyBinding(InputKey.F3, GlobalAction.MusicPlay)
|
||||||
};
|
};
|
||||||
|
|
||||||
protected override IEnumerable<Drawable> KeyBindingInputQueue
|
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) => handler?.OnPressed(e) == true;
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
// To ensure the global actions are handled with priority, this GlobalActionContainer is actually placed after game content.
|
|
||||||
// It does not contain children as expected, so we need to forward the NonPositionalInputQueue from the parent input manager to correctly
|
|
||||||
// allow the whole game to handle these actions.
|
|
||||||
|
|
||||||
// An eventual solution to this hack is to create localised action containers for individual components like SongSelect, but this will take some rearranging.
|
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e) => handler?.OnReleased(e);
|
||||||
var inputQueue = parentInputManager?.NonPositionalInputQueue ?? base.KeyBindingInputQueue;
|
|
||||||
|
|
||||||
return handler != null ? inputQueue.Prepend(handler) : inputQueue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum GlobalAction
|
public enum GlobalAction
|
||||||
@ -378,5 +361,8 @@ namespace osu.Game.Input.Bindings
|
|||||||
|
|
||||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleInGameLeaderboard))]
|
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleInGameLeaderboard))]
|
||||||
ToggleInGameLeaderboard,
|
ToggleInGameLeaderboard,
|
||||||
|
|
||||||
|
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))]
|
||||||
|
EditorToggleRotateControl,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -344,6 +344,11 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString ExportReplay => new TranslatableString(getKey(@"export_replay"), @"Export replay");
|
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}";
|
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
19
osu.Game/Localisation/OnlinePlayStrings.cs
Normal file
19
osu.Game/Localisation/OnlinePlayStrings.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Localisation;
|
||||||
|
|
||||||
|
namespace osu.Game.Localisation
|
||||||
|
{
|
||||||
|
public static class OnlinePlayStrings
|
||||||
|
{
|
||||||
|
private const string prefix = @"osu.Game.Resources.Localisation.OnlinePlay";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Playlist durations longer than 2 weeks require an active osu!supporter tag."
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString SupporterOnlyDurationNotice => new TranslatableString(getKey(@"supporter_only_duration_notice"), @"Playlist durations longer than 2 weeks require an active osu!supporter tag.");
|
||||||
|
|
||||||
|
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||||
|
}
|
||||||
|
}
|
@ -1025,7 +1025,7 @@ namespace osu.Game
|
|||||||
|
|
||||||
loadComponentSingleFile(CreateHighPerformanceSession(), Add);
|
loadComponentSingleFile(CreateHighPerformanceSession(), Add);
|
||||||
|
|
||||||
loadComponentSingleFile(new BackgroundBeatmapProcessor(), Add);
|
loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add);
|
||||||
|
|
||||||
Add(difficultyRecommender);
|
Add(difficultyRecommender);
|
||||||
Add(externalLinkOpener = new ExternalLinkOpener());
|
Add(externalLinkOpener = new ExternalLinkOpener());
|
||||||
|
@ -392,17 +392,18 @@ namespace osu.Game
|
|||||||
{
|
{
|
||||||
SafeAreaOverrideEdges = SafeAreaOverrideEdges,
|
SafeAreaOverrideEdges = SafeAreaOverrideEdges,
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Child = CreateScalingContainer().WithChildren(new Drawable[]
|
Child = CreateScalingContainer().WithChild(globalBindings = new GlobalActionContainer(this)
|
||||||
{
|
{
|
||||||
(GlobalCursorDisplay = new GlobalCursorDisplay
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both
|
(GlobalCursorDisplay = new GlobalCursorDisplay
|
||||||
}).WithChild(content = new OsuTooltipContainer(GlobalCursorDisplay.MenuCursor)
|
{
|
||||||
{
|
RelativeSizeAxes = Axes.Both
|
||||||
RelativeSizeAxes = Axes.Both
|
}).WithChild(content = new OsuTooltipContainer(GlobalCursorDisplay.MenuCursor)
|
||||||
}),
|
{
|
||||||
// to avoid positional input being blocked by children, ensure the GlobalActionContainer is above everything.
|
RelativeSizeAxes = Axes.Both
|
||||||
globalBindings = new GlobalActionContainer(this)
|
}),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -40,11 +40,13 @@ namespace osu.Game.Rulesets.Objects
|
|||||||
|
|
||||||
private readonly List<Vector2> calculatedPath = new List<Vector2>();
|
private readonly List<Vector2> calculatedPath = new List<Vector2>();
|
||||||
private readonly List<double> cumulativeLength = new List<double>();
|
private readonly List<double> cumulativeLength = new List<double>();
|
||||||
private readonly List<int> segmentEnds = new List<int>();
|
|
||||||
private readonly Cached pathCache = new Cached();
|
private readonly Cached pathCache = new Cached();
|
||||||
|
|
||||||
private double calculatedLength;
|
private double calculatedLength;
|
||||||
|
|
||||||
|
private readonly List<int> segmentEnds = new List<int>();
|
||||||
|
private double[] segmentEndDistances = Array.Empty<double>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="SliderPath"/>.
|
/// Creates a new <see cref="SliderPath"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -196,13 +198,28 @@ namespace osu.Game.Rulesets.Objects
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </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()
|
public IEnumerable<double> GetSegmentEnds()
|
||||||
{
|
{
|
||||||
ensureValid();
|
ensureValid();
|
||||||
|
|
||||||
return segmentEnds.Select(i => cumulativeLength[i] / calculatedLength);
|
return segmentEndDistances.Select(d => d / Distance);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void invalidate()
|
private void invalidate()
|
||||||
@ -251,8 +268,11 @@ namespace osu.Game.Rulesets.Objects
|
|||||||
calculatedPath.Add(t);
|
calculatedPath.Add(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remember the index of the segment end
|
if (i > 0)
|
||||||
segmentEnds.Add(calculatedPath.Count - 1);
|
{
|
||||||
|
// Remember the index of the segment end
|
||||||
|
segmentEnds.Add(calculatedPath.Count - 1);
|
||||||
|
}
|
||||||
|
|
||||||
// Start the new segment at the current vertex
|
// Start the new segment at the current vertex
|
||||||
start = i;
|
start = i;
|
||||||
@ -298,6 +318,14 @@ namespace osu.Game.Rulesets.Objects
|
|||||||
cumulativeLength.Add(calculatedLength);
|
cumulativeLength.Add(calculatedLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store the distances of the segment ends now, because after shortening the indices may be out of range
|
||||||
|
segmentEndDistances = new double[segmentEnds.Count];
|
||||||
|
|
||||||
|
for (int i = 0; i < segmentEnds.Count; i++)
|
||||||
|
{
|
||||||
|
segmentEndDistances[i] = cumulativeLength[segmentEnds[i]];
|
||||||
|
}
|
||||||
|
|
||||||
if (ExpectedDistance.Value is double expectedDistance && calculatedLength != expectedDistance)
|
if (ExpectedDistance.Value is double expectedDistance && calculatedLength != expectedDistance)
|
||||||
{
|
{
|
||||||
// In osu-stable, if the last two control points of a slider are equal, extension is not performed.
|
// In osu-stable, if the last two control points of a slider are equal, extension is not performed.
|
||||||
@ -319,10 +347,6 @@ namespace osu.Game.Rulesets.Objects
|
|||||||
{
|
{
|
||||||
cumulativeLength.RemoveAt(cumulativeLength.Count - 1);
|
cumulativeLength.RemoveAt(cumulativeLength.Count - 1);
|
||||||
calculatedPath.RemoveAt(pathEndIndex--);
|
calculatedPath.RemoveAt(pathEndIndex--);
|
||||||
|
|
||||||
// Shorten the last segment to the expected distance
|
|
||||||
if (segmentEnds.Count > 0)
|
|
||||||
segmentEnds[^1]--;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
@ -25,6 +26,53 @@ namespace osu.Game.Rulesets.Objects
|
|||||||
/// <param name="sliderPath">The <see cref="SliderPath"/>.</param>
|
/// <param name="sliderPath">The <see cref="SliderPath"/>.</param>
|
||||||
/// <param name="positionalOffset">The positional offset of the resulting path. It should be added to the start position of this path.</param>
|
/// <param name="positionalOffset">The positional offset of the resulting path. It should be added to the start position of this path.</param>
|
||||||
public static void Reverse(this SliderPath sliderPath, out Vector2 positionalOffset)
|
public static void Reverse(this SliderPath sliderPath, out Vector2 positionalOffset)
|
||||||
|
{
|
||||||
|
var controlPoints = sliderPath.ControlPoints;
|
||||||
|
|
||||||
|
var inheritedLinearPoints = controlPoints.Where(p => sliderPath.PointsInSegment(p)[0].Type == PathType.Linear && p.Type is null).ToList();
|
||||||
|
|
||||||
|
// Inherited points after a linear point, as well as the first control point if it inherited,
|
||||||
|
// should be treated as linear points, so their types are temporarily changed to linear.
|
||||||
|
inheritedLinearPoints.ForEach(p => p.Type = PathType.Linear);
|
||||||
|
|
||||||
|
double[] segmentEnds = sliderPath.GetSegmentEnds().ToArray();
|
||||||
|
|
||||||
|
// Remove segments after the end of the slider.
|
||||||
|
for (int numSegmentsToRemove = segmentEnds.Count(se => se >= 1) - 1; numSegmentsToRemove > 0 && controlPoints.Count > 0;)
|
||||||
|
{
|
||||||
|
if (controlPoints.Last().Type is not null)
|
||||||
|
{
|
||||||
|
numSegmentsToRemove--;
|
||||||
|
segmentEnds = segmentEnds[..^1];
|
||||||
|
}
|
||||||
|
|
||||||
|
controlPoints.RemoveAt(controlPoints.Count - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore original control point types.
|
||||||
|
inheritedLinearPoints.ForEach(p => p.Type = null);
|
||||||
|
|
||||||
|
// Recalculate middle perfect curve control points at the end of the slider path.
|
||||||
|
if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PerfectCurve && controlPoints[^2].Type is null && segmentEnds.Any())
|
||||||
|
{
|
||||||
|
double lastSegmentStart = segmentEnds.Length > 1 ? segmentEnds[^2] : 0;
|
||||||
|
double lastSegmentEnd = segmentEnds[^1];
|
||||||
|
|
||||||
|
var circleArcPath = new List<Vector2>();
|
||||||
|
sliderPath.GetPathToProgress(circleArcPath, lastSegmentStart / lastSegmentEnd, 1);
|
||||||
|
|
||||||
|
controlPoints[^2].Position = circleArcPath[circleArcPath.Count / 2];
|
||||||
|
}
|
||||||
|
|
||||||
|
sliderPath.reverseControlPoints(out positionalOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reverses the order of the provided <see cref="SliderPath"/>'s <see cref="PathControlPoint"/>s.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sliderPath">The <see cref="SliderPath"/>.</param>
|
||||||
|
/// <param name="positionalOffset">The positional offset of the resulting path. It should be added to the start position of this path.</param>
|
||||||
|
private static void reverseControlPoints(this SliderPath sliderPath, out Vector2 positionalOffset)
|
||||||
{
|
{
|
||||||
var points = sliderPath.ControlPoints.ToArray();
|
var points = sliderPath.ControlPoints.ToArray();
|
||||||
positionalOffset = sliderPath.PositionAt(1);
|
positionalOffset = sliderPath.PositionAt(1);
|
||||||
|
@ -66,7 +66,7 @@ namespace osu.Game.Scoring
|
|||||||
/// If this does not match <see cref="LegacyScoreEncoder.LATEST_VERSION"/>,
|
/// If this does not match <see cref="LegacyScoreEncoder.LATEST_VERSION"/>,
|
||||||
/// the total score has not yet been updated to reflect the current scoring values.
|
/// the total score has not yet been updated to reflect the current scoring values.
|
||||||
///
|
///
|
||||||
/// See <see cref="BackgroundBeatmapProcessor"/>'s conversion logic.
|
/// See <see cref="BackgroundDataStoreProcessor"/>'s conversion logic.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// This may not match the version stored in the replay files.
|
/// This may not match the version stored in the replay files.
|
||||||
@ -81,6 +81,15 @@ namespace osu.Game.Scoring
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public long? LegacyTotalScore { get; set; }
|
public long? LegacyTotalScore { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If background processing of this beatmap failed in some way, this flag will become <c>true</c>.
|
||||||
|
/// Should be used to ensure we don't repeatedly attempt to reprocess the same scores each startup even though we already know they will fail.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// See https://github.com/ppy/osu/issues/24301 for one example of how this can occur (missing beatmap file on disk).
|
||||||
|
/// </remarks>
|
||||||
|
public bool BackgroundReprocessingFailed { get; set; }
|
||||||
|
|
||||||
public int MaxCombo { get; set; }
|
public int MaxCombo { get; set; }
|
||||||
|
|
||||||
public double Accuracy { get; set; }
|
public double Accuracy { get; set; }
|
||||||
|
107
osu.Game/Screens/Edit/Components/EditorToolButton.cs
Normal file
107
osu.Game/Screens/Edit/Components/EditorToolButton.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -34,7 +34,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
|
|
||||||
public Container<SelectionBlueprint<T>> SelectionBlueprints { get; private set; }
|
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>>();
|
private readonly Dictionary<T, SelectionBlueprint<T>> blueprintMap = new Dictionary<T, SelectionBlueprint<T>>();
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
[Resolved(CanBeNull = true)]
|
[Resolved(CanBeNull = true)]
|
||||||
protected IEditorChangeHandler ChangeHandler { get; private set; }
|
protected IEditorChangeHandler ChangeHandler { get; private set; }
|
||||||
|
|
||||||
protected SelectionRotationHandler RotationHandler { get; private set; }
|
public SelectionRotationHandler RotationHandler { get; private set; }
|
||||||
|
|
||||||
protected SelectionHandler()
|
protected SelectionHandler()
|
||||||
{
|
{
|
||||||
|
@ -126,12 +126,9 @@ namespace osu.Game.Screens
|
|||||||
private void load(ShaderManager manager)
|
private void load(ShaderManager manager)
|
||||||
{
|
{
|
||||||
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE));
|
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE));
|
||||||
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR));
|
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2_NO_MASKING, FragmentShaderDescriptor.BLUR));
|
||||||
|
|
||||||
loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE));
|
loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE));
|
||||||
|
|
||||||
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder"));
|
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder"));
|
||||||
|
|
||||||
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE));
|
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio.Track;
|
using osu.Framework.Audio.Track;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
|
|
||||||
@ -13,6 +13,9 @@ namespace osu.Game.Screens.Menu
|
|||||||
{
|
{
|
||||||
public partial class KiaiMenuFountains : BeatSyncedContainer
|
public partial class KiaiMenuFountains : BeatSyncedContainer
|
||||||
{
|
{
|
||||||
|
private StarFountain leftFountain = null!;
|
||||||
|
private StarFountain rightFountain = null!;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
@ -20,13 +23,13 @@ namespace osu.Game.Screens.Menu
|
|||||||
|
|
||||||
Children = new[]
|
Children = new[]
|
||||||
{
|
{
|
||||||
new StarFountain
|
leftFountain = new StarFountain
|
||||||
{
|
{
|
||||||
Anchor = Anchor.BottomLeft,
|
Anchor = Anchor.BottomLeft,
|
||||||
Origin = Anchor.BottomLeft,
|
Origin = Anchor.BottomLeft,
|
||||||
X = 250,
|
X = 250,
|
||||||
},
|
},
|
||||||
new StarFountain
|
rightFountain = new StarFountain
|
||||||
{
|
{
|
||||||
Anchor = Anchor.BottomRight,
|
Anchor = Anchor.BottomRight,
|
||||||
Origin = Anchor.BottomRight,
|
Origin = Anchor.BottomRight,
|
||||||
@ -58,8 +61,25 @@ namespace osu.Game.Screens.Menu
|
|||||||
if (lastTrigger != null && Clock.CurrentTime - lastTrigger < 500)
|
if (lastTrigger != null && Clock.CurrentTime - lastTrigger < 500)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
foreach (var fountain in Children.OfType<StarFountain>())
|
int direction = RNG.Next(-1, 2);
|
||||||
fountain.Shoot();
|
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
case -1:
|
||||||
|
leftFountain.Shoot(1);
|
||||||
|
rightFountain.Shoot(-1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 0:
|
||||||
|
leftFountain.Shoot(0);
|
||||||
|
rightFountain.Shoot(0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
leftFountain.Shoot(-1);
|
||||||
|
rightFountain.Shoot(1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
lastTrigger = Clock.CurrentTime;
|
lastTrigger = Clock.CurrentTime;
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ namespace osu.Game.Screens.Menu
|
|||||||
InternalChild = spewer = new StarFountainSpewer();
|
InternalChild = spewer = new StarFountainSpewer();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Shoot() => spewer.Shoot();
|
public void Shoot(int direction) => spewer.Shoot(direction);
|
||||||
|
|
||||||
protected override void SkinChanged(ISkinSource skin)
|
protected override void SkinChanged(ISkinSource skin)
|
||||||
{
|
{
|
||||||
@ -81,10 +81,10 @@ namespace osu.Game.Screens.Menu
|
|||||||
return lastShootDirection * x_velocity_from_direction * (float)(1 - 2 * (Clock.CurrentTime - lastShootTime!.Value) / shoot_duration) + getRandomVariance(x_velocity_random_variance);
|
return lastShootDirection * x_velocity_from_direction * (float)(1 - 2 * (Clock.CurrentTime - lastShootTime!.Value) / shoot_duration) + getRandomVariance(x_velocity_random_variance);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Shoot()
|
public void Shoot(int direction)
|
||||||
{
|
{
|
||||||
lastShootTime = Clock.CurrentTime;
|
lastShootTime = Clock.CurrentTime;
|
||||||
lastShootDirection = RNG.Next(-1, 2);
|
lastShootDirection = direction;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static float getRandomVariance(float variance) => RNG.NextSingle(-variance, variance);
|
private static float getRandomVariance(float variance) => RNG.NextSingle(-variance, variance);
|
||||||
|
@ -113,7 +113,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
|
|||||||
|
|
||||||
protected partial class Section : Container
|
protected partial class Section : Container
|
||||||
{
|
{
|
||||||
private readonly Container content;
|
private readonly ReverseChildIDFillFlowContainer<Drawable> content;
|
||||||
|
|
||||||
protected override Container<Drawable> Content => content;
|
protected override Container<Drawable> Content => content;
|
||||||
|
|
||||||
@ -135,10 +135,11 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
|
|||||||
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 12),
|
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 12),
|
||||||
Text = title.ToUpperInvariant(),
|
Text = title.ToUpperInvariant(),
|
||||||
},
|
},
|
||||||
content = new Container
|
content = new ReverseChildIDFillFlowContainer<Drawable>
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Y,
|
AutoSizeAxes = Axes.Y,
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Direction = FillDirection.Vertical
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -23,6 +23,7 @@ using osu.Game.Online.Rooms;
|
|||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Screens.OnlinePlay.Match.Components;
|
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osu.Game.Localisation;
|
||||||
|
|
||||||
namespace osu.Game.Screens.OnlinePlay.Playlists
|
namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||||
{
|
{
|
||||||
@ -80,6 +81,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
|||||||
private IBindable<APIUser> localUser = null!;
|
private IBindable<APIUser> localUser = null!;
|
||||||
|
|
||||||
private readonly Room room;
|
private readonly Room room;
|
||||||
|
private OsuSpriteText durationNoticeText = null!;
|
||||||
|
|
||||||
public MatchSettings(Room room)
|
public MatchSettings(Room room)
|
||||||
{
|
{
|
||||||
@ -141,14 +143,22 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
|||||||
},
|
},
|
||||||
new Section("Duration")
|
new Section("Duration")
|
||||||
{
|
{
|
||||||
Child = new Container
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
new Container
|
||||||
Height = 40,
|
|
||||||
Child = DurationField = new DurationDropdown
|
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X
|
RelativeSizeAxes = Axes.X,
|
||||||
}
|
Height = 40,
|
||||||
|
Child = DurationField = new DurationDropdown
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X
|
||||||
|
},
|
||||||
|
},
|
||||||
|
durationNoticeText = new OsuSpriteText
|
||||||
|
{
|
||||||
|
Alpha = 0,
|
||||||
|
Colour = colours.Yellow,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
new Section("Allowed attempts (across all playlist items)")
|
new Section("Allowed attempts (across all playlist items)")
|
||||||
@ -305,6 +315,17 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
|||||||
MaxAttempts.BindValueChanged(count => MaxAttemptsField.Text = count.NewValue?.ToString(), true);
|
MaxAttempts.BindValueChanged(count => MaxAttemptsField.Text = count.NewValue?.ToString(), true);
|
||||||
Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue ?? TimeSpan.FromMinutes(30), true);
|
Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue ?? TimeSpan.FromMinutes(30), true);
|
||||||
|
|
||||||
|
DurationField.Current.BindValueChanged(duration =>
|
||||||
|
{
|
||||||
|
if (hasValidDuration)
|
||||||
|
durationNoticeText.Hide();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
durationNoticeText.Show();
|
||||||
|
durationNoticeText.Text = OnlinePlayStrings.SupporterOnlyDurationNotice;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
localUser = api.LocalUser.GetBoundCopy();
|
localUser = api.LocalUser.GetBoundCopy();
|
||||||
localUser.BindValueChanged(populateDurations, true);
|
localUser.BindValueChanged(populateDurations, true);
|
||||||
|
|
||||||
@ -314,6 +335,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
|||||||
|
|
||||||
private void populateDurations(ValueChangedEvent<APIUser> user)
|
private void populateDurations(ValueChangedEvent<APIUser> user)
|
||||||
{
|
{
|
||||||
|
// roughly correct (see https://github.com/Humanizr/Humanizer/blob/18167e56c082449cc4fe805b8429e3127a7b7f93/readme.md?plain=1#L427)
|
||||||
|
// if we want this to be more accurate we might consider sending an actual end time, not a time span. probably not required though.
|
||||||
|
const int days_in_month = 31;
|
||||||
|
|
||||||
DurationField.Items = new[]
|
DurationField.Items = new[]
|
||||||
{
|
{
|
||||||
TimeSpan.FromMinutes(30),
|
TimeSpan.FromMinutes(30),
|
||||||
@ -326,18 +351,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
|||||||
TimeSpan.FromDays(3),
|
TimeSpan.FromDays(3),
|
||||||
TimeSpan.FromDays(7),
|
TimeSpan.FromDays(7),
|
||||||
TimeSpan.FromDays(14),
|
TimeSpan.FromDays(14),
|
||||||
|
TimeSpan.FromDays(days_in_month),
|
||||||
|
TimeSpan.FromDays(days_in_month * 3),
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: show these in the interface at all times.
|
|
||||||
if (user.NewValue.IsSupporter)
|
|
||||||
{
|
|
||||||
// roughly correct (see https://github.com/Humanizr/Humanizer/blob/18167e56c082449cc4fe805b8429e3127a7b7f93/readme.md?plain=1#L427)
|
|
||||||
// if we want this to be more accurate we might consider sending an actual end time, not a time span. probably not required though.
|
|
||||||
const int days_in_month = 31;
|
|
||||||
|
|
||||||
DurationField.AddDropdownItem(TimeSpan.FromDays(days_in_month));
|
|
||||||
DurationField.AddDropdownItem(TimeSpan.FromDays(days_in_month * 3));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
@ -352,7 +368,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
|||||||
private void onPlaylistChanged(object? sender, NotifyCollectionChangedEventArgs e) =>
|
private void onPlaylistChanged(object? sender, NotifyCollectionChangedEventArgs e) =>
|
||||||
playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}";
|
playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}";
|
||||||
|
|
||||||
private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0;
|
private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0
|
||||||
|
&& hasValidDuration;
|
||||||
|
|
||||||
|
private bool hasValidDuration => DurationField.Current.Value <= TimeSpan.FromDays(14) || localUser.Value.IsSupporter;
|
||||||
|
|
||||||
private void apply()
|
private void apply()
|
||||||
{
|
{
|
||||||
|
@ -232,6 +232,6 @@ namespace osu.Game.Skinning
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static Color4 getComboColour(IHasComboColours source, int colourIndex)
|
private static Color4 getComboColour(IHasComboColours source, int colourIndex)
|
||||||
=> source.ComboColours[colourIndex % source.ComboColours.Count];
|
=> source.ComboColours![colourIndex % source.ComboColours.Count];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -203,6 +203,6 @@ namespace osu.Game.Skinning
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static Color4 getComboColour(IHasComboColours source, int colourIndex)
|
private static Color4 getComboColour(IHasComboColours source, int colourIndex)
|
||||||
=> source.ComboColours[colourIndex % source.ComboColours.Count];
|
=> source.ComboColours![colourIndex % source.ComboColours.Count];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,13 @@ namespace osu.Game.Tests.Visual
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (CreateNestedActionContainer)
|
if (CreateNestedActionContainer)
|
||||||
mainContent.Add(new GlobalActionContainer(null));
|
{
|
||||||
|
var globalActionContainer = new GlobalActionContainer(null)
|
||||||
|
{
|
||||||
|
Child = mainContent
|
||||||
|
};
|
||||||
|
mainContent = globalActionContainer;
|
||||||
|
}
|
||||||
|
|
||||||
base.Content.AddRange(new Drawable[]
|
base.Content.AddRange(new Drawable[]
|
||||||
{
|
{
|
||||||
|
@ -36,8 +36,8 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Realm" Version="11.1.2" />
|
<PackageReference Include="Realm" Version="11.1.2" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2023.815.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2023.822.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.719.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.822.0" />
|
||||||
<PackageReference Include="Sentry" Version="3.28.1" />
|
<PackageReference Include="Sentry" Version="3.28.1" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.32.2" />
|
<PackageReference Include="SharpCompress" Version="0.32.2" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
|
@ -23,6 +23,6 @@
|
|||||||
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.815.0" />
|
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.817.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
Loading…
Reference in New Issue
Block a user