1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-06 02:22:58 +08:00

Merge branch 'master' into osu-selection-scaling

This commit is contained in:
Bartłomiej Dach 2020-10-04 13:28:59 +02:00 committed by GitHub
commit a366591d6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 368 additions and 117 deletions

View File

@ -9,7 +9,7 @@ using osu.Framework.Android;
namespace osu.Android namespace osu.Android
{ {
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullSensor, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)] [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
public class OsuGameActivity : AndroidGameActivity public class OsuGameActivity : AndroidGameActivity
{ {
protected override Framework.Game CreateGame() => new OsuGameAndroid(); protected override Framework.Game CreateGame() => new OsuGameAndroid();

View File

@ -2,6 +2,7 @@
// 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.Linq; using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -13,7 +14,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{ {
public class TestSceneHoldNote : ManiaHitObjectTestScene public class TestSceneHoldNote : ManiaHitObjectTestScene
{ {
public TestSceneHoldNote() [Test]
public void TestHoldNote()
{ {
AddToggleStep("toggle hitting", v => AddToggleStep("toggle hitting", v =>
{ {

View File

@ -28,8 +28,15 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestFixture] [TestFixture]
public class TestSceneNotes : OsuTestScene public class TestSceneNotes : OsuTestScene
{ {
[BackgroundDependencyLoader] [Test]
private void load() public void TestVariousNotes()
{
DrawableNote note1 = null;
DrawableNote note2 = null;
DrawableHoldNote holdNote1 = null;
DrawableHoldNote holdNote2 = null;
AddStep("create notes", () =>
{ {
Child = new FillFlowContainer Child = new FillFlowContainer
{ {
@ -41,12 +48,13 @@ namespace osu.Game.Rulesets.Mania.Tests
Spacing = new Vector2(20), Spacing = new Vector2(20),
Children = new[] Children = new[]
{ {
createNoteDisplay(ScrollingDirection.Down, 1, out var note1), createNoteDisplay(ScrollingDirection.Down, 1, out note1),
createNoteDisplay(ScrollingDirection.Up, 2, out var note2), createNoteDisplay(ScrollingDirection.Up, 2, out note2),
createHoldNoteDisplay(ScrollingDirection.Down, 1, out var holdNote1), createHoldNoteDisplay(ScrollingDirection.Down, 1, out holdNote1),
createHoldNoteDisplay(ScrollingDirection.Up, 2, out var holdNote2), createHoldNoteDisplay(ScrollingDirection.Up, 2, out holdNote2),
} }
}; };
});
AddAssert("note 1 facing downwards", () => verifyAnchors(note1, Anchor.y2)); AddAssert("note 1 facing downwards", () => verifyAnchors(note1, Anchor.y2));
AddAssert("note 2 facing upwards", () => verifyAnchors(note2, Anchor.y0)); AddAssert("note 2 facing upwards", () => verifyAnchors(note2, Anchor.y0));

View File

@ -20,7 +20,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
private int depthIndex; private int depthIndex;
public TestSceneHitCircle() [Test]
public void TestVariousHitCircles()
{ {
AddStep("Miss Big Single", () => SetContents(() => testSingle(2))); AddStep("Miss Big Single", () => SetContents(() => testSingle(2)));
AddStep("Miss Medium Single", () => SetContents(() => testSingle(5))); AddStep("Miss Medium Single", () => SetContents(() => testSingle(5)));

View File

@ -27,7 +27,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
private int depthIndex; private int depthIndex;
public TestSceneSlider() [Test]
public void TestVariousSliders()
{ {
AddStep("Big Single", () => SetContents(() => testSimpleBig())); AddStep("Big Single", () => SetContents(() => testSimpleBig()));
AddStep("Medium Single", () => SetContents(() => testSimpleMedium())); AddStep("Medium Single", () => SetContents(() => testSimpleMedium()));
@ -164,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
var slider = new Slider var slider = new Slider
{ {
StartTime = Time.Current + 1000, StartTime = Time.Current + time_offset,
Position = new Vector2(239, 176), Position = new Vector2(239, 176),
Path = new SliderPath(PathType.PerfectCurve, new[] Path = new SliderPath(PathType.PerfectCurve, new[]
{ {
@ -185,22 +186,26 @@ namespace osu.Game.Rulesets.Osu.Tests
private Drawable testSlowSpeed() => createSlider(speedMultiplier: 0.5); private Drawable testSlowSpeed() => createSlider(speedMultiplier: 0.5);
private Drawable testShortSlowSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 0.5); private Drawable testShortSlowSpeed(int repeats = 0) => createSlider(distance: max_length / 4, repeats: repeats, speedMultiplier: 0.5);
private Drawable testHighSpeed(int repeats = 0) => createSlider(repeats: repeats, speedMultiplier: 15); private Drawable testHighSpeed(int repeats = 0) => createSlider(repeats: repeats, speedMultiplier: 15);
private Drawable testShortHighSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 15); private Drawable testShortHighSpeed(int repeats = 0) => createSlider(distance: max_length / 4, repeats: repeats, speedMultiplier: 15);
private Drawable createSlider(float circleSize = 2, float distance = 400, int repeats = 0, double speedMultiplier = 2, int stackHeight = 0) private const double time_offset = 1500;
private const float max_length = 200;
private Drawable createSlider(float circleSize = 2, float distance = max_length, int repeats = 0, double speedMultiplier = 2, int stackHeight = 0)
{ {
var slider = new Slider var slider = new Slider
{ {
StartTime = Time.Current + 1000, StartTime = Time.Current + time_offset,
Position = new Vector2(-(distance / 2), 0), Position = new Vector2(0, -(distance / 2)),
Path = new SliderPath(PathType.PerfectCurve, new[] Path = new SliderPath(PathType.PerfectCurve, new[]
{ {
Vector2.Zero, Vector2.Zero,
new Vector2(distance, 0), new Vector2(0, distance),
}, distance), }, distance),
RepeatCount = repeats, RepeatCount = repeats,
StackHeight = stackHeight StackHeight = stackHeight
@ -213,14 +218,14 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
var slider = new Slider var slider = new Slider
{ {
StartTime = Time.Current + 1000, StartTime = Time.Current + time_offset,
Position = new Vector2(-200, 0), Position = new Vector2(-max_length / 2, 0),
Path = new SliderPath(PathType.PerfectCurve, new[] Path = new SliderPath(PathType.PerfectCurve, new[]
{ {
Vector2.Zero, Vector2.Zero,
new Vector2(200, 200), new Vector2(max_length / 2, max_length / 2),
new Vector2(400, 0) new Vector2(max_length, 0)
}, 600), }, max_length * 1.5f),
RepeatCount = repeats, RepeatCount = repeats,
}; };
@ -233,16 +238,16 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
var slider = new Slider var slider = new Slider
{ {
StartTime = Time.Current + 1000, StartTime = Time.Current + time_offset,
Position = new Vector2(-200, 0), Position = new Vector2(-max_length / 2, 0),
Path = new SliderPath(PathType.Linear, new[] Path = new SliderPath(PathType.Linear, new[]
{ {
Vector2.Zero, Vector2.Zero,
new Vector2(150, 75), new Vector2(max_length * 0.375f, max_length * 0.18f),
new Vector2(200, 0), new Vector2(max_length / 2, 0),
new Vector2(300, -200), new Vector2(max_length * 0.75f, -max_length / 2),
new Vector2(400, 0), new Vector2(max_length * 0.95f, 0),
new Vector2(430, 0) new Vector2(max_length, 0)
}), }),
RepeatCount = repeats, RepeatCount = repeats,
}; };
@ -256,15 +261,15 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
var slider = new Slider var slider = new Slider
{ {
StartTime = Time.Current + 1000, StartTime = Time.Current + time_offset,
Position = new Vector2(-200, 0), Position = new Vector2(-max_length / 2, 0),
Path = new SliderPath(PathType.Bezier, new[] Path = new SliderPath(PathType.Bezier, new[]
{ {
Vector2.Zero, Vector2.Zero,
new Vector2(150, 75), new Vector2(max_length * 0.375f, max_length * 0.18f),
new Vector2(200, 100), new Vector2(max_length / 2, max_length / 4),
new Vector2(300, -200), new Vector2(max_length * 0.75f, -max_length / 2),
new Vector2(430, 0) new Vector2(max_length, 0)
}), }),
RepeatCount = repeats, RepeatCount = repeats,
}; };
@ -278,16 +283,16 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
var slider = new Slider var slider = new Slider
{ {
StartTime = Time.Current + 1000, StartTime = Time.Current + time_offset,
Position = new Vector2(0, 0), Position = new Vector2(0, 0),
Path = new SliderPath(PathType.Linear, new[] Path = new SliderPath(PathType.Linear, new[]
{ {
Vector2.Zero, Vector2.Zero,
new Vector2(-200, 0), new Vector2(-max_length / 2, 0),
new Vector2(0, 0), new Vector2(0, 0),
new Vector2(0, -200), new Vector2(0, -max_length / 2),
new Vector2(-200, -200), new Vector2(-max_length / 2, -max_length / 2),
new Vector2(0, -200) new Vector2(0, -max_length / 2)
}), }),
RepeatCount = repeats, RepeatCount = repeats,
}; };
@ -305,14 +310,14 @@ namespace osu.Game.Rulesets.Osu.Tests
var slider = new Slider var slider = new Slider
{ {
StartTime = Time.Current + 1000, StartTime = Time.Current + time_offset,
Position = new Vector2(-100, 0), Position = new Vector2(-max_length / 4, 0),
Path = new SliderPath(PathType.Catmull, new[] Path = new SliderPath(PathType.Catmull, new[]
{ {
Vector2.Zero, Vector2.Zero,
new Vector2(50, -50), new Vector2(max_length * 0.125f, max_length * 0.125f),
new Vector2(150, 50), new Vector2(max_length * 0.375f, max_length * 0.125f),
new Vector2(200, 0) new Vector2(max_length / 2, 0)
}), }),
RepeatCount = repeats, RepeatCount = repeats,
NodeSamples = repeatSamples NodeSamples = repeatSamples

View File

@ -6,6 +6,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
@ -25,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(TextureStore textures) private void load(TextureStore textures, DrawableHitObject drawableHitObject)
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Origin = Anchor.Centre, Origin = Anchor.Centre,
Texture = textures.Get(@"Gameplay/osu/disc"), Texture = textures.Get(@"Gameplay/osu/disc"),
}, },
new TrianglesPiece new TrianglesPiece((int)drawableHitObject.HitObject.StartTime)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive, Blending = BlendingParameters.Additive,

View File

@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private Spinner spinner; private Spinner spinner;
private const float initial_scale = 1.3f;
private const float idle_alpha = 0.2f; private const float idle_alpha = 0.2f;
private const float tracking_alpha = 0.4f; private const float tracking_alpha = 0.4f;
@ -41,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
// we are slightly bigger than our parent, to clip the top and bottom of the circle // we are slightly bigger than our parent, to clip the top and bottom of the circle
// this should probably be revisited when scaled spinners are a thing. // this should probably be revisited when scaled spinners are a thing.
Scale = new Vector2(1.3f); Scale = new Vector2(initial_scale);
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
Origin = Anchor.Centre; Origin = Anchor.Centre;
@ -93,6 +94,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
drawableSpinner.RotationTracker.Complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200)); drawableSpinner.RotationTracker.Complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200));
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
} }
protected override void Update() protected override void Update()
@ -115,8 +118,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
fill.Alpha = (float)Interpolation.Damp(fill.Alpha, drawableSpinner.RotationTracker.Tracking ? tracking_alpha : idle_alpha, 0.98f, (float)Math.Abs(Clock.ElapsedFrameTime)); fill.Alpha = (float)Interpolation.Damp(fill.Alpha, drawableSpinner.RotationTracker.Tracking ? tracking_alpha : idle_alpha, 0.98f, (float)Math.Abs(Clock.ElapsedFrameTime));
} }
const float initial_scale = 0.2f; const float initial_fill_scale = 0.2f;
float targetScale = initial_scale + (1 - initial_scale) * drawableSpinner.Progress; float targetScale = initial_fill_scale + (1 - initial_fill_scale) * drawableSpinner.Progress;
fill.Scale = new Vector2((float)Interpolation.Lerp(fill.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1))); fill.Scale = new Vector2((float)Interpolation.Lerp(fill.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1)));
mainContainer.Rotation = drawableSpinner.RotationTracker.Rotation; mainContainer.Rotation = drawableSpinner.RotationTracker.Rotation;
@ -124,14 +127,43 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{ {
centre.ScaleTo(0); if (!(drawableHitObject is DrawableSpinner))
mainContainer.ScaleTo(0); return;
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true)) using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
{
this.ScaleTo(initial_scale);
this.RotateTo(0);
using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
{ {
// constant ambient rotation to give the spinner "spinning" character. // constant ambient rotation to give the spinner "spinning" character.
this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration); this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration);
}
using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset, true))
{
switch (state)
{
case ArmedState.Hit:
this.ScaleTo(initial_scale * 1.2f, 320, Easing.Out);
this.RotateTo(mainContainer.Rotation + 180, 320);
break;
case ArmedState.Miss:
this.ScaleTo(initial_scale * 0.8f, 320, Easing.In);
break;
}
}
}
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
{
centre.ScaleTo(0);
mainContainer.ScaleTo(0);
using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
{
centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint); centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint);
mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint); mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint);
@ -141,24 +173,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint); mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint);
} }
} }
}
// transforms we have from completing the spinner will be rolled back, so reapply immediately. // transforms we have from completing the spinner will be rolled back, so reapply immediately.
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
updateComplete(state == ArmedState.Hit, 0); updateComplete(state == ArmedState.Hit, 0);
using (BeginDelayedSequence(spinner.Duration, true))
{
switch (state)
{
case ArmedState.Hit:
this.ScaleTo(Scale * 1.2f, 320, Easing.Out);
this.RotateTo(mainContainer.Rotation + 180, 320);
break;
case ArmedState.Miss:
this.ScaleTo(Scale * 0.8f, 320, Easing.In);
break;
}
}
} }
private void updateComplete(bool complete, double duration) private void updateComplete(bool complete, double duration)

View File

@ -11,7 +11,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
protected override bool CreateNewTriangles => false; protected override bool CreateNewTriangles => false;
protected override float SpawnRatio => 0.5f; protected override float SpawnRatio => 0.5f;
public TrianglesPiece() public TrianglesPiece(int? seed = null)
: base(seed)
{ {
TriangleScale = 1.2f; TriangleScale = 1.2f;
HideAlphaDiscrepancies = false; HideAlphaDiscrepancies = false;

View File

@ -70,21 +70,31 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
base.LoadComplete(); base.LoadComplete();
this.FadeOut();
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
} }
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{ {
if (!(drawableHitObject is DrawableSpinner))
return;
var spinner = (Spinner)drawableSpinner.HitObject; var spinner = (Spinner)drawableSpinner.HitObject;
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
this.FadeOut();
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true)) using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true))
this.FadeInFromZero(spinner.TimeFadeIn / 2); this.FadeInFromZero(spinner.TimeFadeIn / 2);
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
{
fixedMiddle.FadeColour(Color4.White); fixedMiddle.FadeColour(Color4.White);
using (BeginAbsoluteSequence(spinner.StartTime, true))
using (BeginDelayedSequence(spinner.TimePreempt, true))
fixedMiddle.FadeColour(Color4.Red, spinner.Duration); fixedMiddle.FadeColour(Color4.Red, spinner.Duration);
} }
}
protected override void Update() protected override void Update()
{ {

View File

@ -24,12 +24,16 @@ namespace osu.Game.Rulesets.Osu.Skinning
private Sprite metreSprite; private Sprite metreSprite;
private Container metre; private Container metre;
private bool spinnerBlink;
private const float sprite_scale = 1 / 1.6f; private const float sprite_scale = 1 / 1.6f;
private const float final_metre_height = 692 * sprite_scale; private const float final_metre_height = 692 * sprite_scale;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISkinSource source, DrawableHitObject drawableObject) private void load(ISkinSource source, DrawableHitObject drawableObject)
{ {
spinnerBlink = source.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.SpinnerNoBlink)?.Value != true;
drawableSpinner = (DrawableSpinner)drawableObject; drawableSpinner = (DrawableSpinner)drawableObject;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -84,14 +88,20 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
base.LoadComplete(); base.LoadComplete();
this.FadeOut();
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
} }
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{ {
if (!(drawableHitObject is DrawableSpinner))
return;
var spinner = drawableSpinner.HitObject; var spinner = drawableSpinner.HitObject;
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
this.FadeOut();
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true)) using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true))
this.FadeInFromZero(spinner.TimeFadeIn / 2); this.FadeInFromZero(spinner.TimeFadeIn / 2);
} }
@ -116,12 +126,15 @@ namespace osu.Game.Rulesets.Osu.Skinning
private float getMetreHeight(float progress) private float getMetreHeight(float progress)
{ {
progress = Math.Min(99, progress * 100); progress *= 100;
// the spinner should still blink at 100% progress.
if (spinnerBlink)
progress = Math.Min(99, progress);
int barCount = (int)progress / 10; int barCount = (int)progress / 10;
// todo: add SpinnerNoBlink support if (spinnerBlink && RNG.NextBool(((int)progress % 10) / 10f))
if (RNG.NextBool(((int)progress % 10) / 10f))
barCount++; barCount++;
return (float)barCount / total_bars * final_metre_height; return (float)barCount / total_bars * final_metre_height;

View File

@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
CursorRotate, CursorRotate,
HitCircleOverlayAboveNumber, HitCircleOverlayAboveNumber,
HitCircleOverlayAboveNumer, // Some old skins will have this typo HitCircleOverlayAboveNumer, // Some old skins will have this typo
SpinnerFrequencyModulate SpinnerFrequencyModulate,
SpinnerNoBlink
} }
} }

