1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-15 07:22:55 +08:00

Merge branch 'master' into perf-calculator-remove-working-beatmap

This commit is contained in:
Dean Herbert 2020-10-07 17:43:17 +09:00
commit f1a3b6d0ba
152 changed files with 2730 additions and 703 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.1001.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2020.1004.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

@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.UI
if (!result.Type.AffectsCombo() || !result.HasResult) if (!result.Type.AffectsCombo() || !result.HasResult)
return; return;
if (result.Type == HitResult.Miss) if (!result.IsHit)
{ {
updateCombo(0, null); updateCombo(0, null);
return; return;

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

@ -2,10 +2,40 @@
// 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.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Judgements namespace osu.Game.Rulesets.Mania.Judgements
{ {
public class ManiaJudgement : Judgement public class ManiaJudgement : Judgement
{ {
protected override double HealthIncreaseFor(HitResult result)
{
switch (result)
{
case HitResult.LargeTickHit:
return DEFAULT_MAX_HEALTH_INCREASE * 0.1;
case HitResult.LargeTickMiss:
return -DEFAULT_MAX_HEALTH_INCREASE * 0.1;
case HitResult.Meh:
return -DEFAULT_MAX_HEALTH_INCREASE * 0.5;
case HitResult.Ok:
return -DEFAULT_MAX_HEALTH_INCREASE * 0.3;
case HitResult.Good:
return DEFAULT_MAX_HEALTH_INCREASE * 0.1;
case HitResult.Great:
return DEFAULT_MAX_HEALTH_INCREASE * 0.8;
case HitResult.Perfect:
return DEFAULT_MAX_HEALTH_INCREASE;
default:
return base.HealthIncreaseFor(result);
}
}
} }
} }

View File

@ -29,12 +29,12 @@ namespace osu.Game.Rulesets.Mania
new SettingsEnumDropdown<ManiaScrollingDirection> new SettingsEnumDropdown<ManiaScrollingDirection>
{ {
LabelText = "Scrolling direction", LabelText = "Scrolling direction",
Bindable = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection) Current = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection)
}, },
new SettingsSlider<double, TimeSlider> new SettingsSlider<double, TimeSlider>
{ {
LabelText = "Scroll speed", LabelText = "Scroll speed",
Bindable = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime), Current = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime),
KeyboardStep = 5 KeyboardStep = 5
}, },
}; };

View File

@ -243,7 +243,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
endHold(); endHold();
} }
if (Tail.Result.Type == HitResult.Miss) if (Tail.Judged && !Tail.IsHit)
HasBroken = true; HasBroken = true;
} }

View File

@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (!userTriggered) if (!userTriggered)
{ {
if (!HitObject.HitWindows.CanBeHit(timeOffset)) if (!HitObject.HitWindows.CanBeHit(timeOffset))
ApplyResult(r => r.Type = HitResult.Miss); ApplyResult(r => r.Type = r.Judgement.MinResult);
return; return;
} }

View File

@ -9,7 +9,6 @@ using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects.Drawables namespace osu.Game.Rulesets.Mania.Objects.Drawables
{ {
@ -136,7 +135,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// <summary> /// <summary>
/// Causes this <see cref="DrawableManiaHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>. /// Causes this <see cref="DrawableManiaHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
/// </summary> /// </summary>
public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss); public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult);
} }
public abstract class DrawableManiaHitObject<TObject> : DrawableManiaHitObject public abstract class DrawableManiaHitObject<TObject> : DrawableManiaHitObject

View File

@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (!userTriggered) if (!userTriggered)
{ {
if (!HitObject.HitWindows.CanBeHit(timeOffset)) if (!HitObject.HitWindows.CanBeHit(timeOffset))
ApplyResult(r => r.Type = HitResult.Miss); ApplyResult(r => r.Type = r.Judgement.MinResult);
return; return;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

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

@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
HitObjects = { new HitCircle { Position = new Vector2(256, 192) } } HitObjects = { new HitCircle { Position = new Vector2(256, 192) } }
}, },
PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset < -hitWindows.WindowFor(HitResult.Meh) && Player.Results[0].Type == HitResult.Miss PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset < -hitWindows.WindowFor(HitResult.Meh) && !Player.Results[0].IsHit
}); });
} }
@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
Autoplay = false, Autoplay = false,
Beatmap = beatmap, Beatmap = beatmap,
PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset >= hitWindows.WindowFor(HitResult.Meh) && Player.Results[0].Type == HitResult.Miss PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset >= hitWindows.WindowFor(HitResult.Meh) && !Player.Results[0].IsHit
}); });
} }

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()));

View File

@ -314,11 +314,11 @@ namespace osu.Game.Rulesets.Osu.Tests
private bool assertMaxJudge() => judgementResults.Any() && judgementResults.All(t => t.Type == t.Judgement.MaxResult); private bool assertMaxJudge() => judgementResults.Any() && judgementResults.All(t => t.Type == t.Judgement.MaxResult);
private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.IgnoreHit && judgementResults.First().Type == HitResult.Miss; private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.SmallTickHit && !judgementResults.First().IsHit;
private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.IgnoreHit; private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.SmallTickHit;
private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.IgnoreMiss; private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.SmallTickMiss;
private ScoreAccessibleReplayPlayer currentPlayer; private ScoreAccessibleReplayPlayer currentPlayer;

View File

@ -1,7 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osuTK; using osuTK;
@ -10,40 +16,219 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
public class OsuSelectionHandler : SelectionHandler public class OsuSelectionHandler : SelectionHandler
{ {
public override bool HandleMovement(MoveSelectionEvent moveEvent) protected override void OnSelectionChanged()
{ {
Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue); base.OnSelectionChanged();
Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
// Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted bool canOperate = SelectedHitObjects.Count() > 1 || SelectedHitObjects.Any(s => s is Slider);
foreach (var h in SelectedHitObjects.OfType<OsuHitObject>())
{ SelectionBox.CanRotate = canOperate;
if (h is Spinner) SelectionBox.CanScaleX = canOperate;
{ SelectionBox.CanScaleY = canOperate;
// Spinners don't support position adjustments
continue;
} }
// Stacking is not considered protected override void OnOperationEnded()
minPosition = Vector2.ComponentMin(minPosition, Vector2.ComponentMin(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta)); {
maxPosition = Vector2.ComponentMax(maxPosition, Vector2.ComponentMax(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta)); base.OnOperationEnded();
referenceOrigin = null;
} }
if (minPosition.X < 0 || minPosition.Y < 0 || maxPosition.X > DrawWidth || maxPosition.Y > DrawHeight) public override bool HandleMovement(MoveSelectionEvent moveEvent) =>
return false; moveSelection(moveEvent.InstantDelta);
foreach (var h in SelectedHitObjects.OfType<OsuHitObject>()) /// <summary>
/// During a transform, the initial origin is stored so it can be used throughout the operation.
/// </summary>
private Vector2? referenceOrigin;
public override bool HandleFlip(Direction direction)
{ {
if (h is Spinner) var hitObjects = selectedMovableObjects;
var selectedObjectsQuad = getSurroundingQuad(hitObjects);
var centre = selectedObjectsQuad.Centre;
foreach (var h in hitObjects)
{ {
// Spinners don't support position adjustments var pos = h.Position;
continue;
switch (direction)
{
case Direction.Horizontal:
pos.X = centre.X - (pos.X - centre.X);
break;
case Direction.Vertical:
pos.Y = centre.Y - (pos.Y - centre.Y);
break;
} }
h.Position += moveEvent.InstantDelta; h.Position = pos;
if (h is Slider slider)
{
foreach (var point in slider.Path.ControlPoints)
{
point.Position.Value = new Vector2(
(direction == Direction.Horizontal ? -1 : 1) * point.Position.Value.X,
(direction == Direction.Vertical ? -1 : 1) * point.Position.Value.Y
);
}
}
} }
return true; return true;
} }
public override bool HandleScale(Vector2 scale, Anchor reference)
{
adjustScaleFromAnchor(ref scale, reference);
var hitObjects = selectedMovableObjects;
// for the time being, allow resizing of slider paths only if the slider is
// the only hit object selected. with a group selection, it's likely the user
// is not looking to change the duration of the slider but expand the whole pattern.
if (hitObjects.Length == 1 && hitObjects.First() is Slider slider)
{
Quad quad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value));
Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / quad.Width, 1 + scale.Y / quad.Height);
foreach (var point in slider.Path.ControlPoints)
point.Position.Value *= pathRelativeDeltaScale;
}
else
{
// move the selection before scaling if dragging from top or left anchors.
if ((reference & Anchor.x0) > 0 && !moveSelection(new Vector2(-scale.X, 0))) return false;
if ((reference & Anchor.y0) > 0 && !moveSelection(new Vector2(0, -scale.Y))) return false;
Quad quad = getSurroundingQuad(hitObjects);
foreach (var h in hitObjects)
{
h.Position = new Vector2(
quad.TopLeft.X + (h.X - quad.TopLeft.X) / quad.Width * (quad.Width + scale.X),
quad.TopLeft.Y + (h.Y - quad.TopLeft.Y) / quad.Height * (quad.Height + scale.Y)
);
}
}
return true;
}
private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference)
{
// cancel out scale in axes we don't care about (based on which drag handle was used).
if ((reference & Anchor.x1) > 0) scale.X = 0;
if ((reference & Anchor.y1) > 0) scale.Y = 0;
// reverse the scale direction if dragging from top or left.
if ((reference & Anchor.x0) > 0) scale.X = -scale.X;
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
}
public override bool HandleRotation(float delta)
{
var hitObjects = selectedMovableObjects;
Quad quad = getSurroundingQuad(hitObjects);
referenceOrigin ??= quad.Centre;
foreach (var h in hitObjects)
{
h.Position = rotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta);
if (h is IHasPath path)
{
foreach (var point in path.Path.ControlPoints)
point.Position.Value = rotatePointAroundOrigin(point.Position.Value, Vector2.Zero, delta);
}
}
// this isn't always the case but let's be lenient for now.
return true;
}
private bool moveSelection(Vector2 delta)
{
var hitObjects = selectedMovableObjects;
Quad quad = getSurroundingQuad(hitObjects);
if (quad.TopLeft.X + delta.X < 0 ||
quad.TopLeft.Y + delta.Y < 0 ||
quad.BottomRight.X + delta.X > DrawWidth ||
quad.BottomRight.Y + delta.Y > DrawHeight)
return false;
foreach (var h in hitObjects)
h.Position += delta;
return true;
}
/// <summary>
/// Returns a gamefield-space quad surrounding the provided hit objects.
/// </summary>
/// <param name="hitObjects">The hit objects to calculate a quad for.</param>
private Quad getSurroundingQuad(OsuHitObject[] hitObjects) =>
getSurroundingQuad(hitObjects.SelectMany(h => new[] { h.Position, h.EndPosition }));
/// <summary>
/// Returns a gamefield-space quad surrounding the provided points.
/// </summary>
/// <param name="points">The points to calculate a quad for.</param>
private Quad getSurroundingQuad(IEnumerable<Vector2> points)
{
if (!SelectedHitObjects.Any())
return new Quad();
Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
// Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
foreach (var p in points)
{
minPosition = Vector2.ComponentMin(minPosition, p);
maxPosition = Vector2.ComponentMax(maxPosition, p);
}
Vector2 size = maxPosition - minPosition;
return new Quad(minPosition.X, minPosition.Y, size.X, size.Y);
}
/// <summary>
/// All osu! hitobjects which can be moved/rotated/scaled.
/// </summary>
private OsuHitObject[] selectedMovableObjects => SelectedHitObjects
.OfType<OsuHitObject>()
.Where(h => !(h is Spinner))
.ToArray();
/// <summary>
/// Rotate a point around an arbitrary origin.
/// </summary>
/// <param name="point">The point.</param>
/// <param name="origin">The centre origin to rotate around.</param>
/// <param name="angle">The angle to rotate (in degrees).</param>
private static Vector2 rotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle)
{
angle = -angle;
point.X -= origin.X;
point.Y -= origin.Y;
Vector2 ret;
ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle));
ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle));
ret.X += origin.X;
ret.Y += origin.Y;
return ret;
}
} }
} }

View File

@ -39,6 +39,9 @@ namespace osu.Game.Rulesets.Osu.Mods
base.ApplyToDrawableHitObjects(drawables); base.ApplyToDrawableHitObjects(drawables);
} }
private double lastSliderHeadFadeOutStartTime;
private double lastSliderHeadFadeOutDuration;
protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state) protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state)
{ {
if (!(drawable is DrawableOsuHitObject d)) if (!(drawable is DrawableOsuHitObject d))
@ -54,7 +57,35 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (drawable) switch (drawable)
{ {
case DrawableSliderTail sliderTail:
// use stored values from head circle to achieve same fade sequence.
fadeOutDuration = lastSliderHeadFadeOutDuration;
fadeOutStartTime = lastSliderHeadFadeOutStartTime;
using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true))
sliderTail.FadeOut(fadeOutDuration);
break;
case DrawableSliderRepeat sliderRepeat:
// use stored values from head circle to achieve same fade sequence.
fadeOutDuration = lastSliderHeadFadeOutDuration;
fadeOutStartTime = lastSliderHeadFadeOutStartTime;
using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true))
// only apply to circle piece reverse arrow is not affected by hidden.
sliderRepeat.CirclePiece.FadeOut(fadeOutDuration);
break;
case DrawableHitCircle circle: case DrawableHitCircle circle:
if (circle is DrawableSliderHead)
{
lastSliderHeadFadeOutDuration = fadeOutDuration;
lastSliderHeadFadeOutStartTime = fadeOutStartTime;
}
// we don't want to see the approach circle // we don't want to see the approach circle
using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true)) using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
circle.ApproachCircle.Hide(); circle.ApproachCircle.Hide();

