1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 03:27:24 +08:00

Merge branch 'master' into dont-retry-on-replay-results

This commit is contained in:
Dean Herbert 2020-03-30 18:56:00 +09:00
commit ec18fe1f9f
23 changed files with 453 additions and 32 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@ -0,0 +1,70 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public class TestSceneSpinnerSpunOut : OsuTestScene
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(SpinnerDisc),
typeof(DrawableSpinner),
typeof(DrawableOsuHitObject),
typeof(OsuModSpunOut)
};
[SetUp]
public void SetUp() => Schedule(() =>
{
SelectedMods.Value = new[] { new OsuModSpunOut() };
});
[Test]
public void TestSpunOut()
{
DrawableSpinner spinner = null;
AddStep("create spinner", () => spinner = createSpinner());
AddUntilStep("wait for end", () => Time.Current > spinner.LifetimeEnd);
AddAssert("spinner is completed", () => spinner.Progress >= 1);
}
private DrawableSpinner createSpinner()
{
var spinner = new Spinner
{
StartTime = Time.Current + 500,
EndTime = Time.Current + 2500
};
spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
var drawableSpinner = new DrawableSpinner(spinner)
{
Anchor = Anchor.Centre
};
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())
mod.ApplyToDrawableHitObjects(new[] { drawableSpinner });
Add(drawableSpinner);
return drawableSpinner;
}
}
}

View File

@ -2,21 +2,46 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModSpunOut : Mod
public class OsuModSpunOut : Mod, IApplicableToDrawableHitObjects
{
public override string Name => "Spun Out";
public override string Acronym => "SO";
public override IconUsage? Icon => OsuIcon.ModSpunout;
public override ModType Type => ModType.DifficultyReduction;
public override ModType Type => ModType.Automation;
public override string Description => @"Spinners will be automatically completed.";
public override double ScoreMultiplier => 0.9;
public override bool Ranked => true;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot) };
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
{
foreach (var hitObject in drawables)
{
if (hitObject is DrawableSpinner spinner)
{
spinner.HandleUserInput = false;
spinner.OnUpdate += onSpinnerUpdate;
}
}
}
private void onSpinnerUpdate(Drawable drawable)
{
var spinner = (DrawableSpinner)drawable;
spinner.Disc.Tracking = true;
spinner.Disc.Rotate(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f));
}
}
}

View File

@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public readonly SkinnableDrawable CirclePiece;
private readonly Container scaleContainer;
protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle;
public DrawableHitCircle(HitCircle h)
: base(h)
{
@ -57,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return true;
},
},
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.HitCircle), _ => new MainCirclePiece()),
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(CirclePieceComponent), _ => new MainCirclePiece()),
ApproachCircle = new ApproachCircle
{
Alpha = 0,

View File

@ -14,6 +14,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>();
private readonly IBindable<int> pathVersion = new Bindable<int>();
protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle;
private readonly Slider slider;
public DrawableSliderHead(Slider slider, HitCircle h)

View File

@ -87,6 +87,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public void UpdateSnakingPosition(Vector2 start, Vector2 end)
{
// When the repeat is hit, the arrow should fade out on spot rather than following the slider
if (IsHit) return;
bool isRepeatAtEnd = sliderRepeat.RepeatIndex % 2 == 0;
List<Vector2> curve = ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve;

View File

@ -176,17 +176,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override void Update()
{
Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false;
if (!SpmCounter.IsPresent && Disc.Tracking)
SpmCounter.FadeIn(HitObject.TimeFadeIn);
base.Update();
if (HandleUserInput)
Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false;
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
if (!SpmCounter.IsPresent && Disc.Tracking)
SpmCounter.FadeIn(HitObject.TimeFadeIn);
circle.Rotation = Disc.Rotation;
Ticks.Rotation = Disc.Rotation;
SpmCounter.SetRotation(Disc.RotationAbsolute);

View File

@ -73,6 +73,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}
}
/// <summary>
/// Whether currently in the correct time range to allow spinning.
/// </summary>
private bool isSpinnableTime => spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current;
protected override bool OnMouseMove(MouseMoveEvent e)
{
mousePosition = Parent.ToLocalSpace(e.ScreenSpaceMousePosition);
@ -93,27 +98,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
protected override void Update()
{
base.Update();
var thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2));
bool validAndTracking = tracking && spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current;
var delta = thisAngle - lastAngle;
if (validAndTracking)
{
if (!rotationTransferred)
{
currentRotation = Rotation * 2;
rotationTransferred = true;
}
if (thisAngle - lastAngle > 180)
lastAngle += 360;
else if (lastAngle - thisAngle > 180)
lastAngle -= 360;
currentRotation += thisAngle - lastAngle;
RotationAbsolute += Math.Abs(thisAngle - lastAngle) * Math.Sign(Clock.ElapsedFrameTime);
}
if (tracking)
Rotate(delta);
lastAngle = thisAngle;
@ -128,5 +118,38 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Rotation = (float)Interpolation.Lerp(Rotation, currentRotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1));
}
/// <summary>
/// Rotate the disc by the provided angle (in addition to any existing rotation).
/// </summary>
/// <remarks>
/// Will be a no-op if not a valid time to spin.
/// </remarks>
/// <param name="angle">The delta angle.</param>
public void Rotate(float angle)
{
if (!isSpinnableTime)
return;
if (!rotationTransferred)
{
currentRotation = Rotation * 2;
rotationTransferred = true;
}
if (angle > 180)
{
lastAngle += 360;
angle -= 360;
}
else if (-angle > 180)
{
lastAngle -= 360;
angle += 360;
}
currentRotation += angle;
RotationAbsolute += Math.Abs(angle) * Math.Sign(Clock.ElapsedFrameTime);
}
}
}