View File

@ -3,7 +3,6 @@
using System; using System;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -30,8 +29,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
private readonly Random rng = new Random(1337); private readonly Random rng = new Random(1337);
[BackgroundDependencyLoader] [Test]
private void load() public void TestVariousHits()
{ {
AddStep("Hit", () => addHitJudgement(false)); AddStep("Hit", () => addHitJudgement(false));
AddStep("Strong hit", () => addStrongHitJudgement(false)); AddStep("Strong hit", () => addStrongHitJudgement(false));

View File

@ -12,6 +12,14 @@ namespace osu.Game.Tests.Editing
[TestFixture] [TestFixture]
public class EditorChangeHandlerTest public class EditorChangeHandlerTest
{ {
private int stateChangedFired;
[SetUp]
public void SetUp()
{
stateChangedFired = 0;
}
[Test] [Test]
public void TestSaveRestoreState() public void TestSaveRestoreState()
{ {
@ -23,6 +31,8 @@ namespace osu.Game.Tests.Editing
addArbitraryChange(beatmap); addArbitraryChange(beatmap);
handler.SaveState(); handler.SaveState();
Assert.That(stateChangedFired, Is.EqualTo(1));
Assert.That(handler.CanUndo.Value, Is.True); Assert.That(handler.CanUndo.Value, Is.True);
Assert.That(handler.CanRedo.Value, Is.False); Assert.That(handler.CanRedo.Value, Is.False);
@ -30,6 +40,8 @@ namespace osu.Game.Tests.Editing
Assert.That(handler.CanUndo.Value, Is.False); Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.True); Assert.That(handler.CanRedo.Value, Is.True);
Assert.That(stateChangedFired, Is.EqualTo(2));
} }
[Test] [Test]
@ -45,6 +57,7 @@ namespace osu.Game.Tests.Editing
Assert.That(handler.CanUndo.Value, Is.True); Assert.That(handler.CanUndo.Value, Is.True);
Assert.That(handler.CanRedo.Value, Is.False); Assert.That(handler.CanRedo.Value, Is.False);
Assert.That(stateChangedFired, Is.EqualTo(1));
string hash = handler.CurrentStateHash; string hash = handler.CurrentStateHash;
@ -52,6 +65,7 @@ namespace osu.Game.Tests.Editing
handler.SaveState(); handler.SaveState();
Assert.That(hash, Is.EqualTo(handler.CurrentStateHash)); Assert.That(hash, Is.EqualTo(handler.CurrentStateHash));
Assert.That(stateChangedFired, Is.EqualTo(1));
handler.RestoreState(-1); handler.RestoreState(-1);
@ -60,6 +74,7 @@ namespace osu.Game.Tests.Editing
// we should only be able to restore once even though we saved twice. // we should only be able to restore once even though we saved twice.
Assert.That(handler.CanUndo.Value, Is.False); Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.True); Assert.That(handler.CanRedo.Value, Is.True);
Assert.That(stateChangedFired, Is.EqualTo(2));
} }
[Test] [Test]
@ -71,6 +86,8 @@ namespace osu.Game.Tests.Editing
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++) for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
{ {
Assert.That(stateChangedFired, Is.EqualTo(i));
addArbitraryChange(beatmap); addArbitraryChange(beatmap);
handler.SaveState(); handler.SaveState();
} }
@ -114,7 +131,10 @@ namespace osu.Game.Tests.Editing
{ {
var beatmap = new EditorBeatmap(new Beatmap()); var beatmap = new EditorBeatmap(new Beatmap());
return (new EditorChangeHandler(beatmap), beatmap); var changeHandler = new EditorChangeHandler(beatmap);
changeHandler.OnStateChange += () => stateChangedFired++;
return (changeHandler, beatmap);
} }
private void addArbitraryChange(EditorBeatmap beatmap) private void addArbitraryChange(EditorBeatmap beatmap)