View File

@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (!userTriggered) if (!userTriggered)
{ {
if (!HitObject.HitWindows.CanBeHit(timeOffset)) if (!HitObject.HitWindows.CanBeHit(timeOffset))
ApplyResult(r => r.Type = HitResult.Miss); ApplyResult(r => r.Type = r.Judgement.MinResult);
return; return;
} }
@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
var circleResult = (OsuHitCircleJudgementResult)r; var circleResult = (OsuHitCircleJudgementResult)r;
// Todo: This should also consider misses, but they're a little more interesting to handle, since we don't necessarily know the position at the time of a miss. // Todo: This should also consider misses, but they're a little more interesting to handle, since we don't necessarily know the position at the time of a miss.
if (result != HitResult.Miss) if (result.IsHit())
{ {
var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position); var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position);
circleResult.CursorPositionAtHit = HitObject.StackedPosition + (localMousePosition - DrawSize / 2); circleResult.CursorPositionAtHit = HitObject.StackedPosition + (localMousePosition - DrawSize / 2);

View File

@ -8,7 +8,6 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
@ -68,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// <summary> /// <summary>
/// Causes this <see cref="DrawableOsuHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>. /// Causes this <see cref="DrawableOsuHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
/// </summary> /// </summary>
public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss); public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult);
protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement); protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement);
} }

View File

@ -8,7 +8,6 @@ using osu.Game.Configuration;
using osuTK; using osuTK;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK.Graphics; using osuTK.Graphics;
@ -67,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (JudgedObject != null) if (JudgedObject != null)
{ {
lightingColour = JudgedObject.AccentColour.GetBoundCopy(); lightingColour = JudgedObject.AccentColour.GetBoundCopy();
lightingColour.BindValueChanged(colour => Lighting.Colour = Result.Type == HitResult.Miss ? Color4.Transparent : colour.NewValue, true); lightingColour.BindValueChanged(colour => Lighting.Colour = Result.IsHit ? colour.NewValue : Color4.Transparent, true);
} }
else else
{ {

View File

@ -13,7 +13,6 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -52,6 +51,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling), Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling),
tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both },
tickContainer = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both }, tickContainer = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both },
repeatContainer = new Container<DrawableSliderRepeat> { RelativeSizeAxes = Axes.Both }, repeatContainer = new Container<DrawableSliderRepeat> { RelativeSizeAxes = Axes.Both },
Ball = new SliderBall(s, this) Ball = new SliderBall(s, this)
@ -63,7 +63,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Alpha = 0 Alpha = 0
}, },
headContainer = new Container<DrawableSliderHead> { RelativeSizeAxes = Axes.Both }, headContainer = new Container<DrawableSliderHead> { RelativeSizeAxes = Axes.Both },
tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both },
}; };
} }
@ -250,7 +249,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
// rather than doing it this way, we should probably attach the sample to the tail circle. // rather than doing it this way, we should probably attach the sample to the tail circle.
// this can only be done after we stop using LegacyLastTick. // this can only be done after we stop using LegacyLastTick.
if (TailCircle.Result.Type != HitResult.Miss) if (TailCircle.IsHit)
base.PlaySamples(); base.PlaySamples();
} }

View File

@ -6,9 +6,11 @@ using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using osu.Game.Skinning;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
@ -22,6 +24,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly Drawable scaleContainer; private readonly Drawable scaleContainer;
public readonly Drawable CirclePiece;
public override bool DisplayResult => false; public override bool DisplayResult => false;
public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider) public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider)
@ -34,7 +38,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre; Origin = Anchor.Centre;
InternalChild = scaleContainer = new ReverseArrowPiece(); InternalChild = scaleContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new[]
{
// no default for this; only visible in legacy skins.
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty()),
arrow = new ReverseArrowPiece(),
}
};
} }
private readonly IBindable<float> scaleBindable = new BindableFloat(); private readonly IBindable<float> scaleBindable = new BindableFloat();
@ -85,6 +100,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private bool hasRotation; private bool hasRotation;
private readonly ReverseArrowPiece arrow;
public void UpdateSnakingPosition(Vector2 start, Vector2 end) public void UpdateSnakingPosition(Vector2 start, Vector2 end)
{ {
// When the repeat is hit, the arrow should fade out on spot rather than following the slider // When the repeat is hit, the arrow should fade out on spot rather than following the slider
@ -114,18 +131,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
} }
float aimRotation = MathUtils.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X)); float aimRotation = MathUtils.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X));
while (Math.Abs(aimRotation - Rotation) > 180) while (Math.Abs(aimRotation - arrow.Rotation) > 180)
aimRotation += aimRotation < Rotation ? 360 : -360; aimRotation += aimRotation < arrow.Rotation ? 360 : -360;
if (!hasRotation) if (!hasRotation)
{ {
Rotation = aimRotation; arrow.Rotation = aimRotation;
hasRotation = true; hasRotation = true;
} }
else else
{ {
// If we're already snaking, interpolate to smooth out sharp curves (linear sliders, mainly). // If we're already snaking, interpolate to smooth out sharp curves (linear sliders, mainly).
Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), Rotation, aimRotation, 0, 50, Easing.OutQuint); arrow.Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), arrow.Rotation, aimRotation, 0, 50, Easing.OutQuint);
} }
} }
} }

View File

@ -1,15 +1,20 @@
// 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.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, ITrackSnaking
{ {
private readonly Slider slider; private readonly SliderTailCircle tailCircle;
/// <summary> /// <summary>
/// The judgement text is provided by the <see cref="DrawableSlider"/>. /// The judgement text is provided by the <see cref="DrawableSlider"/>.
@ -18,28 +23,73 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public bool Tracking { get; set; } public bool Tracking { get; set; }
private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>(); private readonly IBindable<float> scaleBindable = new BindableFloat();
private readonly IBindable<int> pathVersion = new Bindable<int>();
public DrawableSliderTail(Slider slider, SliderTailCircle hitCircle) private readonly SkinnableDrawable circlePiece;
: base(hitCircle)
private readonly Container scaleContainer;
public DrawableSliderTail(Slider slider, SliderTailCircle tailCircle)
: base(tailCircle)
{ {
this.slider = slider; this.tailCircle = tailCircle;
Origin = Anchor.Centre; Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both; Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
FillMode = FillMode.Fit;
AlwaysPresent = true; InternalChildren = new Drawable[]
{
scaleContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Children = new Drawable[]
{
// no default for this; only visible in legacy skins.
circlePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty())
}
},
};
}
positionBindable.BindTo(hitCircle.PositionBindable); [BackgroundDependencyLoader]
pathVersion.BindTo(slider.Path.Version); private void load()
{
scaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true);
scaleBindable.BindTo(HitObject.ScaleBindable);
}
positionBindable.BindValueChanged(_ => updatePosition()); protected override void UpdateInitialTransforms()
pathVersion.BindValueChanged(_ => updatePosition(), true); {
base.UpdateInitialTransforms();
// TODO: This has no drawable content. Support for skins should be added. circlePiece.FadeInFromZero(HitObject.TimeFadeIn);
}
protected override void UpdateStateTransforms(ArmedState state)
{
base.UpdateStateTransforms(state);
Debug.Assert(HitObject.HitWindows != null);
switch (state)
{
case ArmedState.Idle:
this.Delay(HitObject.TimePreempt).FadeOut(500);
Expire(true);
break;
case ArmedState.Miss:
this.FadeOut(100);
break;
case ArmedState.Hit:
// todo: temporary / arbitrary
this.Delay(800).FadeOut();
break;
}
} }
protected override void CheckForResult(bool userTriggered, double timeOffset) protected override void CheckForResult(bool userTriggered, double timeOffset)
@ -48,6 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult); ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
} }
private void updatePosition() => Position = HitObject.Position - slider.Position; public void UpdateSnakingPosition(Vector2 start, Vector2 end) =>
Position = tailCircle.RepeatIndex % 2 == 0 ? end : start;
} }
} }

View File

