mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 10:12:54 +08:00
Merge branch 'master' into confine-during-gameplay
This commit is contained in:
commit
d5782c95bd
@ -2,10 +2,40 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Judgements
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
@ -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 assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.IgnoreHit && !judgementResults.First().IsHit;
|
||||
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;
|
||||
|
||||
|
@ -1,7 +1,13 @@
|
||||
// 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 System.Collections.Generic;
|
||||
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.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
@ -10,40 +16,219 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public class OsuSelectionHandler : SelectionHandler
|
||||
{
|
||||
public override bool HandleMovement(MoveSelectionEvent moveEvent)
|
||||
protected override void OnSelectionChanged()
|
||||
{
|
||||
Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
|
||||
Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
|
||||
base.OnSelectionChanged();
|
||||
|
||||
// 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 h in SelectedHitObjects.OfType<OsuHitObject>())
|
||||
bool canOperate = SelectedHitObjects.Count() > 1 || SelectedHitObjects.Any(s => s is Slider);
|
||||
|
||||
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
|
||||
continue;
|
||||
case Direction.Horizontal:
|
||||
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
|
||||
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));
|
||||
}
|
||||
h.Position = pos;
|
||||
|
||||
if (minPosition.X < 0 || minPosition.Y < 0 || maxPosition.X > DrawWidth || maxPosition.Y > DrawHeight)
|
||||
return false;
|
||||
|
||||
foreach (var h in SelectedHitObjects.OfType<OsuHitObject>())
|
||||
{
|
||||
if (h is Spinner)
|
||||
if (h is Slider slider)
|
||||
{
|
||||
// Spinners don't support position adjustments
|
||||
continue;
|
||||
foreach (var point in slider.Path.ControlPoints)
|
||||
{
|
||||
point.Position.Value = new Vector2(
|
||||
(direction == Direction.Horizontal ? -1 : 1) * point.Position.Value.X,
|
||||
(direction == Direction.Vertical ? -1 : 1) * point.Position.Value.Y
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
h.Position += moveEvent.InstantDelta;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,6 +39,9 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
base.ApplyToDrawableHitObjects(drawables);
|
||||
}
|
||||
|
||||
private double lastSliderHeadFadeOutStartTime;
|
||||
private double lastSliderHeadFadeOutDuration;
|
||||
|
||||
protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state)
|
||||
{
|
||||
if (!(drawable is DrawableOsuHitObject d))
|
||||
@ -54,7 +57,35 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
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:
|
||||
|
||||
if (circle is DrawableSliderHead)
|
||||
{
|
||||
lastSliderHeadFadeOutDuration = fadeOutDuration;
|
||||
lastSliderHeadFadeOutStartTime = fadeOutStartTime;
|
||||
}
|
||||
|
||||
// we don't want to see the approach circle
|
||||
using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
|
||||
circle.ApproachCircle.Hide();
|
||||
|
@ -51,6 +51,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
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 },
|
||||
repeatContainer = new Container<DrawableSliderRepeat> { RelativeSizeAxes = Axes.Both },
|
||||
Ball = new SliderBall(s, this)
|
||||
@ -62,7 +63,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
Alpha = 0
|
||||
},
|
||||
headContainer = new Container<DrawableSliderHead> { RelativeSizeAxes = Axes.Both },
|
||||
tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both },
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -6,9 +6,11 @@ using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
@ -22,6 +24,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
private readonly Drawable scaleContainer;
|
||||
|
||||
public readonly Drawable CirclePiece;
|
||||
|
||||
public override bool DisplayResult => false;
|
||||
|
||||
public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider)
|
||||
@ -34,7 +38,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
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();
|
||||
@ -85,6 +100,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
private bool hasRotation;
|
||||
|
||||
private readonly ReverseArrowPiece arrow;
|
||||
|
||||
public void UpdateSnakingPosition(Vector2 start, Vector2 end)
|
||||
{
|
||||
// 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));
|
||||
while (Math.Abs(aimRotation - Rotation) > 180)
|
||||
aimRotation += aimRotation < Rotation ? 360 : -360;
|
||||
while (Math.Abs(aimRotation - arrow.Rotation) > 180)
|
||||
aimRotation += aimRotation < arrow.Rotation ? 360 : -360;
|
||||
|
||||
if (!hasRotation)
|
||||
{
|
||||
Rotation = aimRotation;
|
||||
arrow.Rotation = aimRotation;
|
||||
hasRotation = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,20 @@
|
||||
// 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.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
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>
|
||||
/// 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; }
|
||||
|
||||
private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>();
|
||||
private readonly IBindable<int> pathVersion = new Bindable<int>();
|
||||
private readonly IBindable<float> scaleBindable = new BindableFloat();
|
||||
|
||||
public DrawableSliderTail(Slider slider, SliderTailCircle hitCircle)
|
||||
: base(hitCircle)
|
||||
private readonly SkinnableDrawable circlePiece;
|
||||
|
||||
private readonly Container scaleContainer;
|
||||
|
||||
public DrawableSliderTail(Slider slider, SliderTailCircle tailCircle)
|
||||
: base(tailCircle)
|
||||
{
|
||||
this.slider = slider;
|
||||
|
||||
this.tailCircle = tailCircle;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
FillMode = FillMode.Fit;
|
||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
||||
|
||||
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);
|
||||
pathVersion.BindTo(slider.Path.Version);
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
scaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true);
|
||||
scaleBindable.BindTo(HitObject.ScaleBindable);
|
||||
}
|
||||
|
||||
positionBindable.BindValueChanged(_ => updatePosition());
|
||||
pathVersion.BindValueChanged(_ => updatePosition(), true);
|
||||
protected override void UpdateInitialTransforms()
|
||||
{
|
||||
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)
|
||||
@ -48,6 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -212,7 +212,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
return;
|
||||
|
||||
// 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);
|
||||
|
||||
ApplyResult(r =>
|
||||
@ -268,7 +268,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
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.
|
||||
if (tick != null)
|
||||
|
@ -176,6 +176,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
// if this is to change, we should revisit this.
|
||||
AddNested(TailCircle = new SliderTailCircle(this)
|
||||
{
|
||||
RepeatIndex = e.SpanIndex,
|
||||
StartTime = e.Time,
|
||||
Position = EndPosition,
|
||||
StackHeight = StackHeight
|
||||
@ -183,10 +184,9 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
break;
|
||||
|
||||
case SliderEventType.Repeat:
|
||||
AddNested(new SliderRepeat
|
||||
AddNested(new SliderRepeat(this)
|
||||
{
|
||||
RepeatIndex = e.SpanIndex,
|
||||
SpanDuration = SpanDuration,
|
||||
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
|
||||
Position = Position + Path.PositionAt(e.PathProgress),
|
||||
StackHeight = StackHeight,
|
||||
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
50
osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs
Normal file
50
osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -1,35 +1,19 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects
|
||||
{
|
||||
public class SliderRepeat : OsuHitObject
|
||||
public class SliderRepeat : SliderEndCircle
|
||||
{
|
||||
public int RepeatIndex { get; set; }
|
||||
public double SpanDuration { get; set; }
|
||||
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
|
||||
public SliderRepeat(Slider slider)
|
||||
: base(slider)
|
||||
{
|
||||
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 class SliderRepeatJudgement : OsuJudgement
|
||||
|
@ -1,7 +1,6 @@
|
||||
// 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.Bindables;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
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.
|
||||
/// See <see cref="SliderEventType.LegacyLastTick"/> usage in <see cref="Slider"/> for more information.
|
||||
/// </summary>
|
||||
public class SliderTailCircle : SliderCircle
|
||||
public class SliderTailCircle : SliderEndCircle
|
||||
{
|
||||
private readonly IBindable<int> pathVersion = new Bindable<int>();
|
||||
|
||||
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 class SliderTailJudgement : OsuJudgement
|
||||
{
|
||||
public override HitResult MaxResult => HitResult.IgnoreHit;
|
||||
public override HitResult MaxResult => HitResult.SmallTickHit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
ReverseArrow,
|
||||
HitCircleText,
|
||||
SliderHeadHitCircle,
|
||||
SliderTailHitCircle,
|
||||
SliderFollowCircle,
|
||||
SliderBall,
|
||||
SliderBody,
|
||||
|
@ -1,9 +1,12 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
@ -15,6 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
|
||||
private bool disjointTrail;
|
||||
private double lastTrailTime;
|
||||
private IBindable<float> cursorSize;
|
||||
|
||||
public LegacyCursorTrail()
|
||||
{
|
||||
@ -22,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
private void load(ISkinSource skin, OsuConfigManager config)
|
||||
{
|
||||
Texture = skin.GetTexture("cursortrail");
|
||||
disjointTrail = skin.GetTexture("cursormiddle") == null;
|
||||
@ -32,12 +36,16 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
// stable "magic ratio". see OsuPlayfieldAdjustmentContainer for full explanation.
|
||||
Texture.ScaleAdjust *= 1.6f;
|
||||
}
|
||||
|
||||
cursorSize = config.GetBindable<float>(OsuSetting.GameplayCursorSize).GetBoundCopy();
|
||||
}
|
||||
|
||||
protected override double FadeDuration => disjointTrail ? 150 : 500;
|
||||
|
||||
protected override bool InterpolateMovements => !disjointTrail;
|
||||
|
||||
protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1);
|
||||
|
||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||
{
|
||||
if (!disjointTrail)
|
||||
|
@ -21,10 +21,12 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
public class LegacyMainCirclePiece : CompositeDrawable
|
||||
{
|
||||
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.hasNumber = hasNumber;
|
||||
|
||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
||||
}
|
||||
@ -70,7 +72,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
}
|
||||
}
|
||||
},
|
||||
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),
|
||||
UseFullGlyphHeight = false,
|
||||
@ -78,8 +84,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
bool overlayAboveNumber = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
|
||||
|
||||
@ -107,7 +113,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
|
||||
state.BindValueChanged(updateState, 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)
|
||||
@ -120,16 +127,19 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
circleSprites.FadeOut(legacy_fade_duration, Easing.Out);
|
||||
circleSprites.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
|
||||
|
||||
var legacyVersion = skin.GetConfig<LegacySetting, decimal>(LegacySetting.Version)?.Value;
|
||||
|
||||
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
|
||||
if (hasNumber)
|
||||
{
|
||||
// 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);
|
||||
var legacyVersion = skin.GetConfig<LegacySetting, decimal>(LegacySetting.Version)?.Value;
|
||||
|
||||
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;
|
||||
|
@ -66,6 +66,12 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
|
||||
return null;
|
||||
|
||||
case OsuSkinComponents.SliderTailHitCircle:
|
||||
if (hasHitCircle.Value)
|
||||
return new LegacyMainCirclePiece("sliderendcircle", false);
|
||||
|
||||
return null;
|
||||
|
||||
case OsuSkinComponents.SliderHeadHitCircle:
|
||||
if (hasHitCircle.Value)
|
||||
return new LegacyMainCirclePiece("sliderstartcircle");
|
||||
|
@ -119,6 +119,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
/// </summary>
|
||||
protected virtual bool InterpolateMovements => true;
|
||||
|
||||
protected virtual float IntervalMultiplier => 1.0f;
|
||||
|
||||
private Vector2? lastPosition;
|
||||
private readonly InputResampler resampler = new InputResampler();
|
||||
|
||||
@ -147,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
float distance = diff.Length;
|
||||
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)
|
||||
{
|
||||
|
@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Judgements
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.Great:
|
||||
case HitResult.SmallTickHit:
|
||||
return 0.15;
|
||||
|
||||
default:
|
||||
|
@ -163,16 +163,14 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
target = centreHit;
|
||||
back = centre;
|
||||
|
||||
if (gameplayClock?.IsSeeking != true)
|
||||
drumSample.Centre?.Play();
|
||||
drumSample.Centre?.Play();
|
||||
}
|
||||
else if (action == RimAction)
|
||||
{
|
||||
target = rimHit;
|
||||
back = rim;
|
||||
|
||||
if (gameplayClock?.IsSeeking != true)
|
||||
drumSample.Rim?.Play();
|
||||
drumSample.Rim?.Play();
|
||||
}
|
||||
|
||||
if (target != null)
|
||||
|
69
osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs
Normal file
69
osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -5,6 +5,8 @@ using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
@ -22,6 +24,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
protected override bool EditorComponentsReady => Editor.ChildrenOfType<SetupScreen>().SingleOrDefault()?.IsLoaded == true;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmapManager { get; set; }
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
AddStep("set dummy", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null));
|
||||
@ -38,6 +43,15 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
AddStep("save beatmap", () => Editor.Save());
|
||||
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]
|
||||
|
@ -19,12 +19,14 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
public void TestSlidingSampleStopsOnSeek()
|
||||
{
|
||||
DrawableSlider slider = null;
|
||||
DrawableSample[] samples = null;
|
||||
DrawableSample[] loopingSamples = null;
|
||||
DrawableSample[] onceOffSamples = null;
|
||||
|
||||
AddStep("get first slider", () =>
|
||||
{
|
||||
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());
|
||||
@ -34,14 +36,15 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
if (!slider.Tracking.Value)
|
||||
return false;
|
||||
|
||||
if (!samples.Any(s => s.Playing))
|
||||
if (!loopingSamples.Any(s => s.Playing))
|
||||
return false;
|
||||
|
||||
EditorClock.Seek(20000);
|
||||
return true;
|
||||
});
|
||||
|
||||
AddAssert("slider samples are not playing", () => samples.Length == 5 && samples.All(s => s.Played && !s.Playing));
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -114,6 +114,25 @@ namespace osu.Game.Beatmaps
|
||||
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 readonly List<CancellationTokenSource> linkedCancellationSources = new List<CancellationTokenSource>();
|
||||
|
||||
@ -307,5 +326,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
// Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...)
|
||||
}
|
||||
|
||||
public DifficultyRating DifficultyRating => BeatmapDifficultyManager.GetDifficultyRating(Stars);
|
||||
}
|
||||
}
|
||||
|
@ -135,21 +135,7 @@ namespace osu.Game.Beatmaps
|
||||
public List<ScoreInfo> Scores { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public DifficultyRating DifficultyRating
|
||||
{
|
||||
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 DifficultyRating DifficultyRating => BeatmapDifficultyManager.GetDifficultyRating(StarDifficulty);
|
||||
|
||||
public string[] SearchableTerms => new[]
|
||||
{
|
||||
|
@ -57,7 +57,7 @@ namespace osu.Game.Beatmaps
|
||||
/// </summary>
|
||||
public readonly WorkingBeatmap DefaultBeatmap;
|
||||
|
||||
public override string[] HandledExtensions => new[] { ".osz" };
|
||||
public override IEnumerable<string> HandledExtensions => new[] { ".osz" };
|
||||
|
||||
protected override string[] HashableFileTypes => new[] { ".osu" };
|
||||
|
||||
|
@ -2,6 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Beatmaps.ControlPoints
|
||||
{
|
||||
@ -18,6 +20,8 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
|
||||
public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time);
|
||||
|
||||
public virtual Color4 GetRepresentingColour(OsuColour colours) => colours.Yellow;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this <see cref="ControlPoint"/> results in a meaningful change when placed alongside another.
|
||||
/// </summary>
|
||||
|
@ -2,6 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Beatmaps.ControlPoints
|
||||
{
|
||||
@ -23,6 +25,8 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
MaxValue = 10
|
||||
};
|
||||
|
||||
public override Color4 GetRepresentingColour(OsuColour colours) => colours.GreenDark;
|
||||
|
||||
/// <summary>
|
||||
/// The speed multiplier at this control point.
|
||||
/// </summary>
|
||||
|
@ -2,6 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Beatmaps.ControlPoints
|
||||
{
|
||||
@ -18,6 +20,8 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// </summary>
|
||||
public readonly BindableBool OmitFirstBarLineBindable = new BindableBool();
|
||||
|
||||
public override Color4 GetRepresentingColour(OsuColour colours) => colours.Purple;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the first bar line of this control point is ignored.
|
||||
/// </summary>
|
||||
|
@ -3,6 +3,8 @@
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Beatmaps.ControlPoints
|
||||
{
|
||||
@ -16,6 +18,8 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
SampleVolumeBindable = { Disabled = true }
|
||||
};
|
||||
|
||||
public override Color4 GetRepresentingColour(OsuColour colours) => colours.Pink;
|
||||
|
||||
/// <summary>
|
||||
/// The default sample bank at this control point.
|
||||
/// </summary>
|
||||
|
@ -3,6 +3,8 @@
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Beatmaps.ControlPoints
|
||||
{
|
||||
@ -18,6 +20,8 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// </summary>
|
||||
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
|
||||
{
|
||||
BeatLengthBindable =
|
||||
|
@ -2,7 +2,11 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -14,6 +18,7 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -21,9 +26,6 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
{
|
||||
public class DifficultyIcon : CompositeDrawable, IHasCustomTooltip
|
||||
{
|
||||
private readonly BeatmapInfo beatmap;
|
||||
private readonly RulesetInfo ruleset;
|
||||
|
||||
private readonly Container iconContainer;
|
||||
|
||||
/// <summary>
|
||||
@ -35,23 +37,49 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
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.ruleset = ruleset ?? beatmap.Ruleset;
|
||||
if (shouldShowTooltip)
|
||||
TooltipContent = beatmap;
|
||||
this.shouldShowTooltip = shouldShowTooltip;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = iconContainer = new Container { Size = new Vector2(20f) };
|
||||
}
|
||||
|
||||
public ITooltip GetCustomTooltip() => new DifficultyIconTooltip();
|
||||
|
||||
public object TooltipContent { get; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
@ -70,10 +98,10 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Radius = 5,
|
||||
},
|
||||
Child = new Box
|
||||
Child = background = new Box
|
||||
{
|
||||
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
|
||||
@ -82,16 +110,73 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
Origin = Anchor.Centre,
|
||||
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)
|
||||
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 readonly OsuSpriteText difficultyName, starRating;
|
||||
private readonly Box background;
|
||||
|
||||
private readonly FillFlowContainer difficultyFlow;
|
||||
|
||||
public DifficultyIconTooltip()
|
||||
@ -159,14 +244,22 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
background.Colour = colours.Gray3;
|
||||
}
|
||||
|
||||
private readonly IBindable<StarDifficulty> starDifficulty = new Bindable<StarDifficulty>();
|
||||
|
||||
public bool SetContent(object content)
|
||||
{
|
||||
if (!(content is BeatmapInfo beatmap))
|
||||
if (!(content is DifficultyIconTooltipContent iconContent))
|
||||
return false;
|
||||
|
||||
difficultyName.Text = beatmap.Version;
|
||||
starRating.Text = $"{beatmap.StarDifficulty:0.##}";
|
||||
difficultyFlow.Colour = colours.ForDifficultyRating(beatmap.DifficultyRating, true);
|
||||
difficultyName.Text = iconContent.Beatmap.Version;
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
public class GroupedDifficultyIcon : DifficultyIcon
|
||||
{
|
||||
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
|
||||
{
|
||||
|
@ -70,7 +70,7 @@ namespace osu.Game.Database
|
||||
|
||||
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;
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
// 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.Threading.Tasks;
|
||||
|
||||
namespace osu.Game.Database
|
||||
@ -19,6 +20,6 @@ namespace osu.Game.Database
|
||||
/// <summary>
|
||||
/// An array of accepted file extensions (in the standard format of ".abc").
|
||||
/// </summary>
|
||||
string[] HandledExtensions { get; }
|
||||
IEnumerable<string> HandledExtensions { get; }
|
||||
}
|
||||
}
|
||||
|
@ -232,9 +232,9 @@ namespace osu.Game
|
||||
dependencies.Cache(new SessionStatics());
|
||||
dependencies.Cache(new OsuColour());
|
||||
|
||||
fileImporters.Add(BeatmapManager);
|
||||
fileImporters.Add(ScoreManager);
|
||||
fileImporters.Add(SkinManager);
|
||||
RegisterImportHandler(BeatmapManager);
|
||||
RegisterImportHandler(ScoreManager);
|
||||
RegisterImportHandler(SkinManager);
|
||||
|
||||
// tracks play so loud our samples can't keep up.
|
||||
// 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>();
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
@ -109,40 +109,40 @@ namespace osu.Game.Rulesets.Judgements
|
||||
return 0;
|
||||
|
||||
case HitResult.SmallTickHit:
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.05;
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.5;
|
||||
|
||||
case HitResult.SmallTickMiss:
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE * 0.05;
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE * 0.5;
|
||||
|
||||
case HitResult.LargeTickHit:
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.1;
|
||||
return DEFAULT_MAX_HEALTH_INCREASE;
|
||||
|
||||
case HitResult.LargeTickMiss:
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE * 0.1;
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE;
|
||||
|
||||
case HitResult.Miss:
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE;
|
||||
|
||||
case HitResult.Meh:
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE * 0.5;
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE * 0.05;
|
||||
|
||||
case HitResult.Ok:
|
||||
return -DEFAULT_MAX_HEALTH_INCREASE * 0.3;
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.5;
|
||||
|
||||
case HitResult.Good:
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.1;
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.75;
|
||||
|
||||
case HitResult.Great:
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.8;
|
||||
|
||||
case HitResult.Perfect:
|
||||
return DEFAULT_MAX_HEALTH_INCREASE;
|
||||
|
||||
case HitResult.Perfect:
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 1.05;
|
||||
|
||||
case HitResult.SmallBonus:
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.1;
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.5;
|
||||
|
||||
case HitResult.LargeBonus:
|
||||
return DEFAULT_MAX_HEALTH_INCREASE * 0.2;
|
||||
return DEFAULT_MAX_HEALTH_INCREASE;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -385,9 +385,14 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public virtual void StopAllSamples() => Samples?.Stop();
|
||||
public virtual void StopAllSamples()
|
||||
{
|
||||
if (Samples?.Looping == true)
|
||||
Samples.Stop();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
@ -457,6 +462,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
foreach (var nested in NestedHitObjects)
|
||||
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();
|
||||
|
||||
UpdateResult(false);
|
||||
|
@ -35,6 +35,7 @@ namespace osu.Game.Rulesets.UI
|
||||
public GameplayClock GameplayClock => stabilityGameplayClock;
|
||||
|
||||
[Cached(typeof(GameplayClock))]
|
||||
[Cached(typeof(ISamplePlaybackDisabler))]
|
||||
private readonly StabilityGameplayClock stabilityGameplayClock;
|
||||
|
||||
public FrameStabilityContainer(double gameplayStartTime = double.MinValue)
|
||||
@ -58,13 +59,16 @@ namespace osu.Game.Rulesets.UI
|
||||
private int direction;
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(GameplayClock clock)
|
||||
private void load(GameplayClock clock, ISamplePlaybackDisabler sampleDisabler)
|
||||
{
|
||||
if (clock != null)
|
||||
{
|
||||
parentGameplayClock = stabilityGameplayClock.ParentGameplayClock = clock;
|
||||
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()
|
||||
@ -207,11 +211,15 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
private void setClock()
|
||||
{
|
||||
// in case a parent gameplay clock isn't available, just use the parent clock.
|
||||
parentGameplayClock ??= Clock;
|
||||
|
||||
Clock = GameplayClock;
|
||||
ProcessCustomClock = false;
|
||||
if (parentGameplayClock == null)
|
||||
{
|
||||
// in case a parent gameplay clock isn't available, just use the parent clock.
|
||||
parentGameplayClock ??= Clock;
|
||||
}
|
||||
else
|
||||
{
|
||||
Clock = GameplayClock;
|
||||
}
|
||||
}
|
||||
|
||||
public ReplayInputHandler ReplayInputHandler { get; set; }
|
||||
@ -220,6 +228,8 @@ namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
public GameplayClock ParentGameplayClock;
|
||||
|
||||
public ISamplePlaybackDisabler ParentSampleDisabler;
|
||||
|
||||
public override IEnumerable<Bindable<double>> NonGameplayAdjustments => ParentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty<Bindable<double>>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ namespace osu.Game.Scoring
|
||||
{
|
||||
public class ScoreManager : DownloadableArchiveModelManager<ScoreInfo, ScoreFileInfo>
|
||||
{
|
||||
public override string[] HandledExtensions => new[] { ".osr" };
|
||||
public override IEnumerable<string> HandledExtensions => new[] { ".osr" };
|
||||
|
||||
protected override string[] HashableFileTypes => new[] { ".osr" };
|
||||
|
||||
|
@ -45,7 +45,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
Masking = true,
|
||||
BorderColour = Color4.White,
|
||||
BorderThickness = SelectionHandler.BORDER_RADIUS,
|
||||
BorderThickness = SelectionBox.BORDER_RADIUS,
|
||||
Child = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
|
216
osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs
Normal file
216
osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs
Normal file
@ -0,0 +1,216 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
public class SelectionBox : CompositeDrawable
|
||||
{
|
||||
public Action<float> OnRotation;
|
||||
public Action<Vector2, Anchor> OnScale;
|
||||
public Action<Direction> OnFlip;
|
||||
|
||||
public Action OperationStarted;
|
||||
public Action OperationEnded;
|
||||
|
||||
private bool canRotate;
|
||||
|
||||
/// <summary>
|
||||
/// Whether rotation support should be enabled.
|
||||
/// </summary>
|
||||
public bool CanRotate
|
||||
{
|
||||
get => canRotate;
|
||||
set
|
||||
{
|
||||
if (canRotate == value) return;
|
||||
|
||||
canRotate = value;
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
|
||||
private bool canScaleX;
|
||||
|
||||
/// <summary>
|
||||
/// Whether vertical scale support should be enabled.
|
||||
/// </summary>
|
||||
public bool CanScaleX
|
||||
{
|
||||
get => canScaleX;
|
||||
set
|
||||
{
|
||||
if (canScaleX == value) return;
|
||||
|
||||
canScaleX = value;
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
|
||||
private bool canScaleY;
|
||||
|
||||
/// <summary>
|
||||
/// Whether horizontal scale support should be enabled.
|
||||
/// </summary>
|
||||
public bool CanScaleY
|
||||
{
|
||||
get => canScaleY;
|
||||
set
|
||||
{
|
||||
if (canScaleY == value) return;
|
||||
|
||||
canScaleY = value;
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
|
||||
private FillFlowContainer buttons;
|
||||
|
||||
public const float BORDER_RADIUS = 3;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
recreate();
|
||||
}
|
||||
|
||||
private void recreate()
|
||||
{
|
||||
if (LoadState < LoadState.Loading)
|
||||
return;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
Masking = true,
|
||||
BorderThickness = BORDER_RADIUS,
|
||||
BorderColour = colours.YellowDark,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
|
||||
AlwaysPresent = true,
|
||||
Alpha = 0
|
||||
},
|
||||
}
|
||||
},
|
||||
buttons = new FillFlowContainer
|
||||
{
|
||||
Y = 20,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.Centre
|
||||
}
|
||||
};
|
||||
|
||||
if (CanScaleX) addXScaleComponents();
|
||||
if (CanScaleX && CanScaleY) addFullScaleComponents();
|
||||
if (CanScaleY) addYScaleComponents();
|
||||
if (CanRotate) addRotationComponents();
|
||||
}
|
||||
|
||||
private void addRotationComponents()
|
||||
{
|
||||
const float separation = 40;
|
||||
|
||||
addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise", () => OnRotation?.Invoke(-90));
|
||||
addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise", () => OnRotation?.Invoke(90));
|
||||
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Depth = float.MaxValue,
|
||||
Colour = colours.YellowLight,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Alpha = 0.3f,
|
||||
Size = new Vector2(BORDER_RADIUS, separation),
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
},
|
||||
new SelectionBoxDragHandleButton(FontAwesome.Solid.Redo, "Free rotate")
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Y = -separation,
|
||||
HandleDrag = e => OnRotation?.Invoke(e.Delta.X),
|
||||
OperationStarted = operationStarted,
|
||||
OperationEnded = operationEnded
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void addYScaleComponents()
|
||||
{
|
||||
addButton(FontAwesome.Solid.ArrowsAltV, "Flip vertically", () => OnFlip?.Invoke(Direction.Vertical));
|
||||
|
||||
addDragHandle(Anchor.TopCentre);
|
||||
addDragHandle(Anchor.BottomCentre);
|
||||
}
|
||||
|
||||
private void addFullScaleComponents()
|
||||
{
|
||||
addDragHandle(Anchor.TopLeft);
|
||||
addDragHandle(Anchor.TopRight);
|
||||
addDragHandle(Anchor.BottomLeft);
|
||||
addDragHandle(Anchor.BottomRight);
|
||||
}
|
||||
|
||||
private void addXScaleComponents()
|
||||
{
|
||||
addButton(FontAwesome.Solid.ArrowsAltH, "Flip horizontally", () => OnFlip?.Invoke(Direction.Horizontal));
|
||||
|
||||
addDragHandle(Anchor.CentreLeft);
|
||||
addDragHandle(Anchor.CentreRight);
|
||||
}
|
||||
|
||||
private void addButton(IconUsage icon, string tooltip, Action action)
|
||||
{
|
||||
buttons.Add(new SelectionBoxDragHandleButton(icon, tooltip)
|
||||
{
|
||||
OperationStarted = operationStarted,
|
||||
OperationEnded = operationEnded,
|
||||
Action = action
|
||||
});
|
||||
}
|
||||
|
||||
private void addDragHandle(Anchor anchor) => AddInternal(new SelectionBoxDragHandle
|
||||
{
|
||||
Anchor = anchor,
|
||||
HandleDrag = e => OnScale?.Invoke(e.Delta, anchor),
|
||||
OperationStarted = operationStarted,
|
||||
OperationEnded = operationEnded
|
||||
});
|
||||
|
||||
private int activeOperations;
|
||||
|
||||
private void operationEnded()
|
||||
{
|
||||
if (--activeOperations == 0)
|
||||
OperationEnded?.Invoke();
|
||||
}
|
||||
|
||||
private void operationStarted()
|
||||
{
|
||||
if (activeOperations++ == 0)
|
||||
OperationStarted?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
public class SelectionBoxDragHandle : Container
|
||||
{
|
||||
public Action OperationStarted;
|
||||
public Action OperationEnded;
|
||||
|
||||
public Action<DragEvent> HandleDrag { get; set; }
|
||||
|
||||
private Circle circle;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Size = new Vector2(10);
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
circle = new Circle
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
UpdateHoverState();
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
UpdateHoverState();
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
base.OnHoverLost(e);
|
||||
UpdateHoverState();
|
||||
}
|
||||
|
||||
protected bool HandlingMouse;
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
HandlingMouse = true;
|
||||
UpdateHoverState();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
OperationStarted?.Invoke();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnDrag(DragEvent e)
|
||||
{
|
||||
HandleDrag?.Invoke(e);
|
||||
base.OnDrag(e);
|
||||
}
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e)
|
||||
{
|
||||
HandlingMouse = false;
|
||||
OperationEnded?.Invoke();
|
||||
|
||||
UpdateHoverState();
|
||||
base.OnDragEnd(e);
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
HandlingMouse = false;
|
||||
UpdateHoverState();
|
||||
base.OnMouseUp(e);
|
||||
}
|
||||
|
||||
protected virtual void UpdateHoverState()
|
||||
{
|
||||
circle.Colour = HandlingMouse ? colours.GrayF : (IsHovered ? colours.Red : colours.YellowDark);
|
||||
this.ScaleTo(HandlingMouse || IsHovered ? 1.5f : 1, 100, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// A drag "handle" which shares the visual appearance but behaves more like a clickable button.
|
||||
/// </summary>
|
||||
public sealed class SelectionBoxDragHandleButton : SelectionBoxDragHandle, IHasTooltip
|
||||
{
|
||||
private SpriteIcon icon;
|
||||
|
||||
private readonly IconUsage iconUsage;
|
||||
|
||||
public Action Action;
|
||||
|
||||
public SelectionBoxDragHandleButton(IconUsage iconUsage, string tooltip)
|
||||
{
|
||||
this.iconUsage = iconUsage;
|
||||
|
||||
TooltipText = tooltip;
|
||||
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Size *= 2;
|
||||
AddInternal(icon = new SpriteIcon
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = new Vector2(0.5f),
|
||||
Icon = iconUsage,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
OperationStarted?.Invoke();
|
||||
Action?.Invoke();
|
||||
OperationEnded?.Invoke();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void UpdateHoverState()
|
||||
{
|
||||
base.UpdateHoverState();
|
||||
icon.Colour = !HandlingMouse && IsHovered ? Color4.White : Color4.Black;
|
||||
}
|
||||
|
||||
public string TooltipText { get; }
|
||||
}
|
||||
}
|
@ -32,8 +32,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// </summary>
|
||||
public class SelectionHandler : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IHasContextMenu
|
||||
{
|
||||
public const float BORDER_RADIUS = 2;
|
||||
|
||||
public IEnumerable<SelectionBlueprint> SelectedBlueprints => selectedBlueprints;
|
||||
private readonly List<SelectionBlueprint> selectedBlueprints;
|
||||
|
||||
@ -45,6 +43,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
private OsuSpriteText selectionDetailsText;
|
||||
|
||||
protected SelectionBox SelectionBox { get; private set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
protected EditorBeatmap EditorBeatmap { get; private set; }
|
||||
|
||||
@ -69,19 +69,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
BorderThickness = BORDER_RADIUS,
|
||||
BorderColour = colours.YellowDark,
|
||||
Child = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
AlwaysPresent = true,
|
||||
Alpha = 0
|
||||
}
|
||||
},
|
||||
// todo: should maybe be inside the SelectionBox?
|
||||
new Container
|
||||
{
|
||||
Name = "info text",
|
||||
@ -100,11 +88,39 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
Font = OsuFont.Default.With(size: 11)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
SelectionBox = CreateSelectionBox(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public SelectionBox CreateSelectionBox()
|
||||
=> new SelectionBox
|
||||
{
|
||||
OperationStarted = OnOperationBegan,
|
||||
OperationEnded = OnOperationEnded,
|
||||
|
||||
OnRotation = angle => HandleRotation(angle),
|
||||
OnScale = (amount, anchor) => HandleScale(amount, anchor),
|
||||
OnFlip = direction => HandleFlip(direction),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a drag operation ends from the selection box.
|
||||
/// </summary>
|
||||
protected virtual void OnOperationBegan()
|
||||
{
|
||||
ChangeHandler.BeginChange();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a drag operation begins from the selection box.
|
||||
/// </summary>
|
||||
protected virtual void OnOperationEnded()
|
||||
{
|
||||
ChangeHandler.EndChange();
|
||||
}
|
||||
|
||||
#region User Input Handling
|
||||
|
||||
/// <summary>
|
||||
@ -119,7 +135,29 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
/// Whether any <see cref="DrawableHitObject"/>s could be moved.
|
||||
/// Returning true will also propagate StartTime changes provided by the closest <see cref="IPositionSnapProvider.SnapScreenSpacePositionToValidTime"/>.
|
||||
/// </returns>
|
||||
public virtual bool HandleMovement(MoveSelectionEvent moveEvent) => true;
|
||||
public virtual bool HandleMovement(MoveSelectionEvent moveEvent) => false;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the selected <see cref="DrawableHitObject"/>s being rotated.
|
||||
/// </summary>
|
||||
/// <param name="angle">The delta angle to apply to the selection.</param>
|
||||
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be moved.</returns>
|
||||
public virtual bool HandleRotation(float angle) => false;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the selected <see cref="DrawableHitObject"/>s being scaled.
|
||||
/// </summary>
|
||||
/// <param name="scale">The delta scale to apply, in playfield local coordinates.</param>
|
||||
/// <param name="anchor">The point of reference where the scale is originating from.</param>
|
||||
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be moved.</returns>
|
||||
public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false;
|
||||
|
||||
/// <summary>
|
||||
/// Handled the selected <see cref="DrawableHitObject"/>s being flipped.
|
||||
/// </summary>
|
||||
/// <param name="direction">The direction to flip</param>
|
||||
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be moved.</returns>
|
||||
public virtual bool HandleFlip(Direction direction) => false;
|
||||
|
||||
public bool OnPressed(PlatformAction action)
|
||||
{
|
||||
@ -222,11 +260,22 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
selectionDetailsText.Text = count > 0 ? count.ToString() : string.Empty;
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
Show();
|
||||
OnSelectionChanged();
|
||||
}
|
||||
else
|
||||
Hide();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggered whenever more than one object is selected, on each change.
|
||||
/// Should update the selection box's state to match supported operations.
|
||||
/// </summary>
|
||||
protected virtual void OnSelectionChanged()
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
@ -0,0 +1,67 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
public class DifficultyPointPiece : CompositeDrawable
|
||||
{
|
||||
private readonly DifficultyControlPoint difficultyPoint;
|
||||
|
||||
private OsuSpriteText speedMultiplierText;
|
||||
private readonly BindableNumber<double> speedMultiplier;
|
||||
|
||||
public DifficultyPointPiece(DifficultyControlPoint difficultyPoint)
|
||||
{
|
||||
this.difficultyPoint = difficultyPoint;
|
||||
speedMultiplier = difficultyPoint.SpeedMultiplierBindable.GetBoundCopy();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
AutoSizeAxes = Axes.X;
|
||||
|
||||
Color4 colour = difficultyPoint.GetRepresentingColour(colours);
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colour,
|
||||
Width = 2,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colour,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
speedMultiplierText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.Default.With(weight: FontWeight.Bold),
|
||||
Colour = Color4.White,
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
speedMultiplier.BindValueChanged(multiplier => speedMultiplierText.Text = $"{multiplier.NewValue:n2}x", true);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
public class SamplePointPiece : CompositeDrawable
|
||||
{
|
||||
private readonly SampleControlPoint samplePoint;
|
||||
|
||||
private readonly Bindable<string> bank;
|
||||
private readonly BindableNumber<int> volume;
|
||||
|
||||
private OsuSpriteText text;
|
||||
private Box volumeBox;
|
||||
|
||||
public SamplePointPiece(SampleControlPoint samplePoint)
|
||||
{
|
||||
this.samplePoint = samplePoint;
|
||||
volume = samplePoint.SampleVolumeBindable.GetBoundCopy();
|
||||
bank = samplePoint.SampleBankBindable.GetBoundCopy();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Origin = Anchor.TopLeft;
|
||||
Anchor = Anchor.TopLeft;
|
||||
|
||||
AutoSizeAxes = Axes.X;
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
|
||||
Color4 colour = samplePoint.GetRepresentingColour(colours);
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = 20,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
volumeBox = new Box
|
||||
{
|
||||
X = 2,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Colour = ColourInfo.GradientVertical(colour, Color4.Black),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new Box
|
||||
{
|
||||
Colour = colour.Lighten(0.2f),
|
||||
Width = 2,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
},
|
||||
}
|
||||
},
|
||||
text = new OsuSpriteText
|
||||
{
|
||||
X = 2,
|
||||
Y = -5,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Alpha = 0.9f,
|
||||
Rotation = -90,
|
||||
Font = OsuFont.Default.With(weight: FontWeight.SemiBold)
|
||||
}
|
||||
};
|
||||
|
||||
volume.BindValueChanged(volume => volumeBox.Height = volume.NewValue / 100f, true);
|
||||
bank.BindValueChanged(bank => text.Text = bank.NewValue, true);
|
||||
}
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Audio;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
@ -21,6 +22,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
public class Timeline : ZoomableScrollContainer, IPositionSnapProvider
|
||||
{
|
||||
public readonly Bindable<bool> WaveformVisible = new Bindable<bool>();
|
||||
|
||||
public readonly Bindable<bool> ControlPointsVisible = new Bindable<bool>();
|
||||
|
||||
public readonly Bindable<bool> TicksVisible = new Bindable<bool>();
|
||||
|
||||
public readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>();
|
||||
|
||||
[Resolved]
|
||||
@ -57,23 +63,41 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
|
||||
private WaveformGraph waveform;
|
||||
|
||||
private TimelineTickDisplay ticks;
|
||||
|
||||
private TimelineControlPointDisplay controlPoints;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours)
|
||||
{
|
||||
Add(waveform = new WaveformGraph
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colours.Blue.Opacity(0.2f),
|
||||
LowColour = colours.BlueLighter,
|
||||
MidColour = colours.BlueDark,
|
||||
HighColour = colours.BlueDarker,
|
||||
Depth = float.MaxValue
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = float.MaxValue,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
waveform = new WaveformGraph
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colours.Blue.Opacity(0.2f),
|
||||
LowColour = colours.BlueLighter,
|
||||
MidColour = colours.BlueDark,
|
||||
HighColour = colours.BlueDarker,
|
||||
},
|
||||
ticks = new TimelineTickDisplay(),
|
||||
controlPoints = new TimelineControlPointDisplay(),
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// We don't want the centre marker to scroll
|
||||
AddInternal(new CentreMarker { Depth = float.MaxValue });
|
||||
|
||||
WaveformVisible.ValueChanged += visible => waveform.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint);
|
||||
ControlPointsVisible.ValueChanged += visible => controlPoints.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint);
|
||||
TicksVisible.ValueChanged += visible => ticks.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint);
|
||||
|
||||
Beatmap.BindTo(beatmap);
|
||||
Beatmap.BindValueChanged(b =>
|
||||
|
@ -25,6 +25,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
CornerRadius = 5;
|
||||
|
||||
OsuCheckbox waveformCheckbox;
|
||||
OsuCheckbox controlPointsCheckbox;
|
||||
OsuCheckbox ticksCheckbox;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
@ -57,12 +59,26 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
Origin = Anchor.CentreLeft,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Width = 160,
|
||||
Padding = new MarginPadding { Horizontal = 15 },
|
||||
Padding = new MarginPadding { Horizontal = 10 },
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0, 4),
|
||||
Children = new[]
|
||||
{
|
||||
waveformCheckbox = new OsuCheckbox { LabelText = "Waveform" }
|
||||
waveformCheckbox = new OsuCheckbox
|
||||
{
|
||||
LabelText = "Waveform",
|
||||
Current = { Value = true },
|
||||
},
|
||||
controlPointsCheckbox = new OsuCheckbox
|
||||
{
|
||||
LabelText = "Control Points",
|
||||
Current = { Value = true },
|
||||
},
|
||||
ticksCheckbox = new OsuCheckbox
|
||||
{
|
||||
LabelText = "Ticks",
|
||||
Current = { Value = true },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -119,9 +135,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
}
|
||||
};
|
||||
|
||||
waveformCheckbox.Current.Value = true;
|
||||
|
||||
Timeline.WaveformVisible.BindTo(waveformCheckbox.Current);
|
||||
Timeline.ControlPointsVisible.BindTo(controlPointsCheckbox.Current);
|
||||
Timeline.TicksVisible.BindTo(ticksCheckbox.Current);
|
||||
}
|
||||
|
||||
private void changeZoom(float change) => Timeline.Zoom += change;
|
||||
|
@ -0,0 +1,57 @@
|
||||
// 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.Specialized;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
/// <summary>
|
||||
/// The part of the timeline that displays the control points.
|
||||
/// </summary>
|
||||
public class TimelineControlPointDisplay : TimelinePart<TimelineControlPointGroup>
|
||||
{
|
||||
private IBindableList<ControlPointGroup> controlPointGroups;
|
||||
|
||||
public TimelineControlPointDisplay()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
protected override void LoadBeatmap(WorkingBeatmap beatmap)
|
||||
{
|
||||
base.LoadBeatmap(beatmap);
|
||||
|
||||
controlPointGroups = beatmap.Beatmap.ControlPointInfo.Groups.GetBoundCopy();
|
||||
controlPointGroups.BindCollectionChanged((sender, args) =>
|
||||
{
|
||||
switch (args.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
Clear();
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
foreach (var group in args.NewItems.OfType<ControlPointGroup>())
|
||||
Add(new TimelineControlPointGroup(group));
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
foreach (var group in args.OldItems.OfType<ControlPointGroup>())
|
||||
{
|
||||
var matching = Children.SingleOrDefault(gv => gv.Group == group);
|
||||
|
||||
matching?.Expire();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
public class TimelineControlPointGroup : CompositeDrawable
|
||||
{
|
||||
public readonly ControlPointGroup Group;
|
||||
|
||||
private BindableList<ControlPoint> controlPoints;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
public TimelineControlPointGroup(ControlPointGroup group)
|
||||
{
|
||||
Group = group;
|
||||
|
||||
RelativePositionAxes = Axes.X;
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
AutoSizeAxes = Axes.X;
|
||||
|
||||
X = (float)group.Time;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
controlPoints = (BindableList<ControlPoint>)Group.ControlPoints.GetBoundCopy();
|
||||
controlPoints.BindCollectionChanged((_, __) =>
|
||||
{
|
||||
ClearInternal();
|
||||
|
||||
foreach (var point in controlPoints)
|
||||
{
|
||||
switch (point)
|
||||
{
|
||||
case DifficultyControlPoint difficultyPoint:
|
||||
AddInternal(new DifficultyPointPiece(difficultyPoint) { Depth = -2 });
|
||||
break;
|
||||
|
||||
case TimingControlPoint timingPoint:
|
||||
AddInternal(new TimingPointPiece(timingPoint));
|
||||
break;
|
||||
|
||||
case SampleControlPoint samplePoint:
|
||||
AddInternal(new SamplePointPiece(samplePoint) { Depth = -1 });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
public class TimingPointPiece : CompositeDrawable
|
||||
{
|
||||
private readonly TimingControlPoint point;
|
||||
|
||||
private readonly BindableNumber<double> beatLength;
|
||||
private OsuSpriteText bpmText;
|
||||
|
||||
public TimingPointPiece(TimingControlPoint point)
|
||||
{
|
||||
this.point = point;
|
||||
beatLength = point.BeatLengthBindable.GetBoundCopy();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Origin = Anchor.CentreLeft;
|
||||
Anchor = Anchor.CentreLeft;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
Color4 colour = point.GetRepresentingColour(colours);
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Alpha = 0.9f,
|
||||
Colour = ColourInfo.GradientHorizontal(colour, colour.Opacity(0.5f)),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
bpmText = new OsuSpriteText
|
||||
{
|
||||
Alpha = 0.9f,
|
||||
Padding = new MarginPadding(3),
|
||||
Font = OsuFont.Default.With(size: 40)
|
||||
}
|
||||
};
|
||||
|
||||
beatLength.BindValueChanged(beatLength =>
|
||||
{
|
||||
bpmText.Text = $"{60000 / beatLength.NewValue:n1} BPM";
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
}
|
@ -84,6 +84,8 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
private DependencyContainer dependencies;
|
||||
|
||||
private bool isNewBeatmap;
|
||||
|
||||
protected override UserActivity InitialActivity => new UserActivity.Editing(Beatmap.Value.BeatmapInfo);
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
@ -113,8 +115,6 @@ namespace osu.Game.Screens.Edit
|
||||
// todo: remove caching of this and consume via editorBeatmap?
|
||||
dependencies.Cache(beatDivisor);
|
||||
|
||||
bool isNewBeatmap = false;
|
||||
|
||||
if (Beatmap.Value is DummyWorkingBeatmap)
|
||||
{
|
||||
isNewBeatmap = true;
|
||||
@ -287,6 +287,9 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
protected void Save()
|
||||
{
|
||||
// no longer new after first user-triggered save.
|
||||
isNewBeatmap = false;
|
||||
|
||||
// apply any set-level metadata changes.
|
||||
beatmapManager.Update(playableBeatmap.BeatmapInfo.BeatmapSet);
|
||||
|
||||
@ -435,10 +438,20 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
public override bool OnExiting(IScreen next)
|
||||
{
|
||||
if (!exitConfirmed && dialogOverlay != null && HasUnsavedChanges && !(dialogOverlay.CurrentDialog is PromptForSaveDialog))
|
||||
if (!exitConfirmed)
|
||||
{
|
||||
dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave));
|
||||
return true;
|
||||
// if the confirm dialog is already showing (or we can't show it, ie. in tests) exit without save.
|
||||
if (dialogOverlay == null || dialogOverlay.CurrentDialog is PromptForSaveDialog)
|
||||
{
|
||||
confirmExit();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isNewBeatmap || HasUnsavedChanges)
|
||||
{
|
||||
dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Background.FadeColour(Color4.White, 500);
|
||||
@ -456,6 +469,12 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
private void confirmExit()
|
||||
{
|
||||
if (isNewBeatmap)
|
||||
{
|
||||
// confirming exit without save means we should delete the new beatmap completely.
|
||||
beatmapManager.Delete(playableBeatmap.BeatmapInfo.BeatmapSet);
|
||||
}
|
||||
|
||||
exitConfirmed = true;
|
||||
this.Exit();
|
||||
}
|
||||
|
@ -81,9 +81,6 @@ namespace osu.Game.Screens.Edit
|
||||
SaveState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the current <see cref="Editor"/> state.
|
||||
/// </summary>
|
||||
public void SaveState()
|
||||
{
|
||||
if (bulkChangesStarted > 0)
|
||||
|
@ -44,10 +44,5 @@ namespace osu.Game.Screens.Edit
|
||||
.Then()
|
||||
.FadeTo(1f, 250, Easing.OutQuint);
|
||||
}
|
||||
|
||||
public void Exit()
|
||||
{
|
||||
Expire();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,7 +112,6 @@ namespace osu.Game.Screens.Edit
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new[]
|
||||
{
|
||||
new TimelineTickDisplay(),
|
||||
CreateTimelineContent(),
|
||||
}
|
||||
}, t =>
|
||||
|
@ -35,5 +35,11 @@ namespace osu.Game.Screens.Edit
|
||||
/// This should be invoked as soon as possible after <see cref="BeginChange"/> to cause a state change.
|
||||
/// </remarks>
|
||||
void EndChange();
|
||||
|
||||
/// <summary>
|
||||
/// Immediately saves the current <see cref="Editor"/> state.
|
||||
/// Note that this will be a no-op if there is a change in progress via <see cref="BeginChange"/>.
|
||||
/// </summary>
|
||||
void SaveState();
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,10 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -13,6 +15,7 @@ using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
@ -23,14 +26,24 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Setup
|
||||
{
|
||||
public class SetupScreen : EditorScreen
|
||||
public class SetupScreen : EditorScreen, ICanAcceptFiles
|
||||
{
|
||||
public IEnumerable<string> HandledExtensions => ImageExtensions.Concat(AudioExtensions);
|
||||
|
||||
public static string[] ImageExtensions { get; } = { ".jpg", ".jpeg", ".png" };
|
||||
|
||||
public static string[] AudioExtensions { get; } = { ".mp3", ".ogg" };
|
||||
|
||||
private FillFlowContainer flow;
|
||||
private LabelledTextBox artistTextBox;
|
||||
private LabelledTextBox titleTextBox;
|
||||
private LabelledTextBox creatorTextBox;
|
||||
private LabelledTextBox difficultyTextBox;
|
||||
private LabelledTextBox audioTrackTextBox;
|
||||
private Container backgroundSpriteContainer;
|
||||
|
||||
[Resolved]
|
||||
private OsuGameBase game { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private MusicController music { get; set; }
|
||||
@ -83,19 +96,12 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
backgroundSpriteContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 250,
|
||||
Masking = true,
|
||||
CornerRadius = 10,
|
||||
Child = new BeatmapBackgroundSprite(Beatmap.Value)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
FillMode = FillMode.Fill,
|
||||
},
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
@ -144,12 +150,81 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
}
|
||||
};
|
||||
|
||||
updateBackgroundSprite();
|
||||
|
||||
audioTrackTextBox.Current.BindValueChanged(audioTrackChanged);
|
||||
|
||||
foreach (var item in flow.OfType<LabelledTextBox>())
|
||||
item.OnCommit += onCommit;
|
||||
}
|
||||
|
||||
Task ICanAcceptFiles.Import(params string[] paths)
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
var firstFile = new FileInfo(paths.First());
|
||||
|
||||
if (ImageExtensions.Contains(firstFile.Extension))
|
||||
{
|
||||
ChangeBackgroundImage(firstFile.FullName);
|
||||
}
|
||||
else if (AudioExtensions.Contains(firstFile.Extension))
|
||||
{
|
||||
audioTrackTextBox.Text = firstFile.FullName;
|
||||
}
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void updateBackgroundSprite()
|
||||
{
|
||||
LoadComponentAsync(new BeatmapBackgroundSprite(Beatmap.Value)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
FillMode = FillMode.Fill,
|
||||
}, background =>
|
||||
{
|
||||
backgroundSpriteContainer.Child = background;
|
||||
background.FadeInFromZero(500);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
game.RegisterImportHandler(this);
|
||||
}
|
||||
|
||||
public bool ChangeBackgroundImage(string path)
|
||||
{
|
||||
var info = new FileInfo(path);
|
||||
|
||||
if (!info.Exists)
|
||||
return false;
|
||||
|
||||
var set = Beatmap.Value.BeatmapSetInfo;
|
||||
|
||||
// remove the previous background for now.
|
||||
// in the future we probably want to check if this is being used elsewhere (other difficulties?)
|
||||
var oldFile = set.Files.FirstOrDefault(f => f.Filename == Beatmap.Value.Metadata.BackgroundFile);
|
||||
|
||||
using (var stream = info.OpenRead())
|
||||
{
|
||||
if (oldFile != null)
|
||||
beatmaps.ReplaceFile(set, oldFile, stream, info.Name);
|
||||
else
|
||||
beatmaps.AddFile(set, stream, info.Name);
|
||||
}
|
||||
|
||||
Beatmap.Value.Metadata.BackgroundFile = info.Name;
|
||||
updateBackgroundSprite();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ChangeAudioTrack(string path)
|
||||
{
|
||||
var info = new FileInfo(path);
|
||||
@ -196,6 +271,12 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
Beatmap.Value.Metadata.AuthorString = creatorTextBox.Current.Value;
|
||||
Beatmap.Value.BeatmapInfo.Version = difficultyTextBox.Current.Value;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
game?.UnregisterImportHandler(this);
|
||||
}
|
||||
}
|
||||
|
||||
internal class FileChooserLabelledTextBox : LabelledTextBox
|
||||
@ -230,7 +311,7 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
|
||||
public void DisplayFileChooser()
|
||||
{
|
||||
Target.Child = new FileSelector(validFileExtensions: new[] { ".mp3", ".ogg" })
|
||||
Target.Child = new FileSelector(validFileExtensions: SetupScreen.AudioExtensions)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 400,
|
||||
|
@ -114,7 +114,14 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
|
||||
controlPoints = group.ControlPoints.GetBoundCopy();
|
||||
controlPoints.CollectionChanged += (_, __) => createChildren();
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
createChildren();
|
||||
}
|
||||
|
||||
@ -125,20 +132,22 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
|
||||
private Drawable createAttribute(ControlPoint controlPoint)
|
||||
{
|
||||
Color4 colour = controlPoint.GetRepresentingColour(colours);
|
||||
|
||||
switch (controlPoint)
|
||||
{
|
||||
case TimingControlPoint timing:
|
||||
return new RowAttribute("timing", () => $"{60000 / timing.BeatLength:n1}bpm {timing.TimeSignature}");
|
||||
return new RowAttribute("timing", () => $"{60000 / timing.BeatLength:n1}bpm {timing.TimeSignature}", colour);
|
||||
|
||||
case DifficultyControlPoint difficulty:
|
||||
|
||||
return new RowAttribute("difficulty", () => $"{difficulty.SpeedMultiplier:n2}x");
|
||||
return new RowAttribute("difficulty", () => $"{difficulty.SpeedMultiplier:n2}x", colour);
|
||||
|
||||
case EffectControlPoint effect:
|
||||
return new RowAttribute("effect", () => $"{(effect.KiaiMode ? "Kiai " : "")}{(effect.OmitFirstBarLine ? "NoBarLine " : "")}");
|
||||
return new RowAttribute("effect", () => $"{(effect.KiaiMode ? "Kiai " : "")}{(effect.OmitFirstBarLine ? "NoBarLine " : "")}", colour);
|
||||
|
||||
case SampleControlPoint sample:
|
||||
return new RowAttribute("sample", () => $"{sample.SampleBank} {sample.SampleVolume}%");
|
||||
return new RowAttribute("sample", () => $"{sample.SampleBank} {sample.SampleVolume}%", colour);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -28,6 +28,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
if (point.NewValue != null)
|
||||
{
|
||||
multiplierSlider.Current = point.NewValue.SpeedMultiplierBindable;
|
||||
multiplierSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,10 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
if (point.NewValue != null)
|
||||
{
|
||||
kiai.Current = point.NewValue.KiaiModeBindable;
|
||||
kiai.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
||||
|
||||
omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable;
|
||||
omitBarLine.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
@ -16,11 +17,13 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
private readonly string header;
|
||||
private readonly Func<string> content;
|
||||
private readonly Color4 colour;
|
||||
|
||||
public RowAttribute(string header, Func<string> content)
|
||||
public RowAttribute(string header, Func<string> content, Color4 colour)
|
||||
{
|
||||
this.header = header;
|
||||
this.content = content;
|
||||
this.colour = colour;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -40,7 +43,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colours.Yellow,
|
||||
Colour = colour,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new OsuSpriteText
|
||||
@ -50,7 +53,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
Origin = Anchor.Centre,
|
||||
Font = OsuFont.Default.With(weight: FontWeight.SemiBold, size: 12),
|
||||
Text = header,
|
||||
Colour = colours.Gray3
|
||||
Colour = colours.Gray0
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -35,7 +35,10 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
if (point.NewValue != null)
|
||||
{
|
||||
bank.Current = point.NewValue.SampleBankBindable;
|
||||
bank.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
||||
|
||||
volume.Current = point.NewValue.SampleVolumeBindable;
|
||||
volume.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,9 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
[Resolved]
|
||||
protected Bindable<ControlPointGroup> SelectedGroup { get; private set; }
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
protected IEditorChangeHandler ChangeHandler { get; private set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
|
@ -38,6 +38,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
},
|
||||
slider = new SettingsSlider<T>
|
||||
{
|
||||
TransferValueOnCommit = true,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
|
||||
using osu.Game.Screens.Edit.Compose.Components.Timeline;
|
||||
using osuTK;
|
||||
|
||||
@ -31,11 +30,6 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
}
|
||||
|
||||
protected override Drawable CreateTimelineContent() => new ControlPointPart
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
|
||||
protected override Drawable CreateMainContent() => new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@ -87,6 +81,9 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
[Resolved]
|
||||
private Bindable<ControlPointGroup> selectedGroup { get; set; }
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private IEditorChangeHandler changeHandler { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
@ -146,6 +143,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
controlGroups.BindCollectionChanged((sender, args) =>
|
||||
{
|
||||
table.ControlGroups = controlGroups;
|
||||
changeHandler.SaveState();
|
||||
}, true);
|
||||
}
|
||||
|
||||
|
@ -37,8 +37,13 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
if (point.NewValue != null)
|
||||
{
|
||||
bpmSlider.Bindable = point.NewValue.BeatLengthBindable;
|
||||
bpmSlider.Bindable.BindValueChanged(_ => ChangeHandler?.SaveState());
|
||||
|
||||
bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable;
|
||||
// no need to hook change handler here as it's the same bindable as above
|
||||
|
||||
timeSignature.Bindable = point.NewValue.TimeSignatureBindable;
|
||||
timeSignature.Bindable.BindValueChanged(_ => ChangeHandler?.SaveState());
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,6 +122,8 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
bpmBindable.BindValueChanged(bpm => beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue));
|
||||
|
||||
base.Bindable = bpmBindable;
|
||||
|
||||
TransferValueOnCommit = true;
|
||||
}
|
||||
|
||||
public override Bindable<double> Bindable
|
||||
|
@ -42,8 +42,11 @@ namespace osu.Game.Screens.Menu
|
||||
ValidForResume = false;
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours, IAPIProvider api)
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
@ -104,7 +107,9 @@ namespace osu.Game.Screens.Menu
|
||||
|
||||
iconColour = colours.Yellow;
|
||||
|
||||
currentUser.BindTo(api.LocalUser);
|
||||
// manually transfer the user once, but only do the final bind in LoadComplete to avoid thread woes (API scheduler could run while this screen is still loading).
|
||||
// the manual transfer is here to ensure all text content is loaded ahead of time as this is very early in the game load process and we want to avoid stutters.
|
||||
currentUser.Value = api.LocalUser.Value;
|
||||
currentUser.BindValueChanged(e =>
|
||||
{
|
||||
supportFlow.Children.ForEach(d => d.FadeOut().Expire());
|
||||
@ -141,6 +146,8 @@ namespace osu.Game.Screens.Menu
|
||||
base.LoadComplete();
|
||||
if (nextScreen != null)
|
||||
LoadComponentAsync(nextScreen);
|
||||
|
||||
currentUser.BindTo(api.LocalUser);
|
||||
}
|
||||
|
||||
public override void OnEntering(IScreen last)
|
||||
|
@ -60,7 +60,7 @@ namespace osu.Game.Screens.Multi.Components
|
||||
if (item?.Beatmap != null)
|
||||
{
|
||||
drawableRuleset.FadeIn(transition_duration);
|
||||
drawableRuleset.Child = new DifficultyIcon(item.Beatmap.Value, item.Ruleset.Value) { Size = new Vector2(height) };
|
||||
drawableRuleset.Child = new DifficultyIcon(item.Beatmap.Value, item.Ruleset.Value, item.RequiredMods) { Size = new Vector2(height) };
|
||||
}
|
||||
else
|
||||
drawableRuleset.FadeOut(transition_duration);
|
||||
|
@ -103,7 +103,7 @@ namespace osu.Game.Screens.Multi
|
||||
|
||||
private void refresh()
|
||||
{
|
||||
difficultyIconContainer.Child = new DifficultyIcon(beatmap.Value, ruleset.Value) { Size = new Vector2(32) };
|
||||
difficultyIconContainer.Child = new DifficultyIcon(beatmap.Value, ruleset.Value, requiredMods) { Size = new Vector2(32) };
|
||||
|
||||
beatmapText.Clear();
|
||||
beatmapText.AddLink(Item.Beatmap.ToString(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineBeatmapID.ToString());
|
||||
|
@ -28,6 +28,8 @@ namespace osu.Game.Screens.Play
|
||||
/// </summary>
|
||||
public virtual IEnumerable<Bindable<double>> NonGameplayAdjustments => Enumerable.Empty<Bindable<double>>();
|
||||
|
||||
private readonly Bindable<bool> samplePlaybackDisabled = new Bindable<bool>();
|
||||
|
||||
public GameplayClock(IFrameBasedClock underlyingClock)
|
||||
{
|
||||
this.underlyingClock = underlyingClock;
|
||||
@ -62,13 +64,15 @@ namespace osu.Game.Screens.Play
|
||||
public bool IsRunning => underlyingClock.IsRunning;
|
||||
|
||||
/// <summary>
|
||||
/// Whether an ongoing seek operation is active.
|
||||
/// Whether nested samples supporting the <see cref="ISamplePlaybackDisabler"/> interface should be paused.
|
||||
/// </summary>
|
||||
public virtual bool IsSeeking => false;
|
||||
protected virtual bool ShouldDisableSamplePlayback => IsPaused.Value;
|
||||
|
||||
public void ProcessFrame()
|
||||
{
|
||||
// we do not want to process the underlying clock.
|
||||
// intentionally not updating the underlying clock (handled externally).
|
||||
|
||||
samplePlaybackDisabled.Value = ShouldDisableSamplePlayback;
|
||||
}
|
||||
|
||||
public double ElapsedFrameTime => underlyingClock.ElapsedFrameTime;
|
||||
@ -79,6 +83,6 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public IClock Source => underlyingClock;
|
||||
|
||||
public IBindable<bool> SamplePlaybackDisabled => IsPaused;
|
||||
IBindable<bool> ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled;
|
||||
}
|
||||
}
|
||||
|
@ -34,21 +34,21 @@ namespace osu.Game.Skinning
|
||||
samplePlaybackDisabled.BindTo(samplePlaybackDisabler.SamplePlaybackDisabled);
|
||||
samplePlaybackDisabled.BindValueChanged(disabled =>
|
||||
{
|
||||
if (RequestedPlaying)
|
||||
if (!RequestedPlaying) return;
|
||||
|
||||
// let non-looping samples that have already been started play out to completion (sounds better than abruptly cutting off).
|
||||
if (!Looping) return;
|
||||
|
||||
if (disabled.NewValue)
|
||||
base.Stop();
|
||||
else
|
||||
{
|
||||
if (disabled.NewValue)
|
||||
base.Stop();
|
||||
// it's not easy to know if a sample has finished playing (to end).
|
||||
// to keep things simple only resume playing looping samples.
|
||||
else if (Looping)
|
||||
// schedule so we don't start playing a sample which is no longer alive.
|
||||
Schedule(() =>
|
||||
{
|
||||
// schedule so we don't start playing a sample which is no longer alive.
|
||||
Schedule(() =>
|
||||
{
|
||||
if (RequestedPlaying)
|
||||
base.Play();
|
||||
});
|
||||
}
|
||||
if (RequestedPlaying)
|
||||
base.Play();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ namespace osu.Game.Skinning
|
||||
public readonly Bindable<Skin> CurrentSkin = new Bindable<Skin>(new DefaultSkin());
|
||||
public readonly Bindable<SkinInfo> CurrentSkinInfo = new Bindable<SkinInfo>(SkinInfo.Default) { Default = SkinInfo.Default };
|
||||
|
||||
public override string[] HandledExtensions => new[] { ".osk" };
|
||||
public override IEnumerable<string> HandledExtensions => new[] { ".osk" };
|
||||
|
||||
protected override string[] HashableFileTypes => new[] { ".ini" };
|
||||
|
||||
|
@ -79,6 +79,11 @@ namespace osu.Game.Updater
|
||||
bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".AppImage"));
|
||||
break;
|
||||
|
||||
case RuntimeInfo.Platform.iOS:
|
||||
// iOS releases are available via testflight. this link seems to work well enough for now.
|
||||
// see https://stackoverflow.com/a/32960501
|
||||
return "itms-beta://beta.itunes.apple.com/v1/app/1447765923";
|
||||
|
||||
case RuntimeInfo.Platform.Android:
|
||||
// on our testing device this causes the download to magically disappear.
|
||||
//bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".apk"));
|
||||
|
@ -11,5 +11,7 @@ namespace osu.iOS
|
||||
public class OsuGameIOS : OsuGame
|
||||
{
|
||||
public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString());
|
||||
|
||||
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user