1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-15 20:53:00 +08:00

Merge branch 'master' into more-timeline-toggles

This commit is contained in:
Bartłomiej Dach 2020-10-04 12:53:10 +02:00 committed by GitHub
commit 7957773d58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 609 additions and 165 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.930.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2020.1001.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

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,25 +28,33 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestFixture] [TestFixture]
public class TestSceneNotes : OsuTestScene public class TestSceneNotes : OsuTestScene
{ {
[BackgroundDependencyLoader] [Test]
private void load() public void TestVariousNotes()
{ {
Child = new FillFlowContainer DrawableNote note1 = null;
DrawableNote note2 = null;
DrawableHoldNote holdNote1 = null;
DrawableHoldNote holdNote2 = null;
AddStep("create notes", () =>
{ {
Clock = new FramedClock(new ManualClock()), Child = new FillFlowContainer
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(20),
Children = new[]
{ {
createNoteDisplay(ScrollingDirection.Down, 1, out var note1), Clock = new FramedClock(new ManualClock()),
createNoteDisplay(ScrollingDirection.Up, 2, out var note2), Anchor = Anchor.Centre,
createHoldNoteDisplay(ScrollingDirection.Down, 1, out var holdNote1), Origin = Anchor.Centre,
createHoldNoteDisplay(ScrollingDirection.Up, 2, out var holdNote2), AutoSizeAxes = Axes.Both,
} Direction = FillDirection.Horizontal,
}; Spacing = new Vector2(20),
Children = new[]
{
createNoteDisplay(ScrollingDirection.Down, 1, out note1),
createNoteDisplay(ScrollingDirection.Up, 2, out note2),
createHoldNoteDisplay(ScrollingDirection.Down, 1, out holdNote1),
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

@ -110,6 +110,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
} }
} }
public override void StopAllSamples()
{
base.StopAllSamples();
slidingSample?.Stop();
}
private void updateSlidingSample(ValueChangedEvent<bool> tracking) private void updateSlidingSample(ValueChangedEvent<bool> tracking)
{ {
if (tracking.NewValue) if (tracking.NewValue)

View File

@ -124,6 +124,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
} }
} }
public override void StopAllSamples()
{
base.StopAllSamples();
spinningSample?.Stop();
}
protected override void AddNestedHitObject(DrawableHitObject hitObject) protected override void AddNestedHitObject(DrawableHitObject hitObject)
{ {
base.AddNestedHitObject(hitObject); base.AddNestedHitObject(hitObject);

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,41 +127,57 @@ 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))
{ {
// constant ambient rotation to give the spinner "spinning" character. this.ScaleTo(initial_scale);
this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration); this.RotateTo(0);
centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint);
mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint);
using (BeginDelayedSequence(spinner.TimePreempt / 2, true)) using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
{ {
centre.ScaleTo(0.5f, spinner.TimePreempt / 2, Easing.OutQuint); // constant ambient rotation to give the spinner "spinning" character.
mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint); 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);
mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint);
using (BeginDelayedSequence(spinner.TimePreempt / 2, true))
{
centre.ScaleTo(0.5f, 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.
updateComplete(state == ArmedState.Hit, 0); using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
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,20 +70,30 @@ 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);
fixedMiddle.FadeColour(Color4.White); using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true))
using (BeginAbsoluteSequence(spinner.StartTime, true)) {
fixedMiddle.FadeColour(Color4.Red, spinner.Duration); fixedMiddle.FadeColour(Color4.White);
using (BeginDelayedSequence(spinner.TimePreempt, true))
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

@ -0,0 +1,47 @@
// 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.Audio;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneEditorSamplePlayback : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
[Test]
public void TestSlidingSampleStopsOnSeek()
{
DrawableSlider slider = null;
DrawableSample[] samples = null;
AddStep("get first slider", () =>
{
slider = Editor.ChildrenOfType<DrawableSlider>().OrderBy(s => s.HitObject.StartTime).First();
samples = slider.ChildrenOfType<DrawableSample>().ToArray();
});
AddStep("start playback", () => EditorClock.Start());
AddUntilStep("wait for slider sliding then seek", () =>
{
if (!slider.Tracking.Value)
return false;
if (!samples.Any(s => s.Playing))
return false;
EditorClock.Seek(20000);
return true;
});
AddAssert("slider samples are not playing", () => samples.Length == 5 && samples.All(s => s.Played && !s.Playing));
}
}
}

View File