View File

@ -139,6 +139,22 @@ namespace osu.Game.Tests.NonVisual
Assert.That(cpi.Groups.Count, Is.EqualTo(0)); Assert.That(cpi.Groups.Count, Is.EqualTo(0));
} }
[Test]
public void TestRemoveGroupAlsoRemovedControlPoints()
{
var cpi = new ControlPointInfo();
var group = cpi.GroupAt(1000, true);
group.Add(new SampleControlPoint());
Assert.That(cpi.SamplePoints.Count, Is.EqualTo(1));
cpi.RemoveGroup(group);
Assert.That(cpi.SamplePoints.Count, Is.EqualTo(0));
}
[Test] [Test]
public void TestAddControlPointToGroup() public void TestAddControlPointToGroup()
{ {

View File

@ -158,6 +158,9 @@ namespace osu.Game.Beatmaps.ControlPoints
public void RemoveGroup(ControlPointGroup group) public void RemoveGroup(ControlPointGroup group)
{ {
foreach (var item in group.ControlPoints.ToArray())
group.Remove(item);
group.ItemAdded -= groupItemAdded; group.ItemAdded -= groupItemAdded;
group.ItemRemoved -= groupItemRemoved; group.ItemRemoved -= groupItemRemoved;

View File

@ -86,13 +86,24 @@ namespace osu.Game.Graphics.Backgrounds
/// </summary> /// </summary>
public float Velocity = 1; public float Velocity = 1;
private readonly Random stableRandom;
private float nextRandom() => (float)(stableRandom?.NextDouble() ?? RNG.NextSingle());
private readonly SortedList<TriangleParticle> parts = new SortedList<TriangleParticle>(Comparer<TriangleParticle>.Default); private readonly SortedList<TriangleParticle> parts = new SortedList<TriangleParticle>(Comparer<TriangleParticle>.Default);
private IShader shader; private IShader shader;
private readonly Texture texture; private readonly Texture texture;
public Triangles() /// <summary>
/// Construct a new triangle visualisation.
/// </summary>
/// <param name="seed">An optional seed to stabilise random positions / attributes. Note that this does not guarantee stable playback when seeking in time.</param>
public Triangles(int? seed = null)
{ {
if (seed != null)
stableRandom = new Random(seed.Value);
texture = Texture.WhitePixel; texture = Texture.WhitePixel;
} }
@ -175,8 +186,8 @@ namespace osu.Game.Graphics.Backgrounds
{ {
TriangleParticle particle = CreateTriangle(); TriangleParticle particle = CreateTriangle();
particle.Position = new Vector2(RNG.NextSingle(), randomY ? RNG.NextSingle() : 1); particle.Position = new Vector2(nextRandom(), randomY ? nextRandom() : 1);
particle.ColourShade = RNG.NextSingle(); particle.ColourShade = nextRandom();
particle.Colour = CreateTriangleShade(particle.ColourShade); particle.Colour = CreateTriangleShade(particle.ColourShade);
return particle; return particle;
@ -191,8 +202,8 @@ namespace osu.Game.Graphics.Backgrounds
const float std_dev = 0.16f; const float std_dev = 0.16f;
const float mean = 0.5f; const float mean = 0.5f;
float u1 = 1 - RNG.NextSingle(); //uniform(0,1] random floats float u1 = 1 - nextRandom(); //uniform(0,1] random floats
float u2 = 1 - RNG.NextSingle(); float u2 = 1 - nextRandom();
float randStdNormal = (float)(Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2)); // random normal(0,1) float randStdNormal = (float)(Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Sin(2.0 * Math.PI * u2)); // random normal(0,1)
var scale = Math.Max(triangleScale * (mean + std_dev * randStdNormal), 0.1f); // random normal(mean,stdDev^2) var scale = Math.Max(triangleScale * (mean + std_dev * randStdNormal), 0.1f); // random normal(mean,stdDev^2)

View File

@ -40,17 +40,28 @@ namespace osu.Game.Rulesets.Edit
Playfield.DisplayJudgements.Value = false; Playfield.DisplayJudgements.Value = false;
} }
[Resolved(canBeNull: true)]
private IEditorChangeHandler changeHandler { get; set; }
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
beatmap.HitObjectAdded += addHitObject; beatmap.HitObjectAdded += addHitObject;
beatmap.HitObjectUpdated += updateReplay;
beatmap.HitObjectRemoved += removeHitObject; beatmap.HitObjectRemoved += removeHitObject;
if (changeHandler != null)
{
// for now only regenerate replay on a finalised state change, not HitObjectUpdated.
changeHandler.OnStateChange += updateReplay;
}
else
{
beatmap.HitObjectUpdated += _ => updateReplay();
}
} }
private void updateReplay(HitObject obj = null) => private void updateReplay() => drawableRuleset.RegenerateAutoplay();
drawableRuleset.RegenerateAutoplay();
private void addHitObject(HitObject hitObject) private void addHitObject(HitObject hitObject)
{ {
@ -58,8 +69,6 @@ namespace osu.Game.Rulesets.Edit
drawableRuleset.Playfield.Add(drawableObject); drawableRuleset.Playfield.Add(drawableObject);
drawableRuleset.Playfield.PostProcess(); drawableRuleset.Playfield.PostProcess();
updateReplay();
} }
private void removeHitObject(HitObject hitObject) private void removeHitObject(HitObject hitObject)
@ -68,8 +77,6 @@ namespace osu.Game.Rulesets.Edit
drawableRuleset.Playfield.Remove(drawableObject); drawableRuleset.Playfield.Remove(drawableObject);
drawableRuleset.Playfield.PostProcess(); drawableRuleset.Playfield.PostProcess();
drawableRuleset.RegenerateAutoplay();
} }
public override bool PropagatePositionalInputSubTree => false; public override bool PropagatePositionalInputSubTree => false;