View File

@ -113,7 +113,6 @@ namespace osu.Game.Rulesets.Osu
new OsuModEasy(),
new OsuModNoFail(),
new MultiMod(new OsuModHalfTime(), new OsuModDaycore()),
new OsuModSpunOut(),
};
case ModType.DifficultyIncrease:
@ -139,6 +138,7 @@ namespace osu.Game.Rulesets.Osu
new MultiMod(new OsuModAutoplay(), new OsuModCinema()),
new OsuModRelax(),
new OsuModAutopilot(),
new OsuModSpunOut(),
};
case ModType.Fun:

View File

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

View File

@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
@ -18,8 +19,12 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
public class LegacyMainCirclePiece : CompositeDrawable
{
public LegacyMainCirclePiece()
private readonly string priorityLookup;
public LegacyMainCirclePiece(string priorityLookup = null)
{
this.priorityLookup = priorityLookup;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
}
@ -39,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
hitCircleSprite = new Sprite
{
Texture = skin.GetTexture("hitcircle"),
Texture = getTextureWithFallback(string.Empty),
Colour = drawableObject.AccentColour.Value,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -51,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
}, confineMode: ConfineMode.NoScaling),
new Sprite
{
Texture = skin.GetTexture("hitcircleoverlay"),
Texture = getTextureWithFallback("overlay"),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
@ -65,6 +70,16 @@ namespace osu.Game.Rulesets.Osu.Skinning
indexInCurrentCombo.BindTo(osuObject.IndexInCurrentComboBindable);
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
Texture getTextureWithFallback(string name)
{
Texture tex = null;
if (!string.IsNullOrEmpty(priorityLookup))
tex = skin.GetTexture($"{priorityLookup}{name}");
return tex ?? skin.GetTexture($"hitcircle{name}");
}
}
private void updateState(ValueChangedEvent<ArmedState> state)

View File

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

View File

@ -0,0 +1,9 @@
[Mania]
Keys: 4
ColumnWidth: 10,10,10,10
HitPosition: 470
[Mania]
Keys: 4
ColumnWidth: 20,20,20,20
HitPosition: 460

View File

@ -0,0 +1,4 @@
[Mania]
Keys: 4
ColumnWidth: 10,10,10,10,10,10,10
HitPosition: 470

View File

@ -0,0 +1,9 @@
[Mania]
Keys: 4
ColumnWidth: 10,10,10,10
HitPosition: 470
[Mania]
Keys: 2
ColumnWidth: 20,20
HitPosition: 460

View File

@ -0,0 +1,4 @@
[Mania]
Keys: 4
ColumnWidth: 10,10,10,10
HitPosition: 470

View File