@ -212,7 +212,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return; return;
// Trigger a miss result for remaining ticks to avoid infinite gameplay. // Trigger a miss result for remaining ticks to avoid infinite gameplay.
foreach (var tick in ticks.Where(t => !t.IsHit)) foreach (var tick in ticks.Where(t => !t.Result.HasResult))
tick.TriggerResult(false); tick.TriggerResult(false);
ApplyResult(r => ApplyResult(r =>
@ -224,7 +224,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
else if (Progress > .75) else if (Progress > .75)
r.Type = HitResult.Meh; r.Type = HitResult.Meh;
else if (Time.Current >= Spinner.EndTime) else if (Time.Current >= Spinner.EndTime)
r.Type = HitResult.Miss; r.Type = r.Judgement.MinResult;
}); });
} }
@ -268,7 +268,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
while (wholeSpins != spins) while (wholeSpins != spins)
{ {
var tick = ticks.FirstOrDefault(t => !t.IsHit); var tick = ticks.FirstOrDefault(t => !t.Result.HasResult);
// tick may be null if we've hit the spin limit. // tick may be null if we've hit the spin limit.
if (tick != null) if (tick != null)

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Origin = Anchor.Centre; Origin = Anchor.Centre;
Masking = true; Masking = true;
BorderThickness = 10; BorderThickness = 9; // roughly matches slider borders and makes stacked circles distinctly visible from each other.
BorderColour = Color4.White; BorderColour = Color4.White;
Child = new Box Child = new Box

View File

@ -176,6 +176,7 @@ namespace osu.Game.Rulesets.Osu.Objects
// if this is to change, we should revisit this. // if this is to change, we should revisit this.
AddNested(TailCircle = new SliderTailCircle(this) AddNested(TailCircle = new SliderTailCircle(this)
{ {
RepeatIndex = e.SpanIndex,
StartTime = e.Time, StartTime = e.Time,
Position = EndPosition, Position = EndPosition,
StackHeight = StackHeight StackHeight = StackHeight
@ -183,10 +184,9 @@ namespace osu.Game.Rulesets.Osu.Objects
break; break;
case SliderEventType.Repeat: case SliderEventType.Repeat:
AddNested(new SliderRepeat AddNested(new SliderRepeat(this)
{ {
RepeatIndex = e.SpanIndex, RepeatIndex = e.SpanIndex,
SpanDuration = SpanDuration,
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration, StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
Position = Position + Path.PositionAt(e.PathProgress), Position = Position + Path.PositionAt(e.PathProgress),
StackHeight = StackHeight, StackHeight = StackHeight,

View File

@ -1,9 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Rulesets.Osu.Objects
{
public class SliderCircle : HitCircle
{
}
}

View File

@ -0,0 +1,50 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
{
/// <summary>
/// A hit circle which is at the end of a slider path (either repeat or final tail).
/// </summary>
public abstract class SliderEndCircle : HitCircle
{
private readonly Slider slider;
protected SliderEndCircle(Slider slider)
{
this.slider = slider;
}
public int RepeatIndex { get; set; }
public double SpanDuration => slider.SpanDuration;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
if (RepeatIndex > 0)
{
// Repeat points after the first span should appear behind the still-visible one.
TimeFadeIn = 0;
// The next end circle should appear exactly after the previous circle (on the same end) is hit.
TimePreempt = SpanDuration * 2;
}
else
{
// taken from osu-stable
const float first_end_circle_preempt_adjust = 2 / 3f;
// The first end circle should fade in with the slider.
TimePreempt = (StartTime - slider.StartTime) + slider.TimePreempt * first_end_circle_preempt_adjust;
}
}
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
}

View File

@ -1,35 +1,19 @@
// 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.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects namespace osu.Game.Rulesets.Osu.Objects
{ {
public class SliderRepeat : OsuHitObject public class SliderRepeat : SliderEndCircle
{ {
public int RepeatIndex { get; set; } public SliderRepeat(Slider slider)
public double SpanDuration { get; set; } : base(slider)
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{ {
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
// Out preempt should be one span early to give the user ample warning.
TimePreempt += SpanDuration;
// We want to show the first RepeatPoint as the TimePreempt dictates but on short (and possibly fast) sliders
// we may need to cut down this time on following RepeatPoints to only show up to two RepeatPoints at any given time.
if (RepeatIndex > 0)
TimePreempt = Math.Min(SpanDuration * 2, TimePreempt);
} }
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public override Judgement CreateJudgement() => new SliderRepeatJudgement(); public override Judgement CreateJudgement() => new SliderRepeatJudgement();
public class SliderRepeatJudgement : OsuJudgement public class SliderRepeatJudgement : OsuJudgement

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Judgements;
@ -13,23 +12,18 @@ namespace osu.Game.Rulesets.Osu.Objects
/// Note that this should not be used for timing correctness. /// Note that this should not be used for timing correctness.
/// See <see cref="SliderEventType.LegacyLastTick"/> usage in <see cref="Slider"/> for more information. /// See <see cref="SliderEventType.LegacyLastTick"/> usage in <see cref="Slider"/> for more information.
/// </summary> /// </summary>
public class SliderTailCircle : SliderCircle public class SliderTailCircle : SliderEndCircle
{ {
private readonly IBindable<int> pathVersion = new Bindable<int>();
public SliderTailCircle(Slider slider) public SliderTailCircle(Slider slider)
: base(slider)
{ {
pathVersion.BindTo(slider.Path.Version);
pathVersion.BindValueChanged(_ => Position = slider.EndPosition);
} }
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public override Judgement CreateJudgement() => new SliderTailJudgement(); public override Judgement CreateJudgement() => new SliderTailJudgement();
public class SliderTailJudgement : OsuJudgement public class SliderTailJudgement : OsuJudgement
{ {
public override HitResult MaxResult => HitResult.IgnoreHit; public override HitResult MaxResult => HitResult.SmallTickHit;
} }
} }
} }

View File

@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu
ReverseArrow, ReverseArrow,
HitCircleText, HitCircleText,
SliderHeadHitCircle, SliderHeadHitCircle,
SliderTailHitCircle,
SliderFollowCircle, SliderFollowCircle,
SliderBall, SliderBall,
SliderBody, SliderBody,

View File

@ -1,9 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -15,6 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
private bool disjointTrail; private bool disjointTrail;
private double lastTrailTime; private double lastTrailTime;
private IBindable<float> cursorSize;
public LegacyCursorTrail() public LegacyCursorTrail()
{ {
@ -22,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISkinSource skin) private void load(ISkinSource skin, OsuConfigManager config)
{ {
Texture = skin.GetTexture("cursortrail"); Texture = skin.GetTexture("cursortrail");
disjointTrail = skin.GetTexture("cursormiddle") == null; disjointTrail = skin.GetTexture("cursormiddle") == null;
@ -32,12 +36,16 @@ namespace osu.Game.Rulesets.Osu.Skinning
// stable "magic ratio". see OsuPlayfieldAdjustmentContainer for full explanation. // stable "magic ratio". see OsuPlayfieldAdjustmentContainer for full explanation.
Texture.ScaleAdjust *= 1.6f; Texture.ScaleAdjust *= 1.6f;
} }
cursorSize = config.GetBindable<float>(OsuSetting.GameplayCursorSize).GetBoundCopy();
} }
protected override double FadeDuration => disjointTrail ? 150 : 500; protected override double FadeDuration => disjointTrail ? 150 : 500;
protected override bool InterpolateMovements => !disjointTrail; protected override bool InterpolateMovements => !disjointTrail;
protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1);
protected override bool OnMouseMove(MouseMoveEvent e) protected override bool OnMouseMove(MouseMoveEvent e)
{ {
if (!disjointTrail) if (!disjointTrail)

View File

@ -21,10 +21,12 @@ namespace osu.Game.Rulesets.Osu.Skinning
public class LegacyMainCirclePiece : CompositeDrawable public class LegacyMainCirclePiece : CompositeDrawable
{ {
private readonly string priorityLookup; private readonly string priorityLookup;
private readonly bool hasNumber;
public LegacyMainCirclePiece(string priorityLookup = null) public LegacyMainCirclePiece(string priorityLookup = null, bool hasNumber = true)
{ {
this.priorityLookup = priorityLookup; this.priorityLookup = priorityLookup;
this.hasNumber = hasNumber;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
} }
@ -47,6 +49,23 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject; OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject;
bool allowFallback = false;
// attempt lookup using priority specification
Texture baseTexture = getTextureWithFallback(string.Empty);
// if the base texture was not found without a fallback, switch on fallback mode and re-perform the lookup.
if (baseTexture == null)
{
allowFallback = true;
baseTexture = getTextureWithFallback(string.Empty);
}
// at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it.
// the flow above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist.
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png (potentially from the default/fall-through skin).
Texture overlayTexture = getTextureWithFallback("overlay");
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
circleSprites = new Container<Sprite> circleSprites = new Container<Sprite>
@ -58,19 +77,23 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
hitCircleSprite = new Sprite hitCircleSprite = new Sprite
{ {
Texture = getTextureWithFallback(string.Empty), Texture = baseTexture,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}, },
hitCircleOverlay = new Sprite hitCircleOverlay = new Sprite
{ {
Texture = getTextureWithFallback("overlay"), Texture = overlayTexture,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
} }
} }
}, },
hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText };
if (hasNumber)
{
AddInternal(hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
{ {
Font = OsuFont.Numeric.With(size: 40), Font = OsuFont.Numeric.With(size: 40),
UseFullGlyphHeight = false, UseFullGlyphHeight = false,
@ -78,8 +101,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}, });
}; }
bool overlayAboveNumber = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true; bool overlayAboveNumber = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
@ -95,8 +118,13 @@ namespace osu.Game.Rulesets.Osu.Skinning
Texture tex = null; Texture tex = null;
if (!string.IsNullOrEmpty(priorityLookup)) if (!string.IsNullOrEmpty(priorityLookup))
{
tex = skin.GetTexture($"{priorityLookup}{name}"); tex = skin.GetTexture($"{priorityLookup}{name}");
if (!allowFallback)
return tex;
}
return tex ?? skin.GetTexture($"hitcircle{name}"); return tex ?? skin.GetTexture($"hitcircle{name}");
} }
} }
@ -107,6 +135,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
state.BindValueChanged(updateState, true); state.BindValueChanged(updateState, true);
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
if (hasNumber)
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
} }
@ -120,6 +149,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
circleSprites.FadeOut(legacy_fade_duration, Easing.Out); circleSprites.FadeOut(legacy_fade_duration, Easing.Out);
circleSprites.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); circleSprites.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
if (hasNumber)
{
var legacyVersion = skin.GetConfig<LegacySetting, decimal>(LegacySetting.Version)?.Value; var legacyVersion = skin.GetConfig<LegacySetting, decimal>(LegacySetting.Version)?.Value;
if (legacyVersion >= 2.0m) if (legacyVersion >= 2.0m)
@ -131,6 +162,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
hitCircleText.FadeOut(legacy_fade_duration, Easing.Out); hitCircleText.FadeOut(legacy_fade_duration, Easing.Out);
hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
} }
}
break; break;
} }

View File

@ -66,6 +66,12 @@ namespace osu.Game.Rulesets.Osu.Skinning
return null; return null;
case OsuSkinComponents.SliderTailHitCircle:
if (hasHitCircle.Value)
return new LegacyMainCirclePiece("sliderendcircle", false);
return null;
case OsuSkinComponents.SliderHeadHitCircle: case OsuSkinComponents.SliderHeadHitCircle:
if (hasHitCircle.Value) if (hasHitCircle.Value)
return new LegacyMainCirclePiece("sliderstartcircle"); return new LegacyMainCirclePiece("sliderstartcircle");

View File

@ -119,6 +119,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
/// </summary> /// </summary>
protected virtual bool InterpolateMovements => true; protected virtual bool InterpolateMovements => true;
protected virtual float IntervalMultiplier => 1.0f;
private Vector2? lastPosition; private Vector2? lastPosition;
private readonly InputResampler resampler = new InputResampler(); private readonly InputResampler resampler = new InputResampler();
@ -147,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
float distance = diff.Length; float distance = diff.Length;
Vector2 direction = diff / distance; Vector2 direction = diff / distance;
float interval = partSize.X / 2.5f; float interval = partSize.X / 2.5f * IntervalMultiplier;
for (float d = interval; d < distance; d += interval) for (float d = interval; d < distance; d += interval)
{ {

View File

@ -27,17 +27,17 @@ namespace osu.Game.Rulesets.Osu.UI
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Snaking in sliders", LabelText = "Snaking in sliders",
Bindable = config.GetBindable<bool>(OsuRulesetSetting.SnakingInSliders) Current = config.GetBindable<bool>(OsuRulesetSetting.SnakingInSliders)
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Snaking out sliders", LabelText = "Snaking out sliders",
Bindable = config.GetBindable<bool>(OsuRulesetSetting.SnakingOutSliders) Current = config.GetBindable<bool>(OsuRulesetSetting.SnakingOutSliders)
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Cursor trail", LabelText = "Cursor trail",
Bindable = config.GetBindable<bool>(OsuRulesetSetting.ShowCursorTrail) Current = config.GetBindable<bool>(OsuRulesetSetting.ShowCursorTrail)
}, },
}; };
} }

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

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Judgements
{ {
switch (result) switch (result)
{ {
case HitResult.Great: case HitResult.SmallTickHit:
return 0.15; return 0.15;
default: default:

View File

@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (!(obj is DrawableDrumRollTick)) if (!(obj is DrawableDrumRollTick))
return; return;
if (result.Type > HitResult.Miss) if (result.IsHit)
rollingHits++; rollingHits++;
else else
rollingHits--; rollingHits--;
@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Ok); ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Ok);
} }
else else
ApplyResult(r => r.Type = HitResult.Miss); ApplyResult(r => r.Type = r.Judgement.MinResult);
} }
protected override void UpdateStateTransforms(ArmedState state) protected override void UpdateStateTransforms(ArmedState state)

View File

@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (!userTriggered) if (!userTriggered)
{ {
if (!HitObject.HitWindows.CanBeHit(timeOffset)) if (!HitObject.HitWindows.CanBeHit(timeOffset))
ApplyResult(r => r.Type = HitResult.Miss); ApplyResult(r => r.Type = r.Judgement.MinResult);
return; return;
} }
@ -152,7 +152,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
return; return;
if (!validActionPressed) if (!validActionPressed)
ApplyResult(r => r.Type = HitResult.Miss); ApplyResult(r => r.Type = r.Judgement.MinResult);
else else
ApplyResult(r => r.Type = result); ApplyResult(r => r.Type = result);
} }

View File

@ -211,9 +211,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
tick.TriggerResult(false); tick.TriggerResult(false);
} }
var hitResult = numHits > HitObject.RequiredHits / 2 ? HitResult.Ok : HitResult.Miss; ApplyResult(r => r.Type = numHits > HitObject.RequiredHits / 2 ? HitResult.Ok : r.Judgement.MinResult);
ApplyResult(r => r.Type = hitResult);
} }
} }

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring
private double hpMultiplier; private double hpMultiplier;
/// <summary> /// <summary>
/// HP multiplier for a <see cref="HitResult.Miss"/>. /// HP multiplier for a <see cref="HitResult"/> that does not satisfy <see cref="HitResultExtensions.IsHit"/>.
/// </summary> /// </summary>
private double hpMissMultiplier; private double hpMissMultiplier;
@ -45,6 +45,6 @@ namespace osu.Game.Rulesets.Taiko.Scoring
} }
protected override double GetHealthIncreaseFor(JudgementResult result) protected override double GetHealthIncreaseFor(JudgementResult result)
=> base.GetHealthIncreaseFor(result) * (result.Type == HitResult.Miss ? hpMissMultiplier : hpMultiplier); => base.GetHealthIncreaseFor(result) * (result.IsHit ? hpMultiplier : hpMissMultiplier);
} }
} }

View File

@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
if (r?.Type.AffectsCombo() == false) if (r?.Type.AffectsCombo() == false)
return; return;
passing = r == null || r.Type > HitResult.Miss; passing = r == null || r.IsHit;
foreach (var sprite in InternalChildren.OfType<ScrollerSprite>()) foreach (var sprite in InternalChildren.OfType<ScrollerSprite>())
sprite.Passing = passing; sprite.Passing = passing;

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.UI
Alpha = 0.15f; Alpha = 0.15f;
Masking = true; Masking = true;
if (result == HitResult.Miss) if (!result.IsHit())
return; return;
bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim; bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim;

View File

@ -163,7 +163,6 @@ namespace osu.Game.Rulesets.Taiko.UI
target = centreHit; target = centreHit;
back = centre; back = centre;
if (gameplayClock?.IsSeeking != true)
drumSample.Centre?.Play(); drumSample.Centre?.Play();
} }
else if (action == RimAction) else if (action == RimAction)
@ -171,7 +170,6 @@ namespace osu.Game.Rulesets.Taiko.UI
target = rimHit; target = rimHit;
back = rim; back = rim;
if (gameplayClock?.IsSeeking != true)
drumSample.Rim?.Play(); drumSample.Rim?.Play();
} }

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

@ -0,0 +1,39 @@
// 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.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Timing;
using osu.Game.Screens.Play;
namespace osu.Game.Tests.NonVisual
{
[TestFixture]
public class GameplayClockTest
{
[TestCase(0)]
[TestCase(1)]
public void TestTrueGameplayRateWithZeroAdjustment(double underlyingClockRate)
{
var framedClock = new FramedClock(new ManualClock { Rate = underlyingClockRate });
var gameplayClock = new TestGameplayClock(framedClock);
gameplayClock.MutableNonGameplayAdjustments.Add(new BindableDouble());
Assert.That(gameplayClock.TrueGameplayRate, Is.EqualTo(0));
}
private class TestGameplayClock : GameplayClock
{
public List<Bindable<double>> MutableNonGameplayAdjustments { get; } = new List<Bindable<double>>();
public override IEnumerable<Bindable<double>> NonGameplayAdjustments => MutableNonGameplayAdjustments;
public TestGameplayClock(IFrameBasedClock underlyingClock)
: base(underlyingClock)
{
}
}
}
}

View File

@ -0,0 +1,69 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneComposeSelectBox : OsuTestScene
{
private Container selectionArea;
public TestSceneComposeSelectBox()
{
SelectionBox selectionBox = null;
AddStep("create box", () =>
Child = selectionArea = new Container
{
Size = new Vector2(400),
Position = -new Vector2(150),
Anchor = Anchor.Centre,
Children = new Drawable[]
{
selectionBox = new SelectionBox
{
CanRotate = true,
CanScaleX = true,
CanScaleY = true,
OnRotation = handleRotation,
OnScale = handleScale
}
}
});
AddToggleStep("toggle rotation", state => selectionBox.CanRotate = state);
AddToggleStep("toggle x", state => selectionBox.CanScaleX = state);
AddToggleStep("toggle y", state => selectionBox.CanScaleY = state);
}
private void handleScale(Vector2 amount, Anchor reference)
{
if ((reference & Anchor.y1) == 0)
{
int directionY = (reference & Anchor.y0) > 0 ? -1 : 1;
if (directionY < 0)
selectionArea.Y += amount.Y;
selectionArea.Height += directionY * amount.Y;
}
if ((reference & Anchor.x1) == 0)
{
int directionX = (reference & Anchor.x0) > 0 ? -1 : 1;
if (directionX < 0)
selectionArea.X += amount.X;
selectionArea.Width += directionX * amount.X;
}
}
private void handleRotation(float angle)
{
// kinda silly and wrong, but just showing that the drag handles work.
selectionArea.Rotation += angle;
}
}
}