View File

@ -51,12 +51,12 @@ namespace osu.Game.Rulesets.Objects.Drawables
public override bool PropagateNonPositionalInputSubTree => HandleUserInput; public override bool PropagateNonPositionalInputSubTree => HandleUserInput;
/// <summary> /// <summary>
/// Invoked when a <see cref="JudgementResult"/> has been applied by this <see cref="DrawableHitObject"/> or a nested <see cref="DrawableHitObject"/>. /// Invoked by this or a nested <see cref="DrawableHitObject"/> after a <see cref="JudgementResult"/> has been applied.
/// </summary> /// </summary>
public event Action<DrawableHitObject, JudgementResult> OnNewResult; public event Action<DrawableHitObject, JudgementResult> OnNewResult;
/// <summary> /// <summary>
/// Invoked when a <see cref="JudgementResult"/> is being reverted by this <see cref="DrawableHitObject"/> or a nested <see cref="DrawableHitObject"/>. /// Invoked by this or a nested <see cref="DrawableHitObject"/> prior to a <see cref="JudgementResult"/> being reverted.
/// </summary> /// </summary>
public event Action<DrawableHitObject, JudgementResult> OnRevertResult; public event Action<DrawableHitObject, JudgementResult> OnRevertResult;
@ -236,7 +236,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
#region State / Transform Management #region State / Transform Management
/// <summary> /// <summary>
/// Bind to apply a custom state which can override the default implementation. /// Invoked by this or a nested <see cref="DrawableHitObject"/> to apply a custom state that can override the default implementation.
/// </summary> /// </summary>
public event Action<DrawableHitObject, ArmedState> ApplyCustomUpdateState; public event Action<DrawableHitObject, ArmedState> ApplyCustomUpdateState;
@ -469,6 +469,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// <param name="application">The callback that applies changes to the <see cref="JudgementResult"/>.</param> /// <param name="application">The callback that applies changes to the <see cref="JudgementResult"/>.</param>
protected void ApplyResult(Action<JudgementResult> application) protected void ApplyResult(Action<JudgementResult> application)
{ {
if (Result.HasResult)
throw new InvalidOperationException("Cannot apply result on a hitobject that already has a result.");
application?.Invoke(Result); application?.Invoke(Result);
if (!Result.HasResult) if (!Result.HasResult)

View File

@ -21,6 +21,8 @@ namespace osu.Game.Screens.Edit
public readonly Bindable<bool> CanUndo = new Bindable<bool>(); public readonly Bindable<bool> CanUndo = new Bindable<bool>();
public readonly Bindable<bool> CanRedo = new Bindable<bool>(); public readonly Bindable<bool> CanRedo = new Bindable<bool>();
public event Action OnStateChange;
private readonly LegacyEditorBeatmapPatcher patcher; private readonly LegacyEditorBeatmapPatcher patcher;
private readonly List<byte[]> savedStates = new List<byte[]>(); private readonly List<byte[]> savedStates = new List<byte[]>();
@ -109,6 +111,8 @@ namespace osu.Game.Screens.Edit
savedStates.Add(newState); savedStates.Add(newState);
currentState = savedStates.Count - 1; currentState = savedStates.Count - 1;
OnStateChange?.Invoke();
updateBindables(); updateBindables();
} }
} }
@ -136,6 +140,7 @@ namespace osu.Game.Screens.Edit
isRestoring = false; isRestoring = false;
OnStateChange?.Invoke();
updateBindables(); updateBindables();
} }