@ -28,7 +28,7 @@ namespace osu.Game.Tournament.Components
{ {
base.Bindable = new Bindable<string>(); base.Bindable = new Bindable<string>();
((OsuTextBox)Control).OnCommit = (sender, newText) => ((OsuTextBox)Control).OnCommit += (sender, newText) =>
{ {
try try
{ {

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

@ -38,6 +38,7 @@ namespace osu.Game.Online.API.Requests.Responses
Rank = Rank, Rank = Rank,
Ruleset = ruleset, Ruleset = ruleset,
Mods = mods, Mods = mods,
IsLegacyScore = true
}; };
if (Statistics != null) if (Statistics != null)

View File

@ -59,12 +59,13 @@ namespace osu.Game.Online.Chat
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = textbox_height, Height = textbox_height,
PlaceholderText = "type your message", PlaceholderText = "type your message",
OnCommit = postMessage,
ReleaseFocusOnCommit = false, ReleaseFocusOnCommit = false,
HoldFocus = true, HoldFocus = true,
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
}); });
textbox.OnCommit += postMessage;
} }
Channel.BindValueChanged(channelChanged); Channel.BindValueChanged(channelChanged);

View File

@ -146,7 +146,6 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Height = 1, Height = 1,
PlaceholderText = "type your message", PlaceholderText = "type your message",
OnCommit = postMessage,
ReleaseFocusOnCommit = false, ReleaseFocusOnCommit = false,
HoldFocus = true, HoldFocus = true,
} }
@ -186,6 +185,8 @@ namespace osu.Game.Overlays
}, },
}; };
textbox.OnCommit += postMessage;
ChannelTabControl.Current.ValueChanged += current => channelManager.CurrentChannel.Value = current.NewValue; ChannelTabControl.Current.ValueChanged += current => channelManager.CurrentChannel.Value = current.NewValue;
ChannelTabControl.ChannelSelectorActive.ValueChanged += active => ChannelSelectionOverlay.State.Value = active.NewValue ? Visibility.Visible : Visibility.Hidden; ChannelTabControl.ChannelSelectorActive.ValueChanged += active => ChannelSelectionOverlay.State.Value = active.NewValue ? Visibility.Visible : Visibility.Hidden;
ChannelSelectionOverlay.State.ValueChanged += state => ChannelSelectionOverlay.State.ValueChanged += state =>

View File

@ -75,7 +75,7 @@ namespace osu.Game.Overlays.Music
}, },
}; };
filter.Search.OnCommit = (sender, newText) => filter.Search.OnCommit += (sender, newText) =>
{ {
BeatmapInfo toSelect = list.FirstVisibleSet?.Beatmaps?.FirstOrDefault(); BeatmapInfo toSelect = list.FirstVisibleSet?.Beatmaps?.FirstOrDefault();

View File

@ -236,7 +236,6 @@ namespace osu.Game.Overlays.Settings.Sections.General
PlaceholderText = "password", PlaceholderText = "password",
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
TabbableContentContainer = this, TabbableContentContainer = this,
OnCommit = (sender, newText) => performLogin()
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
@ -276,6 +275,8 @@ namespace osu.Game.Overlays.Settings.Sections.General
} }
} }
}; };
password.OnCommit += (sender, newText) => performLogin();
} }
public override bool AcceptsFocus => true; public override bool AcceptsFocus => true;

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

@ -124,19 +124,19 @@ namespace osu.Game.Rulesets.Judgements
return -DEFAULT_MAX_HEALTH_INCREASE; return -DEFAULT_MAX_HEALTH_INCREASE;
case HitResult.Meh: case HitResult.Meh:
return -DEFAULT_MAX_HEALTH_INCREASE * 0.05; return -DEFAULT_MAX_HEALTH_INCREASE * 0.5;
case HitResult.Ok: case HitResult.Ok:
return -DEFAULT_MAX_HEALTH_INCREASE * 0.01; return -DEFAULT_MAX_HEALTH_INCREASE * 0.3;
case HitResult.Good: case HitResult.Good:
return DEFAULT_MAX_HEALTH_INCREASE * 0.5; return DEFAULT_MAX_HEALTH_INCREASE * 0.1;
case HitResult.Great: case HitResult.Great:
return DEFAULT_MAX_HEALTH_INCREASE; return DEFAULT_MAX_HEALTH_INCREASE * 0.8;
case HitResult.Perfect: case HitResult.Perfect:
return DEFAULT_MAX_HEALTH_INCREASE * 1.05; return DEFAULT_MAX_HEALTH_INCREASE;
case HitResult.SmallBonus: case HitResult.SmallBonus:
return DEFAULT_MAX_HEALTH_INCREASE * 0.1; return DEFAULT_MAX_HEALTH_INCREASE * 0.1;

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;
@ -384,6 +384,11 @@ namespace osu.Game.Rulesets.Objects.Drawables
} }
} }
/// <summary>
/// Stops playback of all samples. Automatically called when <see cref="DrawableHitObject{TObject}"/>'s lifetime has been exceeded.
/// </summary>
public virtual void StopAllSamples() => Samples?.Stop();
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
@ -452,6 +457,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
foreach (var nested in NestedHitObjects) foreach (var nested in NestedHitObjects)
nested.OnKilled(); nested.OnKilled();
StopAllSamples();
UpdateResult(false); UpdateResult(false);
} }
@ -462,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