View File

@ -5,6 +5,8 @@ using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -22,6 +24,9 @@ namespace osu.Game.Tests.Visual.Editing
protected override bool EditorComponentsReady => Editor.ChildrenOfType<SetupScreen>().SingleOrDefault()?.IsLoaded == true; protected override bool EditorComponentsReady => Editor.ChildrenOfType<SetupScreen>().SingleOrDefault()?.IsLoaded == true;
[Resolved]
private BeatmapManager beatmapManager { get; set; }
public override void SetUpSteps() public override void SetUpSteps()
{ {
AddStep("set dummy", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null)); AddStep("set dummy", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null));
@ -38,6 +43,15 @@ namespace osu.Game.Tests.Visual.Editing
{ {
AddStep("save beatmap", () => Editor.Save()); AddStep("save beatmap", () => Editor.Save());
AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.ID > 0); AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.ID > 0);
AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == false);
}
[Test]
public void TestExitWithoutSave()
{
AddStep("exit without save", () => Editor.Exit());
AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen());
AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == true);
} }
[Test] [Test]
@ -55,7 +69,7 @@ namespace osu.Game.Tests.Visual.Editing
using (var zip = ZipArchive.Open(temp)) using (var zip = ZipArchive.Open(temp))
zip.WriteToDirectory(extractedFolder); zip.WriteToDirectory(extractedFolder);
bool success = setup.ChangeAudioTrack(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3")); bool success = setup.ChildrenOfType<ResourcesSection>().First().ChangeAudioTrack(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3"));
File.Delete(temp); File.Delete(temp);
Directory.Delete(extractedFolder, true); Directory.Delete(extractedFolder, true);

View File

@ -19,12 +19,14 @@ namespace osu.Game.Tests.Visual.Editing
public void TestSlidingSampleStopsOnSeek() public void TestSlidingSampleStopsOnSeek()
{ {
DrawableSlider slider = null; DrawableSlider slider = null;
DrawableSample[] samples = null; DrawableSample[] loopingSamples = null;
DrawableSample[] onceOffSamples = null;
AddStep("get first slider", () => AddStep("get first slider", () =>
{ {
slider = Editor.ChildrenOfType<DrawableSlider>().OrderBy(s => s.HitObject.StartTime).First(); slider = Editor.ChildrenOfType<DrawableSlider>().OrderBy(s => s.HitObject.StartTime).First();
samples = slider.ChildrenOfType<DrawableSample>().ToArray(); onceOffSamples = slider.ChildrenOfType<DrawableSample>().Where(s => !s.Looping).ToArray();
loopingSamples = slider.ChildrenOfType<DrawableSample>().Where(s => s.Looping).ToArray();
}); });
AddStep("start playback", () => EditorClock.Start()); AddStep("start playback", () => EditorClock.Start());
@ -34,14 +36,15 @@ namespace osu.Game.Tests.Visual.Editing
if (!slider.Tracking.Value) if (!slider.Tracking.Value)
return false; return false;
if (!samples.Any(s => s.Playing)) if (!loopingSamples.Any(s => s.Playing))
return false; return false;
EditorClock.Seek(20000); EditorClock.Seek(20000);
return true; return true;
}); });
AddAssert("slider samples are not playing", () => samples.Length == 5 && samples.All(s => s.Played && !s.Playing)); AddAssert("non-looping samples are playing", () => onceOffSamples.Length == 4 && loopingSamples.All(s => s.Played || s.Playing));
AddAssert("looping samples are not playing", () => loopingSamples.Length == 1 && loopingSamples.All(s => s.Played && !s.Playing));
} }
} }
} }

View File

@ -0,0 +1,68 @@
// 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.Collections.Generic;
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;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneGameplaySamplePlayback : PlayerTestScene
{
[Test]
public void TestAllSamplesStopDuringSeek()
{
DrawableSlider slider = null;
DrawableSample[] samples = null;
ISamplePlaybackDisabler gameplayClock = null;
AddStep("get variables", () =>
{
gameplayClock = Player.ChildrenOfType<FrameStabilityContainer>().First().GameplayClock;
slider = Player.ChildrenOfType<DrawableSlider>().OrderBy(s => s.HitObject.StartTime).First();
samples = slider.ChildrenOfType<DrawableSample>().ToArray();
});
AddUntilStep("wait for slider sliding then seek", () =>
{
if (!slider.Tracking.Value)
return false;
if (!samples.Any(s => s.Playing))
return false;
Player.ChildrenOfType<GameplayClockContainer>().First().Seek(40000);
return true;
});
AddAssert("sample playback disabled", () => gameplayClock.SamplePlaybackDisabled.Value);
// because we are in frame stable context, it's quite likely that not all samples are "played" at this point.
// the important thing is that at least one started, and that sample has since stopped.
AddAssert("all looping samples stopped immediately", () => allStopped(allLoopingSounds));
AddUntilStep("all samples stopped eventually", () => allStopped(allSounds));
AddAssert("sample playback still disabled", () => gameplayClock.SamplePlaybackDisabled.Value);
AddUntilStep("seek finished, sample playback enabled", () => !gameplayClock.SamplePlaybackDisabled.Value);
AddUntilStep("any sample is playing", () => Player.ChildrenOfType<PausableSkinnableSound>().Any(s => s.IsPlaying));
}
private IEnumerable<PausableSkinnableSound> allSounds => Player.ChildrenOfType<PausableSkinnableSound>();
private IEnumerable<PausableSkinnableSound> allLoopingSounds => allSounds.Where(sound => sound.Looping);
private bool allStopped(IEnumerable<PausableSkinnableSound> sounds) => sounds.All(sound => !sound.IsPlaying);
protected override bool Autoplay => true;
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
}
}

View File

@ -0,0 +1,46 @@
// 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 NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneLabelledSliderBar : OsuTestScene
{
[TestCase(false)]
[TestCase(true)]
public void TestSliderBar(bool hasDescription) => createSliderBar(hasDescription);
private void createSliderBar(bool hasDescription = false)
{
AddStep("create component", () =>
{
LabelledSliderBar<double> component;
Child = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 500,
AutoSizeAxes = Axes.Y,
Child = component = new LabelledSliderBar<double>
{
Current = new BindableDouble(5)
{
MinValue = 0,
MaxValue = 10,
Precision = 1,
}
}
};
component.Label = "a sample component";
component.Description = hasDescription ? "this text describes the component" : string.Empty;
});
}
}
}

View File

