1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-15 08:12:56 +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,25 +28,33 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestFixture] [TestFixture]
public class TestSceneNotes : OsuTestScene public class TestSceneNotes : OsuTestScene
{ {
[BackgroundDependencyLoader] [Test]
private void load() public void TestVariousNotes()
{ {
Child = new FillFlowContainer DrawableNote note1 = null;
DrawableNote note2 = null;
DrawableHoldNote holdNote1 = null;
DrawableHoldNote holdNote2 = null;
AddStep("create notes", () =>
{ {
Clock = new FramedClock(new ManualClock()), Child = new FillFlowContainer
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(20),
Children = new[]
{ {
createNoteDisplay(ScrollingDirection.Down, 1, out var note1), Clock = new FramedClock(new ManualClock()),
createNoteDisplay(ScrollingDirection.Up, 2, out var note2), Anchor = Anchor.Centre,
createHoldNoteDisplay(ScrollingDirection.Down, 1, out var holdNote1), Origin = Anchor.Centre,
createHoldNoteDisplay(ScrollingDirection.Up, 2, out var holdNote2), AutoSizeAxes = Axes.Both,
} Direction = FillDirection.Horizontal,
}; Spacing = new Vector2(20),
Children = new[]
{
createNoteDisplay(ScrollingDirection.Down, 1, out note1),
createNoteDisplay(ScrollingDirection.Up, 2, out note2),
createHoldNoteDisplay(ScrollingDirection.Down, 1, out holdNote1),
createHoldNoteDisplay(ScrollingDirection.Up, 2, out holdNote2),
}
};
});
AddAssert("note 1 facing downwards", () => verifyAnchors(note1, Anchor.y2)); AddAssert("note 1 facing downwards", () => verifyAnchors(note1, Anchor.y2));
AddAssert("note 2 facing upwards", () => verifyAnchors(note2, Anchor.y0)); AddAssert("note 2 facing upwards", () => verifyAnchors(note2, Anchor.y0));

View File

@ -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;
SelectionBox.CanScaleX = canOperate;
SelectionBox.CanScaleY = canOperate;
}
protected override void OnOperationEnded()
{
base.OnOperationEnded();
referenceOrigin = null;
}
public override bool HandleMovement(MoveSelectionEvent moveEvent) =>
moveSelection(moveEvent.InstantDelta);
/// <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)
{
var hitObjects = selectedMovableObjects;
var selectedObjectsQuad = getSurroundingQuad(hitObjects);
var centre = selectedObjectsQuad.Centre;
foreach (var h in hitObjects)
{ {
if (h is Spinner) var pos = h.Position;
switch (direction)
{ {
// Spinners don't support position adjustments case Direction.Horizontal:
continue; pos.X = centre.X - (pos.X - centre.X);
break;
case Direction.Vertical:
pos.Y = centre.Y - (pos.Y - centre.Y);
break;
} }
// Stacking is not considered h.Position = pos;
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));
}
if (minPosition.X < 0 || minPosition.Y < 0 || maxPosition.X > DrawWidth || maxPosition.Y > DrawHeight) if (h is Slider slider)
return false;
foreach (var h in SelectedHitObjects.OfType<OsuHitObject>())
{
if (h is Spinner)
{ {
// Spinners don't support position adjustments foreach (var point in slider.Path.ControlPoints)
continue; {
point.Position.Value = new Vector2(
(direction == Direction.Horizontal ? -1 : 1) * point.Position.Value.X,
(direction == Direction.Vertical ? -1 : 1) * point.Position.Value.Y
);
}
} }
h.Position += moveEvent.InstantDelta;
} }
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,7 +135,8 @@ 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);
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); if (hasNumber)
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
} }
private void updateState(ValueChangedEvent<ArmedState> state) private void updateState(ValueChangedEvent<ArmedState> state)
@ -120,16 +149,19 @@ 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);
var legacyVersion = skin.GetConfig<LegacySetting, decimal>(LegacySetting.Version)?.Value; if (hasNumber)
if (legacyVersion >= 2.0m)
// legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out);
else
{ {
// old skins scale and fade it normally along other pieces. var legacyVersion = skin.GetConfig<LegacySetting, decimal>(LegacySetting.Version)?.Value;
hitCircleText.FadeOut(legacy_fade_duration, Easing.Out);
hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); if (legacyVersion >= 2.0m)
// legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out);
else
{
// old skins scale and fade it normally along other pieces.
hitCircleText.FadeOut(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,16 +163,14 @@ 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)
{ {
target = rimHit; target = rimHit;
back = rim; back = rim;
if (gameplayClock?.IsSeeking != true) drumSample.Rim?.Play();
drumSample.Rim?.Play();
} }
if (target != null) if (target != null)

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()
@ -207,11 +211,15 @@ namespace osu.Game.Rulesets.UI
private void setClock() private void setClock()
{ {
// in case a parent gameplay clock isn't available, just use the parent clock. if (parentGameplayClock == null)
parentGameplayClock ??= Clock; {
// in case a parent gameplay clock isn't available, just use the parent clock.
Clock = GameplayClock; parentGameplayClock ??= Clock;
ProcessCustomClock = false; }
else
{
Clock = GameplayClock;
}
} }
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