@ -185,6 +185,34 @@ namespace osu.Game.Scoring
[JsonProperty("position")] [JsonProperty("position")]
public int? Position { get; set; } public int? Position { get; set; }
private bool isLegacyScore;
/// <summary>
/// Whether this <see cref="ScoreInfo"/> represents a legacy (osu!stable) score.
/// </summary>
[JsonIgnore]
[NotMapped]
public bool IsLegacyScore
{
get
{
if (isLegacyScore)
return true;
// The above check will catch legacy online scores that have an appropriate UserString + UserId.
// For non-online scores such as those imported in, a heuristic is used based on the following table:
//
// Mode | UserString | UserId
// --------------- | ---------- | ---------
// stable | <username> | 1
// lazer | <username> | <userid>
// lazer (offline) | Guest | 1
return ID > 0 && UserID == 1 && UserString != "Guest";
}
set => isLegacyScore = value;
}
public IEnumerable<(HitResult result, int count, int? maxCount)> GetStatisticsForDisplay() public IEnumerable<(HitResult result, int count, int? maxCount)> GetStatisticsForDisplay()
{ {
foreach (var key in OrderAttributeUtils.GetValuesInOrder<HitResult>()) foreach (var key in OrderAttributeUtils.GetValuesInOrder<HitResult>())

View File

@ -10,6 +10,7 @@ using System.Threading;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -149,23 +150,38 @@ namespace osu.Game.Scoring
return; return;
} }
int? beatmapMaxCombo = score.Beatmap.MaxCombo; int beatmapMaxCombo;
if (beatmapMaxCombo == null) if (score.IsLegacyScore)
{ {
if (score.Beatmap.ID == 0 || difficulties == null) // This score is guaranteed to be an osu!stable score.
// The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used.
if (score.Beatmap.MaxCombo == null)
{ {
// We don't have enough information (max combo) to compute the score, so let's use the provided score. if (score.Beatmap.ID == 0 || difficulties == null)
Value = score.TotalScore; {
// We don't have enough information (max combo) to compute the score, so use the provided score.
Value = score.TotalScore;
return;
}
// We can compute the max combo locally after the async beatmap difficulty computation.
difficultyBindable = difficulties().GetBindableDifficulty(score.Beatmap, score.Ruleset, score.Mods, (difficultyCancellationSource = new CancellationTokenSource()).Token);
difficultyBindable.BindValueChanged(d => updateScore(d.NewValue.MaxCombo), true);
return; return;
} }
// We can compute the max combo locally after the async beatmap difficulty computation. beatmapMaxCombo = score.Beatmap.MaxCombo.Value;
difficultyBindable = difficulties().GetBindableDifficulty(score.Beatmap, score.Ruleset, score.Mods, (difficultyCancellationSource = new CancellationTokenSource()).Token);
difficultyBindable.BindValueChanged(d => updateScore(d.NewValue.MaxCombo), true);
} }
else else
updateScore(beatmapMaxCombo.Value); {
// This score is guaranteed to be an osu!lazer score.
// The combo must be determined through the score's statistics, as both the beatmap's max combo and the difficulty calculator will provide osu!stable combo values.
beatmapMaxCombo = Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetOrDefault(r)).Sum();
}
updateScore(beatmapMaxCombo);
} }
private void updateScore(int beatmapMaxCombo) private void updateScore(int beatmapMaxCombo)

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