@ -10,34 +10,34 @@ namespace osu.Game.Tournament.Components
{ {
public class DateTextBox : SettingsTextBox public class DateTextBox : SettingsTextBox
{ {
public new Bindable<DateTimeOffset> Bindable public new Bindable<DateTimeOffset> Current
{ {
get => bindable; get => current;
set set
{ {
bindable = value.GetBoundCopy(); current = value.GetBoundCopy();
bindable.BindValueChanged(dto => current.BindValueChanged(dto =>
base.Bindable.Value = dto.NewValue.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"), true); base.Current.Value = dto.NewValue.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"), true);
} }
} }
// hold a reference to the provided bindable so we don't have to in every settings section. // hold a reference to the provided bindable so we don't have to in every settings section.
private Bindable<DateTimeOffset> bindable = new Bindable<DateTimeOffset>(); private Bindable<DateTimeOffset> current = new Bindable<DateTimeOffset>();
public DateTextBox() public DateTextBox()
{ {
base.Bindable = new Bindable<string>(); base.Current = new Bindable<string>();
((OsuTextBox)Control).OnCommit += (sender, newText) => ((OsuTextBox)Control).OnCommit += (sender, newText) =>
{ {
try try
{ {
bindable.Value = DateTimeOffset.Parse(sender.Text); current.Value = DateTimeOffset.Parse(sender.Text);
} }
catch catch
{ {
// reset textbox content to its last valid state on a parse failure. // reset textbox content to its last valid state on a parse failure.
bindable.TriggerChange(); current.TriggerChange();
} }
}; };
} }

View File

@ -63,25 +63,25 @@ namespace osu.Game.Tournament.Screens.Editors
{ {
LabelText = "Name", LabelText = "Name",
Width = 0.33f, Width = 0.33f,
Bindable = Model.Name Current = Model.Name
}, },
new SettingsTextBox new SettingsTextBox
{ {
LabelText = "Description", LabelText = "Description",
Width = 0.33f, Width = 0.33f,
Bindable = Model.Description Current = Model.Description
}, },
new DateTextBox new DateTextBox
{ {
LabelText = "Start Time", LabelText = "Start Time",
Width = 0.33f, Width = 0.33f,
Bindable = Model.StartDate Current = Model.StartDate
}, },
new SettingsSlider<int> new SettingsSlider<int>
{ {
LabelText = "Best of", LabelText = "Best of",
Width = 0.33f, Width = 0.33f,
Bindable = Model.BestOf Current = Model.BestOf
}, },
new SettingsButton new SettingsButton
{ {
@ -186,14 +186,14 @@ namespace osu.Game.Tournament.Screens.Editors
LabelText = "Beatmap ID", LabelText = "Beatmap ID",
RelativeSizeAxes = Axes.None, RelativeSizeAxes = Axes.None,
Width = 200, Width = 200,
Bindable = beatmapId, Current = beatmapId,
}, },
new SettingsTextBox new SettingsTextBox
{ {
LabelText = "Mods", LabelText = "Mods",
RelativeSizeAxes = Axes.None, RelativeSizeAxes = Axes.None,
Width = 200, Width = 200,
Bindable = mods, Current = mods,
}, },
drawableContainer = new Container drawableContainer = new Container
{ {

View File

@ -74,13 +74,13 @@ namespace osu.Game.Tournament.Screens.Editors
{ {
LabelText = "Mod", LabelText = "Mod",
Width = 0.33f, Width = 0.33f,
Bindable = Model.Mod Current = Model.Mod
}, },
new SettingsSlider<int> new SettingsSlider<int>
{ {
LabelText = "Seed", LabelText = "Seed",
Width = 0.33f, Width = 0.33f,
Bindable = Model.Seed Current = Model.Seed
}, },
new SettingsButton new SettingsButton
{ {
@ -187,21 +187,21 @@ namespace osu.Game.Tournament.Screens.Editors
LabelText = "Beatmap ID", LabelText = "Beatmap ID",
RelativeSizeAxes = Axes.None, RelativeSizeAxes = Axes.None,
Width = 200, Width = 200,
Bindable = beatmapId, Current = beatmapId,
}, },
new SettingsSlider<int> new SettingsSlider<int>
{ {
LabelText = "Seed", LabelText = "Seed",
RelativeSizeAxes = Axes.None, RelativeSizeAxes = Axes.None,
Width = 200, Width = 200,
Bindable = beatmap.Seed Current = beatmap.Seed
}, },
new SettingsTextBox new SettingsTextBox
{ {
LabelText = "Score", LabelText = "Score",
RelativeSizeAxes = Axes.None, RelativeSizeAxes = Axes.None,
Width = 200, Width = 200,
Bindable = score, Current = score,
}, },
drawableContainer = new Container drawableContainer = new Container
{ {

View File

@ -102,31 +102,31 @@ namespace osu.Game.Tournament.Screens.Editors
{ {
LabelText = "Name", LabelText = "Name",
Width = 0.2f, Width = 0.2f,
Bindable = Model.FullName Current = Model.FullName
}, },
new SettingsTextBox new SettingsTextBox
{ {
LabelText = "Acronym", LabelText = "Acronym",
Width = 0.2f, Width = 0.2f,
Bindable = Model.Acronym Current = Model.Acronym
}, },
new SettingsTextBox new SettingsTextBox
{ {
LabelText = "Flag", LabelText = "Flag",
Width = 0.2f, Width = 0.2f,
Bindable = Model.FlagName Current = Model.FlagName
}, },
new SettingsTextBox new SettingsTextBox
{ {
LabelText = "Seed", LabelText = "Seed",
Width = 0.2f, Width = 0.2f,
Bindable = Model.Seed Current = Model.Seed
}, },
new SettingsSlider<int> new SettingsSlider<int>
{ {
LabelText = "Last Year Placement", LabelText = "Last Year Placement",
Width = 0.33f, Width = 0.33f,
Bindable = Model.LastYearPlacing Current = Model.LastYearPlacing
}, },
new SettingsButton new SettingsButton
{ {
@ -247,7 +247,7 @@ namespace osu.Game.Tournament.Screens.Editors
LabelText = "User ID", LabelText = "User ID",
RelativeSizeAxes = Axes.None, RelativeSizeAxes = Axes.None,
Width = 200, Width = 200,
Bindable = userId, Current = userId,
}, },
drawableContainer = new Container drawableContainer = new Container
{ {

View File

@ -113,13 +113,13 @@ namespace osu.Game.Tournament.Screens.Gameplay
new SettingsSlider<int> new SettingsSlider<int>
{ {
LabelText = "Chroma width", LabelText = "Chroma width",
Bindable = LadderInfo.ChromaKeyWidth, Current = LadderInfo.ChromaKeyWidth,
KeyboardStep = 1, KeyboardStep = 1,
}, },
new SettingsSlider<int> new SettingsSlider<int>
{ {
LabelText = "Players per team", LabelText = "Players per team",
Bindable = LadderInfo.PlayersPerTeam, Current = LadderInfo.PlayersPerTeam,
KeyboardStep = 1, KeyboardStep = 1,
} }
} }

View File

@ -51,15 +51,15 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
editorInfo.Selected.ValueChanged += selection => editorInfo.Selected.ValueChanged += selection =>
{ {
roundDropdown.Bindable = selection.NewValue?.Round; roundDropdown.Current = selection.NewValue?.Round;
losersCheckbox.Current = selection.NewValue?.Losers; losersCheckbox.Current = selection.NewValue?.Losers;
dateTimeBox.Bindable = selection.NewValue?.Date; dateTimeBox.Current = selection.NewValue?.Date;
team1Dropdown.Bindable = selection.NewValue?.Team1; team1Dropdown.Current = selection.NewValue?.Team1;
team2Dropdown.Bindable = selection.NewValue?.Team2; team2Dropdown.Current = selection.NewValue?.Team2;
}; };
roundDropdown.Bindable.ValueChanged += round => roundDropdown.Current.ValueChanged += round =>
{ {
if (editorInfo.Selected.Value?.Date.Value < round.NewValue?.StartDate.Value) if (editorInfo.Selected.Value?.Date.Value < round.NewValue?.StartDate.Value)
{ {
@ -88,7 +88,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
{ {
public SettingsRoundDropdown(BindableList<TournamentRound> rounds) public SettingsRoundDropdown(BindableList<TournamentRound> rounds)
{ {
Bindable = new Bindable<TournamentRound>(); Current = new Bindable<TournamentRound>();
foreach (var r in rounds.Prepend(new TournamentRound())) foreach (var r in rounds.Prepend(new TournamentRound()))
add(r); add(r);

View File

@ -61,7 +61,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
new SettingsTeamDropdown(LadderInfo.Teams) new SettingsTeamDropdown(LadderInfo.Teams)
{ {
LabelText = "Show specific team", LabelText = "Show specific team",
Bindable = currentTeam, Current = currentTeam,
} }
} }
} }

View File

@ -114,6 +114,25 @@ namespace osu.Game.Beatmaps
return computeDifficulty(key, beatmapInfo, rulesetInfo); return computeDifficulty(key, beatmapInfo, rulesetInfo);
} }
/// <summary>
/// Retrieves the <see cref="DifficultyRating"/> that describes a star rating.
/// </summary>
/// <remarks>
/// For more information, see: https://osu.ppy.sh/help/wiki/Difficulties
/// </remarks>
/// <param name="starRating">The star rating.</param>
/// <returns>The <see cref="DifficultyRating"/> that best describes <paramref name="starRating"/>.</returns>
public static DifficultyRating GetDifficultyRating(double starRating)
{
if (starRating < 2.0) return DifficultyRating.Easy;
if (starRating < 2.7) return DifficultyRating.Normal;
if (starRating < 4.0) return DifficultyRating.Hard;
if (starRating < 5.3) return DifficultyRating.Insane;
if (starRating < 6.5) return DifficultyRating.Expert;
return DifficultyRating.ExpertPlus;
}
private CancellationTokenSource trackedUpdateCancellationSource; private CancellationTokenSource trackedUpdateCancellationSource;
private readonly List<CancellationTokenSource> linkedCancellationSources = new List<CancellationTokenSource>(); private readonly List<CancellationTokenSource> linkedCancellationSources = new List<CancellationTokenSource>();
@ -307,5 +326,7 @@ namespace osu.Game.Beatmaps
// Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...) // Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...)
} }
public DifficultyRating DifficultyRating => BeatmapDifficultyManager.GetDifficultyRating(Stars);
} }
} }

View File

@ -135,21 +135,7 @@ namespace osu.Game.Beatmaps
public List<ScoreInfo> Scores { get; set; } public List<ScoreInfo> Scores { get; set; }
[JsonIgnore] [JsonIgnore]
public DifficultyRating DifficultyRating public DifficultyRating DifficultyRating => BeatmapDifficultyManager.GetDifficultyRating(StarDifficulty);
{
get
{
var rating = StarDifficulty;
if (rating < 2.0) return DifficultyRating.Easy;
if (rating < 2.7) return DifficultyRating.Normal;
if (rating < 4.0) return DifficultyRating.Hard;
if (rating < 5.3) return DifficultyRating.Insane;
if (rating < 6.5) return DifficultyRating.Expert;
return DifficultyRating.ExpertPlus;
}
}
public string[] SearchableTerms => new[] public string[] SearchableTerms => new[]
{ {

View File

@ -57,7 +57,7 @@ namespace osu.Game.Beatmaps
/// </summary> /// </summary>
public readonly WorkingBeatmap DefaultBeatmap; public readonly WorkingBeatmap DefaultBeatmap;
public override string[] HandledExtensions => new[] { ".osz" }; public override IEnumerable<string> HandledExtensions => new[] { ".osz" };
protected override string[] HashableFileTypes => new[] { ".osu" }; protected override string[] HashableFileTypes => new[] { ".osu" };

View File

@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using osu.Game.Graphics;
using osuTK.Graphics;
namespace osu.Game.Beatmaps.ControlPoints namespace osu.Game.Beatmaps.ControlPoints
{ {
@ -18,6 +20,8 @@ namespace osu.Game.Beatmaps.ControlPoints
public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time); public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time);
public virtual Color4 GetRepresentingColour(OsuColour colours) => colours.Yellow;
/// <summary> /// <summary>
/// Determines whether this <see cref="ControlPoint"/> results in a meaningful change when placed alongside another. /// Determines whether this <see cref="ControlPoint"/> results in a meaningful change when placed alongside another.
/// </summary> /// </summary>

View File

@ -2,6 +2,8 @@
// 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.Bindables; using osu.Framework.Bindables;
using osu.Game.Graphics;
using osuTK.Graphics;
namespace osu.Game.Beatmaps.ControlPoints namespace osu.Game.Beatmaps.ControlPoints
{ {
@ -23,6 +25,8 @@ namespace osu.Game.Beatmaps.ControlPoints
MaxValue = 10 MaxValue = 10
}; };
public override Color4 GetRepresentingColour(OsuColour colours) => colours.GreenDark;
/// <summary> /// <summary>
/// The speed multiplier at this control point. /// The speed multiplier at this control point.
/// </summary> /// </summary>

View File

@ -2,6 +2,8 @@
// 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.Bindables; using osu.Framework.Bindables;
using osu.Game.Graphics;
using osuTK.Graphics;
namespace osu.Game.Beatmaps.ControlPoints namespace osu.Game.Beatmaps.ControlPoints
{ {
@ -18,6 +20,8 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary> /// </summary>
public readonly BindableBool OmitFirstBarLineBindable = new BindableBool(); public readonly BindableBool OmitFirstBarLineBindable = new BindableBool();
public override Color4 GetRepresentingColour(OsuColour colours) => colours.Purple;
/// <summary> /// <summary>
/// Whether the first bar line of this control point is ignored. /// Whether the first bar line of this control point is ignored.
/// </summary> /// </summary>

View File

@ -3,6 +3,8 @@
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Graphics;
using osuTK.Graphics;
namespace osu.Game.Beatmaps.ControlPoints namespace osu.Game.Beatmaps.ControlPoints
{ {
@ -16,6 +18,8 @@ namespace osu.Game.Beatmaps.ControlPoints
SampleVolumeBindable = { Disabled = true } SampleVolumeBindable = { Disabled = true }
}; };
public override Color4 GetRepresentingColour(OsuColour colours) => colours.Pink;
/// <summary> /// <summary>
/// The default sample bank at this control point. /// The default sample bank at this control point.
/// </summary> /// </summary>

View File

@ -3,6 +3,8 @@
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
using osu.Game.Graphics;
using osuTK.Graphics;
namespace osu.Game.Beatmaps.ControlPoints namespace osu.Game.Beatmaps.ControlPoints
{ {
@ -18,6 +20,8 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary> /// </summary>
private const double default_beat_length = 60000.0 / 60.0; private const double default_beat_length = 60000.0 / 60.0;
public override Color4 GetRepresentingColour(OsuColour colours) => colours.YellowDark;
public static readonly TimingControlPoint DEFAULT = new TimingControlPoint public static readonly TimingControlPoint DEFAULT = new TimingControlPoint
{ {
BeatLengthBindable = BeatLengthBindable =

View File

@ -2,7 +2,11 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Threading;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -14,6 +18,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -21,9 +26,6 @@ namespace osu.Game.Beatmaps.Drawables
{ {
public class DifficultyIcon : CompositeDrawable, IHasCustomTooltip public class DifficultyIcon : CompositeDrawable, IHasCustomTooltip
{ {
private readonly BeatmapInfo beatmap;
private readonly RulesetInfo ruleset;
private readonly Container iconContainer; private readonly Container iconContainer;
/// <summary> /// <summary>
@ -35,23 +37,49 @@ namespace osu.Game.Beatmaps.Drawables
set => iconContainer.Size = value; set => iconContainer.Size = value;
} }
public DifficultyIcon(BeatmapInfo beatmap, RulesetInfo ruleset = null, bool shouldShowTooltip = true) [NotNull]
private readonly BeatmapInfo beatmap;
[CanBeNull]
private readonly RulesetInfo ruleset;
[CanBeNull]
private readonly IReadOnlyList<Mod> mods;
private readonly bool shouldShowTooltip;
private readonly IBindable<StarDifficulty> difficultyBindable = new Bindable<StarDifficulty>();
private Drawable background;
/// <summary>
/// Creates a new <see cref="DifficultyIcon"/> with a given <see cref="RulesetInfo"/> and <see cref="Mod"/> combination.
/// </summary>
/// <param name="beatmap">The beatmap to show the difficulty of.</param>
/// <param name="ruleset">The ruleset to show the difficulty with.</param>
/// <param name="mods">The mods to show the difficulty with.</param>
/// <param name="shouldShowTooltip">Whether to display a tooltip when hovered.</param>
public DifficultyIcon([NotNull] BeatmapInfo beatmap, [CanBeNull] RulesetInfo ruleset, [CanBeNull] IReadOnlyList<Mod> mods, bool shouldShowTooltip = true)
: this(beatmap, shouldShowTooltip)
{
this.ruleset = ruleset ?? beatmap.Ruleset;
this.mods = mods ?? Array.Empty<Mod>();
}
/// <summary>
/// Creates a new <see cref="DifficultyIcon"/> that follows the currently-selected ruleset and mods.
/// </summary>
/// <param name="beatmap">The beatmap to show the difficulty of.</param>
/// <param name="shouldShowTooltip">Whether to display a tooltip when hovered.</param>
public DifficultyIcon([NotNull] BeatmapInfo beatmap, bool shouldShowTooltip = true)
{ {
this.beatmap = beatmap ?? throw new ArgumentNullException(nameof(beatmap)); this.beatmap = beatmap ?? throw new ArgumentNullException(nameof(beatmap));
this.shouldShowTooltip = shouldShowTooltip;
this.ruleset = ruleset ?? beatmap.Ruleset;
if (shouldShowTooltip)
TooltipContent = beatmap;
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
InternalChild = iconContainer = new Container { Size = new Vector2(20f) }; InternalChild = iconContainer = new Container { Size = new Vector2(20f) };
} }
public ITooltip GetCustomTooltip() => new DifficultyIconTooltip();
public object TooltipContent { get; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
@ -70,10 +98,10 @@ namespace osu.Game.Beatmaps.Drawables
Type = EdgeEffectType.Shadow, Type = EdgeEffectType.Shadow,
Radius = 5, Radius = 5,
}, },
Child = new Box Child = background = new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = colours.ForDifficultyRating(beatmap.DifficultyRating), Colour = colours.ForDifficultyRating(beatmap.DifficultyRating) // Default value that will be re-populated once difficulty calculation completes
}, },
}, },
new ConstrainedIconContainer new ConstrainedIconContainer
@ -82,16 +110,73 @@ namespace osu.Game.Beatmaps.Drawables
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
// the null coalesce here is only present to make unit tests work (ruleset dlls aren't copied correctly for testing at the moment) // the null coalesce here is only present to make unit tests work (ruleset dlls aren't copied correctly for testing at the moment)
Icon = ruleset?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle } Icon = (ruleset ?? beatmap.Ruleset)?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle }
} },
new DelayedLoadUnloadWrapper(() => new DifficultyRetriever(beatmap, ruleset, mods) { StarDifficulty = { BindTarget = difficultyBindable } }, 0),
}; };
difficultyBindable.BindValueChanged(difficulty => background.Colour = colours.ForDifficultyRating(difficulty.NewValue.DifficultyRating));
}
public ITooltip GetCustomTooltip() => new DifficultyIconTooltip();
public object TooltipContent => shouldShowTooltip ? new DifficultyIconTooltipContent(beatmap, difficultyBindable) : null;
private class DifficultyRetriever : Component
{
public readonly Bindable<StarDifficulty> StarDifficulty = new Bindable<StarDifficulty>();
private readonly BeatmapInfo beatmap;
private readonly RulesetInfo ruleset;
private readonly IReadOnlyList<Mod> mods;
private CancellationTokenSource difficultyCancellation;
[Resolved]
private BeatmapDifficultyManager difficultyManager { get; set; }
public DifficultyRetriever(BeatmapInfo beatmap, RulesetInfo ruleset, IReadOnlyList<Mod> mods)
{
this.beatmap = beatmap;
this.ruleset = ruleset;
this.mods = mods;
}
private IBindable<StarDifficulty> localStarDifficulty;
[BackgroundDependencyLoader]
private void load()
{
difficultyCancellation = new CancellationTokenSource();
localStarDifficulty = ruleset != null
? difficultyManager.GetBindableDifficulty(beatmap, ruleset, mods, difficultyCancellation.Token)
: difficultyManager.GetBindableDifficulty(beatmap, difficultyCancellation.Token);
localStarDifficulty.BindValueChanged(difficulty => StarDifficulty.Value = difficulty.NewValue);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
difficultyCancellation?.Cancel();
}
}
private class DifficultyIconTooltipContent
{
public readonly BeatmapInfo Beatmap;
public readonly IBindable<StarDifficulty> Difficulty;
public DifficultyIconTooltipContent(BeatmapInfo beatmap, IBindable<StarDifficulty> difficulty)
{
Beatmap = beatmap;
Difficulty = difficulty;
}
} }
private class DifficultyIconTooltip : VisibilityContainer, ITooltip private class DifficultyIconTooltip : VisibilityContainer, ITooltip
{ {
private readonly OsuSpriteText difficultyName, starRating; private readonly OsuSpriteText difficultyName, starRating;
private readonly Box background; private readonly Box background;
private readonly FillFlowContainer difficultyFlow; private readonly FillFlowContainer difficultyFlow;
public DifficultyIconTooltip() public DifficultyIconTooltip()
@ -159,14 +244,22 @@ namespace osu.Game.Beatmaps.Drawables
background.Colour = colours.Gray3; background.Colour = colours.Gray3;
} }
private readonly IBindable<StarDifficulty> starDifficulty = new Bindable<StarDifficulty>();
public bool SetContent(object content) public bool SetContent(object content)
{ {
if (!(content is BeatmapInfo beatmap)) if (!(content is DifficultyIconTooltipContent iconContent))
return false; return false;
difficultyName.Text = beatmap.Version; difficultyName.Text = iconContent.Beatmap.Version;
starRating.Text = $"{beatmap.StarDifficulty:0.##}";
difficultyFlow.Colour = colours.ForDifficultyRating(beatmap.DifficultyRating, true); starDifficulty.UnbindAll();
starDifficulty.BindTo(iconContent.Difficulty);
starDifficulty.BindValueChanged(difficulty =>
{
starRating.Text = $"{difficulty.NewValue.Stars:0.##}";
difficultyFlow.Colour = colours.ForDifficultyRating(difficulty.NewValue.DifficultyRating, true);
}, true);
return true; return true;
} }

View File

@ -20,7 +20,7 @@ namespace osu.Game.Beatmaps.Drawables
public class GroupedDifficultyIcon : DifficultyIcon public class GroupedDifficultyIcon : DifficultyIcon
{ {
public GroupedDifficultyIcon(List<BeatmapInfo> beatmaps, RulesetInfo ruleset, Color4 counterColour) public GroupedDifficultyIcon(List<BeatmapInfo> beatmaps, RulesetInfo ruleset, Color4 counterColour)
: base(beatmaps.OrderBy(b => b.StarDifficulty).Last(), ruleset, false) : base(beatmaps.OrderBy(b => b.StarDifficulty).Last(), ruleset, null, false)
{ {
AddInternal(new OsuSpriteText AddInternal(new OsuSpriteText
{ {

View File

@ -57,7 +57,7 @@ namespace osu.Game.Configuration
yield return new SettingsSlider<float> yield return new SettingsSlider<float>
{ {
LabelText = attr.Label, LabelText = attr.Label,
Bindable = bNumber, Current = bNumber,
KeyboardStep = 0.1f, KeyboardStep = 0.1f,
}; };
@ -67,7 +67,7 @@ namespace osu.Game.Configuration
yield return new SettingsSlider<double> yield return new SettingsSlider<double>
{ {
LabelText = attr.Label, LabelText = attr.Label,
Bindable = bNumber, Current = bNumber,
KeyboardStep = 0.1f, KeyboardStep = 0.1f,
}; };
@ -77,7 +77,7 @@ namespace osu.Game.Configuration
yield return new SettingsSlider<int> yield return new SettingsSlider<int>
{ {
LabelText = attr.Label, LabelText = attr.Label,
Bindable = bNumber Current = bNumber
}; };
break; break;
@ -86,7 +86,7 @@ namespace osu.Game.Configuration
yield return new SettingsCheckbox yield return new SettingsCheckbox
{ {
LabelText = attr.Label, LabelText = attr.Label,
Bindable = bBool Current = bBool
}; };
break; break;
@ -95,7 +95,7 @@ namespace osu.Game.Configuration
yield return new SettingsTextBox yield return new SettingsTextBox
{ {
LabelText = attr.Label, LabelText = attr.Label,
Bindable = bString Current = bString
}; };
break; break;
@ -105,7 +105,7 @@ namespace osu.Game.Configuration
var dropdown = (Drawable)Activator.CreateInstance(dropdownType); var dropdown = (Drawable)Activator.CreateInstance(dropdownType);
dropdownType.GetProperty(nameof(SettingsDropdown<object>.LabelText))?.SetValue(dropdown, attr.Label); dropdownType.GetProperty(nameof(SettingsDropdown<object>.LabelText))?.SetValue(dropdown, attr.Label);
dropdownType.GetProperty(nameof(SettingsDropdown<object>.Bindable))?.SetValue(dropdown, bindable); dropdownType.GetProperty(nameof(SettingsDropdown<object>.Current))?.SetValue(dropdown, bindable);
yield return dropdown; yield return dropdown;

View File

@ -70,7 +70,7 @@ namespace osu.Game.Database
private readonly Bindable<WeakReference<TModel>> itemRemoved = new Bindable<WeakReference<TModel>>(); private readonly Bindable<WeakReference<TModel>> itemRemoved = new Bindable<WeakReference<TModel>>();
public virtual string[] HandledExtensions => new[] { ".zip" }; public virtual IEnumerable<string> HandledExtensions => new[] { ".zip" };
public virtual bool SupportsImportFromStable => RuntimeInfo.IsDesktop; public virtual bool SupportsImportFromStable => RuntimeInfo.IsDesktop;

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.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace osu.Game.Database namespace osu.Game.Database
@ -19,6 +20,6 @@ namespace osu.Game.Database
/// <summary> /// <summary>
/// An array of accepted file extensions (in the standard format of ".abc"). /// An array of accepted file extensions (in the standard format of ".abc").
/// </summary> /// </summary>
string[] HandledExtensions { get; } IEnumerable<string> HandledExtensions { get; }
} }
} }

View File

@ -0,0 +1,24 @@
// 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.Graphics;
using osu.Game.Overlays.Settings;
namespace osu.Game.Graphics.UserInterfaceV2
{
public class LabelledSliderBar<TNumber> : LabelledComponent<SettingsSlider<TNumber>, TNumber>
where TNumber : struct, IEquatable<TNumber>, IComparable<TNumber>, IConvertible
{
public LabelledSliderBar()
: base(true)
{
}
protected override SettingsSlider<TNumber> CreateComponent() => new SettingsSlider<TNumber>
{
TransferValueOnCommit = true,
RelativeSizeAxes = Axes.X,
};
}
}

View File

@ -232,9 +232,9 @@ namespace osu.Game
dependencies.Cache(new SessionStatics()); dependencies.Cache(new SessionStatics());
dependencies.Cache(new OsuColour()); dependencies.Cache(new OsuColour());
fileImporters.Add(BeatmapManager); RegisterImportHandler(BeatmapManager);
fileImporters.Add(ScoreManager); RegisterImportHandler(ScoreManager);
fileImporters.Add(SkinManager); RegisterImportHandler(SkinManager);
// tracks play so loud our samples can't keep up. // tracks play so loud our samples can't keep up.
// this adds a global reduction of track volume for the time being. // this adds a global reduction of track volume for the time being.
@ -343,6 +343,18 @@ namespace osu.Game
private readonly List<ICanAcceptFiles> fileImporters = new List<ICanAcceptFiles>(); private readonly List<ICanAcceptFiles> fileImporters = new List<ICanAcceptFiles>();
/// <summary>
/// Register a global handler for file imports. Most recently registered will have precedence.
/// </summary>
/// <param name="handler">The handler to register.</param>
public void RegisterImportHandler(ICanAcceptFiles handler) => fileImporters.Insert(0, handler);
/// <summary>
/// Unregister a global handler for file imports.
/// </summary>
/// <param name="handler">The previously registered handler.</param>
public void UnregisterImportHandler(ICanAcceptFiles handler) => fileImporters.Remove(handler);
public async Task Import(params string[] paths) public async Task Import(params string[] paths)
{ {
var extension = Path.GetExtension(paths.First())?.ToLowerInvariant(); var extension = Path.GetExtension(paths.First())?.ToLowerInvariant();
@ -354,7 +366,7 @@ namespace osu.Game
} }
} }
public string[] HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions).ToArray(); public IEnumerable<string> HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions);
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {

View File

@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
updateItems(); updateItems();
dropdown.Bindable = audio.AudioDevice; dropdown.Current = audio.AudioDevice;
audio.OnNewDevice += onDeviceChanged; audio.OnNewDevice += onDeviceChanged;
audio.OnLostDevice += onDeviceChanged; audio.OnLostDevice += onDeviceChanged;

View File

@ -21,23 +21,23 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Interface voices", LabelText = "Interface voices",
Bindable = config.GetBindable<bool>(OsuSetting.MenuVoice) Current = config.GetBindable<bool>(OsuSetting.MenuVoice)
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "osu! music theme", LabelText = "osu! music theme",
Bindable = config.GetBindable<bool>(OsuSetting.MenuMusic) Current = config.GetBindable<bool>(OsuSetting.MenuMusic)
}, },
new SettingsDropdown<IntroSequence> new SettingsDropdown<IntroSequence>
{ {
LabelText = "Intro sequence", LabelText = "Intro sequence",
Bindable = config.GetBindable<IntroSequence>(OsuSetting.IntroSequence), Current = config.GetBindable<IntroSequence>(OsuSetting.IntroSequence),
Items = Enum.GetValues(typeof(IntroSequence)).Cast<IntroSequence>() Items = Enum.GetValues(typeof(IntroSequence)).Cast<IntroSequence>()
}, },
new SettingsDropdown<BackgroundSource> new SettingsDropdown<BackgroundSource>
{ {
LabelText = "Background source", LabelText = "Background source",
Bindable = config.GetBindable<BackgroundSource>(OsuSetting.MenuBackgroundSource), Current = config.GetBindable<BackgroundSource>(OsuSetting.MenuBackgroundSource),
Items = Enum.GetValues(typeof(BackgroundSource)).Cast<BackgroundSource>() Items = Enum.GetValues(typeof(BackgroundSource)).Cast<BackgroundSource>()
} }
}; };