@ -0,0 +1,87 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.IO;
using osu.Game.Skinning;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Skins
{
[TestFixture]
public class LegacyManiaSkinDecoderTest
{
[Test]
public void TestParseSingleConfig()
{
var decoder = new LegacyManiaSkinDecoder();
using (var resStream = TestResources.OpenResource("mania-skin-single.ini"))
using (var stream = new LineBufferedReader(resStream))
{
var configs = decoder.Decode(stream);
Assert.That(configs.Count, Is.EqualTo(1));
Assert.That(configs[0].Keys, Is.EqualTo(4));
Assert.That(configs[0].ColumnWidth, Is.EquivalentTo(new float[] { 16, 16, 16, 16 }));
Assert.That(configs[0].HitPosition, Is.EqualTo(16));
}
}
[Test]
public void TestParseMultipleConfig()
{
var decoder = new LegacyManiaSkinDecoder();
using (var resStream = TestResources.OpenResource("mania-skin-multiple.ini"))
using (var stream = new LineBufferedReader(resStream))
{
var configs = decoder.Decode(stream);
Assert.That(configs.Count, Is.EqualTo(2));
Assert.That(configs[0].Keys, Is.EqualTo(4));
Assert.That(configs[0].ColumnWidth, Is.EquivalentTo(new float[] { 16, 16, 16, 16 }));
Assert.That(configs[0].HitPosition, Is.EqualTo(16));
Assert.That(configs[1].Keys, Is.EqualTo(2));
Assert.That(configs[1].ColumnWidth, Is.EquivalentTo(new float[] { 32, 32 }));
Assert.That(configs[1].HitPosition, Is.EqualTo(32));
}
}
[Test]
public void TestParseDuplicateConfig()
{
var decoder = new LegacyManiaSkinDecoder();
using (var resStream = TestResources.OpenResource("mania-skin-single.ini"))
using (var stream = new LineBufferedReader(resStream))
{
var configs = decoder.Decode(stream);
Assert.That(configs.Count, Is.EqualTo(1));
Assert.That(configs[0].Keys, Is.EqualTo(4));
Assert.That(configs[0].ColumnWidth, Is.EquivalentTo(new float[] { 16, 16, 16, 16 }));
Assert.That(configs[0].HitPosition, Is.EqualTo(16));
}
}
[Test]
public void TestParseWithUnnecessaryExtraData()
{
var decoder = new LegacyManiaSkinDecoder();
using (var resStream = TestResources.OpenResource("mania-skin-extra-data.ini"))
using (var stream = new LineBufferedReader(resStream))
{
var configs = decoder.Decode(stream);
Assert.That(configs.Count, Is.EqualTo(1));
Assert.That(configs[0].Keys, Is.EqualTo(4));
Assert.That(configs[0].ColumnWidth, Is.EquivalentTo(new float[] { 16, 16, 16, 16 }));
Assert.That(configs[0].HitPosition, Is.EqualTo(16));
}
}
}
}

View File

@ -91,13 +91,14 @@ namespace osu.Game.Tests.Visual.UserInterface
var easierMods = osu.GetModsFor(ModType.DifficultyReduction);
var harderMods = osu.GetModsFor(ModType.DifficultyIncrease);
var conversionMods = osu.GetModsFor(ModType.Conversion);
var noFailMod = osu.GetModsFor(ModType.DifficultyReduction).FirstOrDefault(m => m is OsuModNoFail);
var hiddenMod = harderMods.FirstOrDefault(m => m is OsuModHidden);
var doubleTimeMod = harderMods.OfType<MultiMod>().FirstOrDefault(m => m.Mods.Any(a => a is OsuModDoubleTime));
var spunOutMod = easierMods.FirstOrDefault(m => m is OsuModSpunOut);
var targetMod = conversionMods.FirstOrDefault(m => m is OsuModTarget);
var easy = easierMods.FirstOrDefault(m => m is OsuModEasy);
var hardRock = harderMods.FirstOrDefault(m => m is OsuModHardRock);
@ -109,7 +110,7 @@ namespace osu.Game.Tests.Visual.UserInterface
testMultiplierTextColour(noFailMod, () => modSelect.LowMultiplierColour);
testMultiplierTextColour(hiddenMod, () => modSelect.HighMultiplierColour);
testUnimplementedMod(spunOutMod);
testUnimplementedMod(targetMod);
}
[Test]

View File

@ -41,6 +41,7 @@ namespace osu.Game.Beatmaps.Formats
section = Section.None;
}
OnBeginNewSection(section);
continue;
}
@ -57,6 +58,14 @@ namespace osu.Game.Beatmaps.Formats
protected virtual bool ShouldSkipLine(string line) => string.IsNullOrWhiteSpace(line) || line.AsSpan().TrimStart().StartsWith("//".AsSpan(), StringComparison.Ordinal);
/// <summary>
/// Invoked when a new <see cref="Section"/> has been entered.
/// </summary>
/// <param name="section">The entered <see cref="Section"/>.</param>
protected virtual void OnBeginNewSection(Section section)
{
}
protected virtual void ParseLine(T output, Section section, string line)
{
line = StripComments(line);
@ -139,7 +148,8 @@ namespace osu.Game.Beatmaps.Formats
Colours,
HitObjects,
Variables,
Fonts
Fonts,
Mania
}
internal class LegacyDifficultyControlPoint : DifficultyControlPoint