@ -2,27 +2,23 @@
// 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 osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Edit.Timing namespace osu.Game.Screens.Edit.Timing
{ {
internal class DifficultySection : Section<DifficultyControlPoint> internal class DifficultySection : Section<DifficultyControlPoint>
{ {
private SettingsSlider<double> multiplier; private SliderWithTextBoxInput<double> multiplierSlider;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Flow.AddRange(new[] Flow.AddRange(new[]
{ {
multiplier = new SettingsSlider<double> multiplierSlider = new SliderWithTextBoxInput<double>("Speed Multiplier")
{ {
LabelText = "Speed Multiplier", Current = new DifficultyControlPoint().SpeedMultiplierBindable
Bindable = new DifficultyControlPoint().SpeedMultiplierBindable,
RelativeSizeAxes = Axes.X,
} }
}); });
} }
@ -31,7 +27,7 @@ namespace osu.Game.Screens.Edit.Timing
{ {
if (point.NewValue != null) if (point.NewValue != null)
{ {
multiplier.Bindable = point.NewValue.SpeedMultiplierBindable; multiplierSlider.Current = point.NewValue.SpeedMultiplierBindable;
} }
} }

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

@ -2,18 +2,17 @@
// 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 osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Edit.Timing namespace osu.Game.Screens.Edit.Timing
{ {
internal class SampleSection : Section<SampleControlPoint> internal class SampleSection : Section<SampleControlPoint>
{ {
private LabelledTextBox bank; private LabelledTextBox bank;
private SettingsSlider<int> volume; private SliderWithTextBoxInput<int> volume;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
@ -24,10 +23,9 @@ namespace osu.Game.Screens.Edit.Timing
{ {
Label = "Bank Name", Label = "Bank Name",
}, },
volume = new SettingsSlider<int> volume = new SliderWithTextBoxInput<int>("Volume")
{ {
Bindable = new SampleControlPoint().SampleVolumeBindable, Current = new SampleControlPoint().SampleVolumeBindable,
LabelText = "Volume",
} }
}); });
} }
@ -37,7 +35,7 @@ namespace osu.Game.Screens.Edit.Timing
if (point.NewValue != null) if (point.NewValue != null)
{ {
bank.Current = point.NewValue.SampleBankBindable; bank.Current = point.NewValue.SampleBankBindable;
volume.Bindable = point.NewValue.SampleVolumeBindable; volume.Current = point.NewValue.SampleVolumeBindable;
} }
} }

View File

@ -0,0 +1,77 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Edit.Timing
{
internal class SliderWithTextBoxInput<T> : CompositeDrawable, IHasCurrentValue<T>
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
{
private readonly SettingsSlider<T> slider;
public SliderWithTextBoxInput(string labelText)
{
LabelledTextBox textbox;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
textbox = new LabelledTextBox
{
Label = labelText,
},
slider = new SettingsSlider<T>
{
RelativeSizeAxes = Axes.X,
}
}
},
};
textbox.OnCommit += (t, isNew) =>
{
if (!isNew) return;
try
{
slider.Bindable.Parse(t.Text);
}
catch
{
// TriggerChange below will restore the previous text value on failure.
}
// This is run regardless of parsing success as the parsed number may not actually trigger a change
// due to bindable clamping. Even in such a case we want to update the textbox to a sane visual state.
Current.TriggerChange();
};
Current.BindValueChanged(val =>
{
textbox.Text = val.NewValue.ToString();
}, true);
}
public Bindable<T> Current
{
get => slider.Bindable;
set => slider.Bindable = value;
}
}
}

View File

@ -65,18 +65,19 @@ namespace osu.Game.Screens.Edit.Timing
{ {
if (!isNew) return; if (!isNew) return;
if (double.TryParse(Current.Value, out double doubleVal)) try
{ {
try if (double.TryParse(Current.Value, out double doubleVal) && doubleVal > 0)
{
beatLengthBindable.Value = beatLengthToBpm(doubleVal); beatLengthBindable.Value = beatLengthToBpm(doubleVal);
}
catch
{
// will restore the previous text value on failure.
beatLengthBindable.TriggerChange();
}
} }
catch
{
// TriggerChange below will restore the previous text value on failure.
}
// This is run regardless of parsing success as the parsed number may not actually trigger a change
// due to bindable clamping. Even in such a case we want to update the textbox to a sane visual state.
beatLengthBindable.TriggerChange();
}; };
beatLengthBindable.BindValueChanged(val => beatLengthBindable.BindValueChanged(val =>

View File

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

View File

@ -41,7 +41,14 @@ namespace osu.Game.Skinning
// it's not easy to know if a sample has finished playing (to end). // it's not easy to know if a sample has finished playing (to end).
// to keep things simple only resume playing looping samples. // to keep things simple only resume playing looping samples.
else if (Looping) else if (Looping)
base.Play(); {
// schedule so we don't start playing a sample which is no longer alive.
Schedule(() =>
{
if (RequestedPlaying)
base.Play();
});
}
} }
}); });
} }

View File

@ -24,7 +24,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.930.0" /> <PackageReference Include="ppy.osu.Framework" Version="2020.1001.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
<PackageReference Include="Sentry" Version="2.1.6" /> <PackageReference Include="Sentry" Version="2.1.6" />
<PackageReference Include="SharpCompress" Version="0.26.0" /> <PackageReference Include="SharpCompress" Version="0.26.0" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.930.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1001.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
</ItemGroup> </ItemGroup>
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. --> <!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
@ -80,11 +80,11 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.930.0" /> <PackageReference Include="ppy.osu.Framework" Version="2020.1001.0" />
<PackageReference Include="SharpCompress" Version="0.26.0" /> <PackageReference Include="SharpCompress" Version="0.26.0" />
<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>