View File

@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
new SettingsSlider<double, OffsetSlider> new SettingsSlider<double, OffsetSlider>
{ {
LabelText = "Audio offset", LabelText = "Audio offset",
Bindable = config.GetBindable<double>(OsuSetting.AudioOffset), Current = config.GetBindable<double>(OsuSetting.AudioOffset),
KeyboardStep = 1f KeyboardStep = 1f
}, },
new SettingsButton new SettingsButton

View File

@ -20,28 +20,28 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
new SettingsSlider<double> new SettingsSlider<double>
{ {
LabelText = "Master", LabelText = "Master",
Bindable = audio.Volume, Current = audio.Volume,
KeyboardStep = 0.01f, KeyboardStep = 0.01f,
DisplayAsPercentage = true DisplayAsPercentage = true
}, },
new SettingsSlider<double> new SettingsSlider<double>
{ {
LabelText = "Master (window inactive)", LabelText = "Master (window inactive)",
Bindable = config.GetBindable<double>(OsuSetting.VolumeInactive), Current = config.GetBindable<double>(OsuSetting.VolumeInactive),
KeyboardStep = 0.01f, KeyboardStep = 0.01f,
DisplayAsPercentage = true DisplayAsPercentage = true
}, },
new SettingsSlider<double> new SettingsSlider<double>
{ {
LabelText = "Effect", LabelText = "Effect",
Bindable = audio.VolumeSample, Current = audio.VolumeSample,
KeyboardStep = 0.01f, KeyboardStep = 0.01f,
DisplayAsPercentage = true DisplayAsPercentage = true
}, },
new SettingsSlider<double> new SettingsSlider<double>
{ {
LabelText = "Music", LabelText = "Music",
Bindable = audio.VolumeTrack, Current = audio.VolumeTrack,
KeyboardStep = 0.01f, KeyboardStep = 0.01f,
DisplayAsPercentage = true DisplayAsPercentage = true
}, },

View File

@ -19,12 +19,12 @@ namespace osu.Game.Overlays.Settings.Sections.Debug
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Show log overlay", LabelText = "Show log overlay",
Bindable = frameworkConfig.GetBindable<bool>(FrameworkSetting.ShowLogOverlay) Current = frameworkConfig.GetBindable<bool>(FrameworkSetting.ShowLogOverlay)
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Bypass front-to-back render pass", LabelText = "Bypass front-to-back render pass",
Bindable = config.GetBindable<bool>(DebugSetting.BypassFrontToBackPass) Current = config.GetBindable<bool>(DebugSetting.BypassFrontToBackPass)
} }
}; };
} }

View File