View File

@ -38,6 +38,15 @@ namespace osu.Game.Rulesets.Objects.Drawables
private readonly Lazy<List<DrawableHitObject>> nestedHitObjects = new Lazy<List<DrawableHitObject>>();
public IReadOnlyList<DrawableHitObject> NestedHitObjects => nestedHitObjects.IsValueCreated ? nestedHitObjects.Value : (IReadOnlyList<DrawableHitObject>)Array.Empty<DrawableHitObject>();
/// <summary>
/// Whether this object should handle any user input events.
/// </summary>
public bool HandleUserInput { get; set; } = true;
public override bool PropagatePositionalInputSubTree => HandleUserInput;
public override bool PropagateNonPositionalInputSubTree => HandleUserInput;
/// <summary>
/// Invoked when a <see cref="JudgementResult"/> has been applied by this <see cref="DrawableHitObject"/> or a nested <see cref="DrawableHitObject"/>.
/// </summary>

View File

@ -0,0 +1,30 @@
// 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;
namespace osu.Game.Skinning
{
public class LegacyManiaSkinConfiguration
{
public readonly int Keys;
public readonly float[] ColumnLineWidth;
public readonly float[] ColumnSpacing;
public readonly float[] ColumnWidth;
public float HitPosition = 124.8f; // (480 - 402) * 1.6f
public LegacyManiaSkinConfiguration(int keys)
{
Keys = keys;
ColumnLineWidth = new float[keys + 1];
ColumnSpacing = new float[keys - 1];
ColumnWidth = new float[keys];
ColumnLineWidth.AsSpan().Fill(2);
ColumnWidth.AsSpan().Fill(48);
}
}
}

View File

@ -0,0 +1,110 @@
// 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.Diagnostics;
using System.Globalization;
using System.Linq;
using osu.Game.Beatmaps.Formats;
namespace osu.Game.Skinning
{
public class LegacyManiaSkinDecoder : LegacyDecoder<List<LegacyManiaSkinConfiguration>>
{
private const float size_scale_factor = 1.6f;
public LegacyManiaSkinDecoder()
: base(1)
{
}
private readonly List<string> pendingLines = new List<string>();
private LegacyManiaSkinConfiguration currentConfig;
protected override void OnBeginNewSection(Section section)
{
base.OnBeginNewSection(section);
// If a new section is reached with pending lines remaining, they can all be discarded as there isn't a valid configuration to parse them into.
pendingLines.Clear();
currentConfig = null;
}
protected override void ParseLine(List<LegacyManiaSkinConfiguration> output, Section section, string line)
{
line = StripComments(line);
switch (section)
{
case Section.Mania:
var pair = SplitKeyVal(line);
switch (pair.Key)
{
case "Keys":
currentConfig = new LegacyManiaSkinConfiguration(int.Parse(pair.Value, CultureInfo.InvariantCulture));
// Silently ignore duplicate configurations.
if (output.All(c => c.Keys != currentConfig.Keys))
output.Add(currentConfig);
// All existing lines can be flushed now that we have a valid configuration.
flushPendingLines();
break;
default:
pendingLines.Add(line);
// Hold all lines until a "Keys" item is found.
if (currentConfig != null)
flushPendingLines();
break;
}
break;
}
}
private void flushPendingLines()
{
Debug.Assert(currentConfig != null);
foreach (var line in pendingLines)
{
var pair = SplitKeyVal(line);
switch (pair.Key)
{
case "ColumnLineWidth":
parseArrayValue(pair.Value, currentConfig.ColumnLineWidth);
break;
case "ColumnSpacing":
parseArrayValue(pair.Value, currentConfig.ColumnSpacing);
break;
case "ColumnWidth":
parseArrayValue(pair.Value, currentConfig.ColumnWidth);
break;
case "HitPosition":
currentConfig.HitPosition = (480 - float.Parse(pair.Value, CultureInfo.InvariantCulture)) * size_scale_factor;
break;
}
}
}
private void parseArrayValue(string value, float[] output)
{
string[] values = value.Split(',');
for (int i = 0; i < values.Length; i++)
{
if (i >= output.Length)
break;
output[i] = float.Parse(values[i], CultureInfo.InvariantCulture) * size_scale_factor;
}
}
}
}