View File

@ -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;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit namespace osu.Game.Screens.Edit
@ -10,6 +11,11 @@ namespace osu.Game.Screens.Edit
/// </summary> /// </summary>
public interface IEditorChangeHandler public interface IEditorChangeHandler
{ {
/// <summary>
/// Fired whenever a state change occurs.
/// </summary>
event Action OnStateChange;
/// <summary> /// <summary>
/// Begins a bulk state change event. <see cref="EndChange"/> should be invoked soon after. /// Begins a bulk state change event. <see cref="EndChange"/> should be invoked soon after.
/// </summary> /// </summary>

View File

@ -41,6 +41,7 @@ namespace osu.Game.Screens.Edit.Timing
private IReadOnlyList<Drawable> createSections() => new Drawable[] private IReadOnlyList<Drawable> createSections() => new Drawable[]
{ {
new GroupSection(),
new TimingSection(), new TimingSection(),
new DifficultySection(), new DifficultySection(),
new SampleSection(), new SampleSection(),

View File

@ -0,0 +1,119 @@
// 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.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osuTK;
namespace osu.Game.Screens.Edit.Timing
{
internal class GroupSection : CompositeDrawable
{
private LabelledTextBox textBox;
private TriangleButton button;
[Resolved]
protected Bindable<ControlPointGroup> SelectedGroup { get; private set; }
[Resolved]
protected IBindable<WorkingBeatmap> Beatmap { get; private set; }
[Resolved]
private EditorClock clock { get; set; }
[Resolved(canBeNull: true)]
private IEditorChangeHandler changeHandler { get; set; }
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Padding = new MarginPadding(10);
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(10),
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
textBox = new LabelledTextBox
{
Label = "Time"
},
button = new TriangleButton
{
Text = "Use current time",
RelativeSizeAxes = Axes.X,
Action = () => changeSelectedGroupTime(clock.CurrentTime)
}
}
},
};
textBox.OnCommit += (sender, isNew) =>
{
if (!isNew)
return;
if (double.TryParse(sender.Text, out var newTime))
{
changeSelectedGroupTime(newTime);
}
else
{
SelectedGroup.TriggerChange();
}
};
SelectedGroup.BindValueChanged(group =>
{
if (group.NewValue == null)
{
textBox.Text = string.Empty;
textBox.Current.Disabled = true;
button.Enabled.Value = false;
return;
}
textBox.Current.Disabled = false;
button.Enabled.Value = true;
textBox.Text = $"{group.NewValue.Time:n0}";
}, true);
}
private void changeSelectedGroupTime(in double time)
{
if (SelectedGroup.Value == null || time == SelectedGroup.Value.Time)
return;
changeHandler?.BeginChange();
var currentGroupItems = SelectedGroup.Value.ControlPoints.ToArray();
Beatmap.Value.Beatmap.ControlPointInfo.RemoveGroup(SelectedGroup.Value);
foreach (var cp in currentGroupItems)
Beatmap.Value.Beatmap.ControlPointInfo.Add(time, cp);
SelectedGroup.Value = Beatmap.Value.Beatmap.ControlPointInfo.GroupAt(time);
changeHandler?.EndChange();
}
}
}

View File

@ -18,7 +18,7 @@ namespace osu.Game.Skinning
ComboPrefix, ComboPrefix,
ComboOverlap, ComboOverlap,
AnimationFramerate, AnimationFramerate,
LayeredHitSounds, LayeredHitSounds
} }
} }
} }

View File

@ -85,6 +85,6 @@
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.7.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="4.7.0" />
<PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2020.213.0" ExcludeAssets="all" /> <PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2020.923.0" ExcludeAssets="all" />
</ItemGroup> </ItemGroup>
</Project> </Project>