@ -21,62 +21,62 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
new SettingsSlider<double> new SettingsSlider<double>
{ {
LabelText = "Background dim", LabelText = "Background dim",
Bindable = config.GetBindable<double>(OsuSetting.DimLevel), Current = config.GetBindable<double>(OsuSetting.DimLevel),
KeyboardStep = 0.01f, KeyboardStep = 0.01f,
DisplayAsPercentage = true DisplayAsPercentage = true
}, },
new SettingsSlider<double> new SettingsSlider<double>
{ {
LabelText = "Background blur", LabelText = "Background blur",
Bindable = config.GetBindable<double>(OsuSetting.BlurLevel), Current = config.GetBindable<double>(OsuSetting.BlurLevel),
KeyboardStep = 0.01f, KeyboardStep = 0.01f,
DisplayAsPercentage = true DisplayAsPercentage = true
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Lighten playfield during breaks", LabelText = "Lighten playfield during breaks",
Bindable = config.GetBindable<bool>(OsuSetting.LightenDuringBreaks) Current = config.GetBindable<bool>(OsuSetting.LightenDuringBreaks)
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Show score overlay", LabelText = "Show score overlay",
Bindable = config.GetBindable<bool>(OsuSetting.ShowInterface) Current = config.GetBindable<bool>(OsuSetting.ShowInterface)
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Show difficulty graph on progress bar", LabelText = "Show difficulty graph on progress bar",
Bindable = config.GetBindable<bool>(OsuSetting.ShowProgressGraph) Current = config.GetBindable<bool>(OsuSetting.ShowProgressGraph)
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Show health display even when you can't fail", LabelText = "Show health display even when you can't fail",
Bindable = config.GetBindable<bool>(OsuSetting.ShowHealthDisplayWhenCantFail), Current = config.GetBindable<bool>(OsuSetting.ShowHealthDisplayWhenCantFail),
Keywords = new[] { "hp", "bar" } Keywords = new[] { "hp", "bar" }
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Fade playfield to red when health is low", LabelText = "Fade playfield to red when health is low",
Bindable = config.GetBindable<bool>(OsuSetting.FadePlayfieldWhenHealthLow), Current = config.GetBindable<bool>(OsuSetting.FadePlayfieldWhenHealthLow),
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Always show key overlay", LabelText = "Always show key overlay",
Bindable = config.GetBindable<bool>(OsuSetting.KeyOverlay) Current = config.GetBindable<bool>(OsuSetting.KeyOverlay)
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Positional hitsounds", LabelText = "Positional hitsounds",
Bindable = config.GetBindable<bool>(OsuSetting.PositionalHitSounds) Current = config.GetBindable<bool>(OsuSetting.PositionalHitSounds)
}, },
new SettingsEnumDropdown<ScoreMeterType> new SettingsEnumDropdown<ScoreMeterType>
{ {
LabelText = "Score meter type", LabelText = "Score meter type",
Bindable = config.GetBindable<ScoreMeterType>(OsuSetting.ScoreMeter) Current = config.GetBindable<ScoreMeterType>(OsuSetting.ScoreMeter)
}, },
new SettingsEnumDropdown<ScoringMode> new SettingsEnumDropdown<ScoringMode>
{ {
LabelText = "Score display mode", LabelText = "Score display mode",
Bindable = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode) Current = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode)
} }
}; };
@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
Add(new SettingsCheckbox Add(new SettingsCheckbox
{ {
LabelText = "Disable Windows key during gameplay", LabelText = "Disable Windows key during gameplay",
Bindable = config.GetBindable<bool>(OsuSetting.GameplayDisableWinKey) Current = config.GetBindable<bool>(OsuSetting.GameplayDisableWinKey)
}); });
} }
} }

View File

@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Increase visibility of first object when visual impairment mods are enabled", LabelText = "Increase visibility of first object when visual impairment mods are enabled",
Bindable = config.GetBindable<bool>(OsuSetting.IncreaseFirstObjectVisibility), Current = config.GetBindable<bool>(OsuSetting.IncreaseFirstObjectVisibility),
}, },
}; };
} }

View File

@ -31,31 +31,31 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Right mouse drag to absolute scroll", LabelText = "Right mouse drag to absolute scroll",
Bindable = config.GetBindable<bool>(OsuSetting.SongSelectRightMouseScroll), Current = config.GetBindable<bool>(OsuSetting.SongSelectRightMouseScroll),
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Show converted beatmaps", LabelText = "Show converted beatmaps",
Bindable = config.GetBindable<bool>(OsuSetting.ShowConvertedBeatmaps), Current = config.GetBindable<bool>(OsuSetting.ShowConvertedBeatmaps),
}, },
new SettingsSlider<double, StarsSlider> new SettingsSlider<double, StarsSlider>
{ {
LabelText = "Display beatmaps from", LabelText = "Display beatmaps from",
Bindable = config.GetBindable<double>(OsuSetting.DisplayStarsMinimum), Current = config.GetBindable<double>(OsuSetting.DisplayStarsMinimum),
KeyboardStep = 0.1f, KeyboardStep = 0.1f,
Keywords = new[] { "minimum", "maximum", "star", "difficulty" } Keywords = new[] { "minimum", "maximum", "star", "difficulty" }
}, },
new SettingsSlider<double, MaximumStarsSlider> new SettingsSlider<double, MaximumStarsSlider>
{ {
LabelText = "up to", LabelText = "up to",
Bindable = config.GetBindable<double>(OsuSetting.DisplayStarsMaximum), Current = config.GetBindable<double>(OsuSetting.DisplayStarsMaximum),
KeyboardStep = 0.1f, KeyboardStep = 0.1f,
Keywords = new[] { "minimum", "maximum", "star", "difficulty" } Keywords = new[] { "minimum", "maximum", "star", "difficulty" }
}, },
new SettingsEnumDropdown<RandomSelectAlgorithm> new SettingsEnumDropdown<RandomSelectAlgorithm>
{ {
LabelText = "Random selection algorithm", LabelText = "Random selection algorithm",
Bindable = config.GetBindable<RandomSelectAlgorithm>(OsuSetting.RandomSelectAlgorithm), Current = config.GetBindable<RandomSelectAlgorithm>(OsuSetting.RandomSelectAlgorithm),
} }
}; };
} }

View File

@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Prefer metadata in original language", LabelText = "Prefer metadata in original language",
Bindable = frameworkConfig.GetBindable<bool>(FrameworkSetting.ShowUnicode) Current = frameworkConfig.GetBindable<bool>(FrameworkSetting.ShowUnicode)
}, },
}; };
} }

View File

@ -240,12 +240,12 @@ namespace osu.Game.Overlays.Settings.Sections.General
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Remember username", LabelText = "Remember username",
Bindable = config.GetBindable<bool>(OsuSetting.SaveUsername), Current = config.GetBindable<bool>(OsuSetting.SaveUsername),
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Stay signed in", LabelText = "Stay signed in",
Bindable = config.GetBindable<bool>(OsuSetting.SavePassword), Current = config.GetBindable<bool>(OsuSetting.SavePassword),
}, },
new Container new Container
{ {

View File

@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
Add(new SettingsEnumDropdown<ReleaseStream> Add(new SettingsEnumDropdown<ReleaseStream>
{ {
LabelText = "Release stream", LabelText = "Release stream",
Bindable = config.GetBindable<ReleaseStream>(OsuSetting.ReleaseStream), Current = config.GetBindable<ReleaseStream>(OsuSetting.ReleaseStream),
}); });
if (updateManager?.CanCheckForUpdate == true) if (updateManager?.CanCheckForUpdate == true)

View File

@ -19,22 +19,22 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Storyboard / Video", LabelText = "Storyboard / Video",
Bindable = config.GetBindable<bool>(OsuSetting.ShowStoryboard) Current = config.GetBindable<bool>(OsuSetting.ShowStoryboard)
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Hit Lighting", LabelText = "Hit Lighting",
Bindable = config.GetBindable<bool>(OsuSetting.HitLighting) Current = config.GetBindable<bool>(OsuSetting.HitLighting)
}, },
new SettingsEnumDropdown<ScreenshotFormat> new SettingsEnumDropdown<ScreenshotFormat>
{ {
LabelText = "Screenshot format", LabelText = "Screenshot format",
Bindable = config.GetBindable<ScreenshotFormat>(OsuSetting.ScreenshotFormat) Current = config.GetBindable<ScreenshotFormat>(OsuSetting.ScreenshotFormat)
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Show menu cursor in screenshots", LabelText = "Show menu cursor in screenshots",
Bindable = config.GetBindable<bool>(OsuSetting.ScreenshotCaptureMenuCursor) Current = config.GetBindable<bool>(OsuSetting.ScreenshotCaptureMenuCursor)
} }
}; };
} }

View File

@ -62,7 +62,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
windowModeDropdown = new SettingsDropdown<WindowMode> windowModeDropdown = new SettingsDropdown<WindowMode>
{ {
LabelText = "Screen mode", LabelText = "Screen mode",
Bindable = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode), Current = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode),
ItemSource = windowModes, ItemSource = windowModes,
}, },
resolutionSettingsContainer = new Container resolutionSettingsContainer = new Container
@ -74,14 +74,14 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
{ {
LabelText = "UI Scaling", LabelText = "UI Scaling",
TransferValueOnCommit = true, TransferValueOnCommit = true,
Bindable = osuConfig.GetBindable<float>(OsuSetting.UIScale), Current = osuConfig.GetBindable<float>(OsuSetting.UIScale),
KeyboardStep = 0.01f, KeyboardStep = 0.01f,
Keywords = new[] { "scale", "letterbox" }, Keywords = new[] { "scale", "letterbox" },
}, },
new SettingsEnumDropdown<ScalingMode> new SettingsEnumDropdown<ScalingMode>
{ {
LabelText = "Screen Scaling", LabelText = "Screen Scaling",
Bindable = osuConfig.GetBindable<ScalingMode>(OsuSetting.Scaling), Current = osuConfig.GetBindable<ScalingMode>(OsuSetting.Scaling),
Keywords = new[] { "scale", "letterbox" }, Keywords = new[] { "scale", "letterbox" },
}, },
scalingSettings = new FillFlowContainer<SettingsSlider<float>> scalingSettings = new FillFlowContainer<SettingsSlider<float>>
@ -97,28 +97,28 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
new SettingsSlider<float> new SettingsSlider<float>
{ {
LabelText = "Horizontal position", LabelText = "Horizontal position",
Bindable = scalingPositionX, Current = scalingPositionX,
KeyboardStep = 0.01f, KeyboardStep = 0.01f,
DisplayAsPercentage = true DisplayAsPercentage = true
}, },
new SettingsSlider<float> new SettingsSlider<float>
{ {
LabelText = "Vertical position", LabelText = "Vertical position",
Bindable = scalingPositionY, Current = scalingPositionY,
KeyboardStep = 0.01f, KeyboardStep = 0.01f,
DisplayAsPercentage = true DisplayAsPercentage = true
}, },
new SettingsSlider<float> new SettingsSlider<float>
{ {
LabelText = "Horizontal scale", LabelText = "Horizontal scale",
Bindable = scalingSizeX, Current = scalingSizeX,
KeyboardStep = 0.01f, KeyboardStep = 0.01f,
DisplayAsPercentage = true DisplayAsPercentage = true
}, },
new SettingsSlider<float> new SettingsSlider<float>
{ {
LabelText = "Vertical scale", LabelText = "Vertical scale",
Bindable = scalingSizeY, Current = scalingSizeY,
KeyboardStep = 0.01f, KeyboardStep = 0.01f,
DisplayAsPercentage = true DisplayAsPercentage = true
}, },
@ -126,7 +126,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
}, },
}; };
scalingSettings.ForEach(s => bindPreviewEvent(s.Bindable)); scalingSettings.ForEach(s => bindPreviewEvent(s.Current));
var resolutions = getResolutions(); var resolutions = getResolutions();
@ -137,10 +137,10 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
LabelText = "Resolution", LabelText = "Resolution",
ShowsDefaultIndicator = false, ShowsDefaultIndicator = false,
Items = resolutions, Items = resolutions,
Bindable = sizeFullscreen Current = sizeFullscreen
}; };
windowModeDropdown.Bindable.BindValueChanged(mode => windowModeDropdown.Current.BindValueChanged(mode =>
{ {
if (mode.NewValue == WindowMode.Fullscreen) if (mode.NewValue == WindowMode.Fullscreen)
{ {

View File

@ -23,17 +23,17 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
new SettingsEnumDropdown<FrameSync> new SettingsEnumDropdown<FrameSync>
{ {
LabelText = "Frame limiter", LabelText = "Frame limiter",
Bindable = config.GetBindable<FrameSync>(FrameworkSetting.FrameSync) Current = config.GetBindable<FrameSync>(FrameworkSetting.FrameSync)
}, },
new SettingsEnumDropdown<ExecutionMode> new SettingsEnumDropdown<ExecutionMode>
{ {
LabelText = "Threading mode", LabelText = "Threading mode",
Bindable = config.GetBindable<ExecutionMode>(FrameworkSetting.ExecutionMode) Current = config.GetBindable<ExecutionMode>(FrameworkSetting.ExecutionMode)
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Show FPS", LabelText = "Show FPS",
Bindable = osuConfig.GetBindable<bool>(OsuSetting.ShowFpsDisplay) Current = osuConfig.GetBindable<bool>(OsuSetting.ShowFpsDisplay)
}, },
}; };
} }

View File

@ -20,17 +20,17 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Rotate cursor when dragging", LabelText = "Rotate cursor when dragging",
Bindable = config.GetBindable<bool>(OsuSetting.CursorRotation) Current = config.GetBindable<bool>(OsuSetting.CursorRotation)
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Parallax", LabelText = "Parallax",
Bindable = config.GetBindable<bool>(OsuSetting.MenuParallax) Current = config.GetBindable<bool>(OsuSetting.MenuParallax)
}, },
new SettingsSlider<float, TimeSlider> new SettingsSlider<float, TimeSlider>
{ {
LabelText = "Hold-to-confirm activation time", LabelText = "Hold-to-confirm activation time",
Bindable = config.GetBindable<float>(OsuSetting.UIHoldActivationDelay), Current = config.GetBindable<float>(OsuSetting.UIHoldActivationDelay),
KeyboardStep = 50 KeyboardStep = 50
}, },
}; };

View File

@ -35,32 +35,32 @@ namespace osu.Game.Overlays.Settings.Sections.Input
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Raw input", LabelText = "Raw input",
Bindable = rawInputToggle Current = rawInputToggle
}, },
new SensitivitySetting new SensitivitySetting
{ {
LabelText = "Cursor sensitivity", LabelText = "Cursor sensitivity",
Bindable = sensitivityBindable Current = sensitivityBindable
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Map absolute input to window", LabelText = "Map absolute input to window",
Bindable = config.GetBindable<bool>(FrameworkSetting.MapAbsoluteInputToWindow) Current = config.GetBindable<bool>(FrameworkSetting.MapAbsoluteInputToWindow)
}, },
new SettingsEnumDropdown<ConfineMouseMode> new SettingsEnumDropdown<ConfineMouseMode>
{ {
LabelText = "Confine mouse cursor to window", LabelText = "Confine mouse cursor to window",
Bindable = config.GetBindable<ConfineMouseMode>(FrameworkSetting.ConfineMouseMode), Current = config.GetBindable<ConfineMouseMode>(FrameworkSetting.ConfineMouseMode),
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Disable mouse wheel during gameplay", LabelText = "Disable mouse wheel during gameplay",
Bindable = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableWheel) Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableWheel)
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Disable mouse buttons during gameplay", LabelText = "Disable mouse buttons during gameplay",
Bindable = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableButtons) Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableButtons)
}, },
}; };

View File

@ -19,13 +19,13 @@ namespace osu.Game.Overlays.Settings.Sections.Online
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Warn about opening external links", LabelText = "Warn about opening external links",
Bindable = config.GetBindable<bool>(OsuSetting.ExternalLinkWarning) Current = config.GetBindable<bool>(OsuSetting.ExternalLinkWarning)
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Prefer downloads without video", LabelText = "Prefer downloads without video",
Keywords = new[] { "no-video" }, Keywords = new[] { "no-video" },
Bindable = config.GetBindable<bool>(OsuSetting.PreferNoVideo) Current = config.GetBindable<bool>(OsuSetting.PreferNoVideo)
}, },
}; };
} }

View File

@ -47,29 +47,29 @@ namespace osu.Game.Overlays.Settings.Sections
new SettingsSlider<float, SizeSlider> new SettingsSlider<float, SizeSlider>
{ {
LabelText = "Menu cursor size", LabelText = "Menu cursor size",
Bindable = config.GetBindable<float>(OsuSetting.MenuCursorSize), Current = config.GetBindable<float>(OsuSetting.MenuCursorSize),
KeyboardStep = 0.01f KeyboardStep = 0.01f
}, },
new SettingsSlider<float, SizeSlider> new SettingsSlider<float, SizeSlider>
{ {
LabelText = "Gameplay cursor size", LabelText = "Gameplay cursor size",
Bindable = config.GetBindable<float>(OsuSetting.GameplayCursorSize), Current = config.GetBindable<float>(OsuSetting.GameplayCursorSize),
KeyboardStep = 0.01f KeyboardStep = 0.01f
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Adjust gameplay cursor size based on current beatmap", LabelText = "Adjust gameplay cursor size based on current beatmap",
Bindable = config.GetBindable<bool>(OsuSetting.AutoCursorSize) Current = config.GetBindable<bool>(OsuSetting.AutoCursorSize)
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Beatmap skins", LabelText = "Beatmap skins",
Bindable = config.GetBindable<bool>(OsuSetting.BeatmapSkins) Current = config.GetBindable<bool>(OsuSetting.BeatmapSkins)
}, },
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Beatmap hitsounds", LabelText = "Beatmap hitsounds",
Bindable = config.GetBindable<bool>(OsuSetting.BeatmapHitsounds) Current = config.GetBindable<bool>(OsuSetting.BeatmapHitsounds)
}, },
}; };
@ -81,7 +81,7 @@ namespace osu.Game.Overlays.Settings.Sections
config.BindWith(OsuSetting.Skin, configBindable); config.BindWith(OsuSetting.Skin, configBindable);
skinDropdown.Bindable = dropdownBindable; skinDropdown.Current = dropdownBindable;
skinDropdown.Items = skins.GetAllUsableSkins().ToArray(); skinDropdown.Items = skins.GetAllUsableSkins().ToArray();
// Todo: This should not be necessary when OsuConfigManager is databased // Todo: This should not be necessary when OsuConfigManager is databased

View File

@ -21,7 +21,7 @@ using osuTK;
namespace osu.Game.Overlays.Settings namespace osu.Game.Overlays.Settings
{ {
public abstract class SettingsItem<T> : Container, IFilterable, ISettingsItem public abstract class SettingsItem<T> : Container, IFilterable, ISettingsItem, IHasCurrentValue<T>
{ {
protected abstract Drawable CreateControl(); protected abstract Drawable CreateControl();
@ -54,7 +54,14 @@ namespace osu.Game.Overlays.Settings
} }
} }
public virtual Bindable<T> Bindable [Obsolete("Use Current instead")] // Can be removed 20210406
public Bindable<T> Bindable
{
get => Current;
set => Current = value;
}
public virtual Bindable<T> Current
{ {
get => controlWithCurrent.Current; get => controlWithCurrent.Current;
set => controlWithCurrent.Current = value; set => controlWithCurrent.Current = value;

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

@ -109,40 +109,40 @@ namespace osu.Game.Rulesets.Judgements
return 0; return 0;
case HitResult.SmallTickHit: case HitResult.SmallTickHit:
return DEFAULT_MAX_HEALTH_INCREASE * 0.05; return DEFAULT_MAX_HEALTH_INCREASE * 0.5;
case HitResult.SmallTickMiss: case HitResult.SmallTickMiss:
return -DEFAULT_MAX_HEALTH_INCREASE * 0.05; return -DEFAULT_MAX_HEALTH_INCREASE * 0.5;
case HitResult.LargeTickHit: case HitResult.LargeTickHit:
return DEFAULT_MAX_HEALTH_INCREASE * 0.1; return DEFAULT_MAX_HEALTH_INCREASE;
case HitResult.LargeTickMiss: case HitResult.LargeTickMiss:
return -DEFAULT_MAX_HEALTH_INCREASE * 0.1; return -DEFAULT_MAX_HEALTH_INCREASE;
case HitResult.Miss: case HitResult.Miss:
return -DEFAULT_MAX_HEALTH_INCREASE; return -DEFAULT_MAX_HEALTH_INCREASE;
case HitResult.Meh: case HitResult.Meh:
return -DEFAULT_MAX_HEALTH_INCREASE * 0.5; return -DEFAULT_MAX_HEALTH_INCREASE * 0.05;
case HitResult.Ok: case HitResult.Ok:
return -DEFAULT_MAX_HEALTH_INCREASE * 0.3; return DEFAULT_MAX_HEALTH_INCREASE * 0.5;
case HitResult.Good: case HitResult.Good:
return DEFAULT_MAX_HEALTH_INCREASE * 0.1; return DEFAULT_MAX_HEALTH_INCREASE * 0.75;
case HitResult.Great: case HitResult.Great:
return DEFAULT_MAX_HEALTH_INCREASE * 0.8;
case HitResult.Perfect:
return DEFAULT_MAX_HEALTH_INCREASE; return DEFAULT_MAX_HEALTH_INCREASE;
case HitResult.Perfect:
return DEFAULT_MAX_HEALTH_INCREASE * 1.05;
case HitResult.SmallBonus: case HitResult.SmallBonus:
return DEFAULT_MAX_HEALTH_INCREASE * 0.1; return DEFAULT_MAX_HEALTH_INCREASE * 0.5;
case HitResult.LargeBonus: case HitResult.LargeBonus:
return DEFAULT_MAX_HEALTH_INCREASE * 0.2; return DEFAULT_MAX_HEALTH_INCREASE;
} }
} }

View File

@ -385,9 +385,14 @@ namespace osu.Game.Rulesets.Objects.Drawables
} }
/// <summary> /// <summary>
/// Stops playback of all samples. Automatically called when <see cref="DrawableHitObject{TObject}"/>'s lifetime has been exceeded. /// Stops playback of all relevant samples. Generally only looping samples should be stopped by this, and the rest let to play out.
/// Automatically called when <see cref="DrawableHitObject{TObject}"/>'s lifetime has been exceeded.
/// </summary> /// </summary>
public virtual void StopAllSamples() => Samples?.Stop(); public virtual void StopAllSamples()
{
if (Samples?.Looping == true)
Samples.Stop();
}
protected override void Update() protected override void Update()
{ {
@ -457,6 +462,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
foreach (var nested in NestedHitObjects) foreach (var nested in NestedHitObjects)
nested.OnKilled(); nested.OnKilled();
// failsafe to ensure looping samples don't get stuck in a playing state.
// this could occur in a non-frame-stable context where DrawableHitObjects get killed before a SkinnableSound has the chance to be stopped.
StopAllSamples(); StopAllSamples();
UpdateResult(false); UpdateResult(false);
@ -506,19 +513,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
Result.TimeOffset = Math.Min(HitObject.HitWindows.WindowFor(HitResult.Miss), Time.Current - endTime); Result.TimeOffset = Math.Min(HitObject.HitWindows.WindowFor(HitResult.Miss), Time.Current - endTime);
switch (Result.Type) if (Result.HasResult)
{ updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss);
case HitResult.None:
break;
case HitResult.Miss:
updateState(ArmedState.Miss);
break;
default:
updateState(ArmedState.Hit);
break;
}
OnNewResult?.Invoke(this, Result); OnNewResult?.Invoke(this, Result);
} }

View File

@ -77,6 +77,7 @@ namespace osu.Game.Rulesets.Objects
/// <summary> /// <summary>
/// The hit windows for this <see cref="HitObject"/>. /// The hit windows for this <see cref="HitObject"/>.
/// </summary> /// </summary>
[JsonIgnore]
public HitWindows HitWindows { get; set; } public HitWindows HitWindows { get; set; }
private readonly List<HitObject> nestedHitObjects = new List<HitObject>(); private readonly List<HitObject> nestedHitObjects = new List<HitObject>();

View File

@ -35,6 +35,7 @@ namespace osu.Game.Rulesets.UI
public GameplayClock GameplayClock => stabilityGameplayClock; public GameplayClock GameplayClock => stabilityGameplayClock;
[Cached(typeof(GameplayClock))] [Cached(typeof(GameplayClock))]
[Cached(typeof(ISamplePlaybackDisabler))]
private readonly StabilityGameplayClock stabilityGameplayClock; private readonly StabilityGameplayClock stabilityGameplayClock;
public FrameStabilityContainer(double gameplayStartTime = double.MinValue) public FrameStabilityContainer(double gameplayStartTime = double.MinValue)
@ -58,13 +59,16 @@ namespace osu.Game.Rulesets.UI
private int direction; private int direction;
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(GameplayClock clock) private void load(GameplayClock clock, ISamplePlaybackDisabler sampleDisabler)
{ {
if (clock != null) if (clock != null)
{ {
parentGameplayClock = stabilityGameplayClock.ParentGameplayClock = clock; parentGameplayClock = stabilityGameplayClock.ParentGameplayClock = clock;
GameplayClock.IsPaused.BindTo(clock.IsPaused); GameplayClock.IsPaused.BindTo(clock.IsPaused);
} }
// this is a bit temporary. should really be done inside of GameplayClock (but requires large structural changes).
stabilityGameplayClock.ParentSampleDisabler = sampleDisabler;
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -206,12 +210,16 @@ namespace osu.Game.Rulesets.UI
} }
private void setClock() private void setClock()
{
if (parentGameplayClock == null)
{ {
// in case a parent gameplay clock isn't available, just use the parent clock. // in case a parent gameplay clock isn't available, just use the parent clock.
parentGameplayClock ??= Clock; parentGameplayClock ??= Clock;
}
else
{
Clock = GameplayClock; Clock = GameplayClock;
ProcessCustomClock = false; }
} }
public ReplayInputHandler ReplayInputHandler { get; set; } public ReplayInputHandler ReplayInputHandler { get; set; }
@ -220,6 +228,8 @@ namespace osu.Game.Rulesets.UI
{ {
public GameplayClock ParentGameplayClock; public GameplayClock ParentGameplayClock;
public ISamplePlaybackDisabler ParentSampleDisabler;
public override IEnumerable<Bindable<double>> NonGameplayAdjustments => ParentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty<Bindable<double>>(); public override IEnumerable<Bindable<double>> NonGameplayAdjustments => ParentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty<Bindable<double>>();
public StabilityGameplayClock(FramedClock underlyingClock) public StabilityGameplayClock(FramedClock underlyingClock)
@ -227,7 +237,11 @@ namespace osu.Game.Rulesets.UI
{ {
} }
public override bool IsSeeking => ParentGameplayClock != null && Math.Abs(CurrentTime - ParentGameplayClock.CurrentTime) > 200; protected override bool ShouldDisableSamplePlayback =>
// handle the case where playback is catching up to real-time.
base.ShouldDisableSamplePlayback
|| ParentSampleDisabler?.SamplePlaybackDisabled.Value == true
|| (ParentGameplayClock != null && Math.Abs(CurrentTime - ParentGameplayClock.CurrentTime) > 200);
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show More