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

Merge branch 'master' into fix-follow-point-transforms

This commit is contained in:
Dean Herbert 2020-03-31 20:03:42 +09:00 committed by GitHub
commit 7a9d878a34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
84 changed files with 1404 additions and 330 deletions

View File

@ -5,6 +5,6 @@
"version": "3.1.100" "version": "3.1.100"
}, },
"msbuild-sdks": { "msbuild-sdks": {
"Microsoft.Build.Traversal": "2.0.24" "Microsoft.Build.Traversal": "2.0.32"
} }
} }

View File

@ -72,10 +72,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
protected override Skill[] CreateSkills(IBeatmap beatmap) protected override Skill[] CreateSkills(IBeatmap beatmap)
{ {
using (var catcher = new Catcher(beatmap.BeatmapInfo.BaseDifficulty)) using (var catcher = new Catcher(beatmap.BeatmapInfo.BaseDifficulty))
{
halfCatcherWidth = catcher.CatchWidth * 0.5f; halfCatcherWidth = catcher.CatchWidth * 0.5f;
halfCatcherWidth *= 0.8f; // We're only using 80% of the catcher's width to simulate imperfect gameplay.
}
return new Skill[] return new Skill[]
{ {

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -30,6 +31,22 @@ namespace osu.Game.Rulesets.Catch.Mods
Value = 5, Value = 5,
}; };
public override string SettingDescription
{
get
{
string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}";
string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}";
return string.Join(", ", new[]
{
circleSize,
base.SettingDescription,
approachRate
}.Where(s => !string.IsNullOrEmpty(s)));
}
}
protected override void TransferSettings(BeatmapDifficulty difficulty) protected override void TransferSettings(BeatmapDifficulty difficulty)
{ {
base.TransferSettings(difficulty); base.TransferSettings(difficulty);

View File

@ -37,10 +37,15 @@ namespace osu.Game.Rulesets.Catch.UI
public CatcherAnimationState CurrentState { get; private set; } public CatcherAnimationState CurrentState { get; private set; }
/// <summary>
/// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable.
/// </summary>
private const float allowed_catch_range = 0.8f;
/// <summary> /// <summary>
/// Width of the area that can be used to attempt catches during gameplay. /// Width of the area that can be used to attempt catches during gameplay.
/// </summary> /// </summary>
internal float CatchWidth => CatcherArea.CATCHER_SIZE * Math.Abs(Scale.X); internal float CatchWidth => CatcherArea.CATCHER_SIZE * Math.Abs(Scale.X) * allowed_catch_range;
protected bool Dashing protected bool Dashing
{ {
@ -141,14 +146,14 @@ namespace osu.Game.Rulesets.Catch.UI
var ourRadius = fruit.DisplayRadius; var ourRadius = fruit.DisplayRadius;
float theirRadius = 0; float theirRadius = 0;
const float allowance = 6; const float allowance = 10;
while (caughtFruit.Any(f => while (caughtFruit.Any(f =>
f.LifetimeEnd == double.MaxValue && f.LifetimeEnd == double.MaxValue &&
Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2))) Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2)))
{ {
var diff = (ourRadius + theirRadius) / allowance; var diff = (ourRadius + theirRadius) / allowance;
fruit.X += (RNG.NextSingle() - 0.5f) * 2 * diff; fruit.X += (RNG.NextSingle() - 0.5f) * diff * 2;
fruit.Y -= RNG.NextSingle() * diff; fruit.Y -= RNG.NextSingle() * diff;
} }

View File

@ -0,0 +1,38 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.UI;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
/// <summary>
/// A container to be used in a <see cref="ManiaSkinnableTestScene"/> to provide a resolvable <see cref="Column"/> dependency.
/// </summary>
public class ColumnTestContainer : Container
{
protected override Container<Drawable> Content => content;
private readonly Container content;
[Cached]
private readonly Column column;
public ColumnTestContainer(int column, ManiaAction action)
{
this.column = new Column(column)
{
Action = { Value = action },
AccentColour = Color4.Orange
};
InternalChild = content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4)
{
RelativeSizeAxes = Axes.Both
};
}
}
}

View File

@ -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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
/// <summary>
/// A test scene for a mania hitobject.
/// </summary>
public abstract class ManiaHitObjectTestScene : ManiaSkinnableTestScene
{
[BackgroundDependencyLoader]
private void load()
{
SetContents(() => new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Height = 0.7f,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
new ColumnTestContainer(0, ManiaAction.Key1)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
Width = 80,
Child = new ScrollingHitObjectContainer
{
RelativeSizeAxes = Axes.Both,
Clock = new FramedClock(new StopwatchClock()),
}.With(c =>
{
c.Add(CreateHitObject().With(h => h.AccentColour.Value = Color4.Orange));
})
},
new ColumnTestContainer(1, ManiaAction.Key2)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
Width = 80,
Child = new ScrollingHitObjectContainer
{
RelativeSizeAxes = Axes.Both,
Clock = new FramedClock(new StopwatchClock()),
}.With(c =>
{
c.Add(CreateHitObject().With(h => h.AccentColour.Value = Color4.Orange));
})
},
}
});
}
protected abstract DrawableManiaHitObject CreateHitObject();
}
}

View File

@ -0,0 +1,58 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Rulesets.UI.Scrolling.Algorithms;
using osu.Game.Tests.Visual;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
/// <summary>
/// A test scene for skinnable mania components.
/// </summary>
public abstract class ManiaSkinnableTestScene : SkinnableTestScene
{
[Cached(Type = typeof(IScrollingInfo))]
private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo();
protected ManiaSkinnableTestScene()
{
scrollingInfo.Direction.Value = ScrollingDirection.Down;
Add(new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.SlateGray.Opacity(0.2f),
Depth = 1
});
}
[Test]
public void TestScrollingDown()
{
AddStep("change direction to down", () => scrollingInfo.Direction.Value = ScrollingDirection.Down);
}
[Test]
public void TestScrollingUp()
{
AddStep("change direction to up", () => scrollingInfo.Direction.Value = ScrollingDirection.Up);
}
private class TestScrollingInfo : IScrollingInfo
{
public readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
IBindable<ScrollingDirection> IScrollingInfo.Direction => Direction;
IBindable<double> IScrollingInfo.TimeRange { get; } = new Bindable<double>(1000);
IScrollAlgorithm IScrollingInfo.Algorithm { get; } = new ConstantScrollAlgorithm();
}
}
}

View File

@ -6,13 +6,13 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Tests.Visual;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests.Skinning
{ {
public class TestSceneDrawableJudgement : SkinnableTestScene public class TestSceneDrawableJudgement : SkinnableTestScene
{ {

View File

@ -16,4 +16,8 @@ namespace osu.Game.Rulesets.Mania
protected override string ComponentName => Component.ToString().ToLower(); protected override string ComponentName => Component.ToString().ToLower();
} }
public enum ManiaSkinComponents
{
}
} }

View File

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

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Replays.Legacy; using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
@ -26,13 +27,7 @@ namespace osu.Game.Rulesets.Mania.Replays
public void FromLegacy(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame lastFrame = null) public void FromLegacy(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
{ {
// We don't need to fully convert, just create the converter var maniaBeatmap = (ManiaBeatmap)beatmap;
var converter = new ManiaBeatmapConverter(beatmap, new ManiaRuleset());
// NB: Via co-op mod, osu-stable can have two stages with floor(col/2) and ceil(col/2) columns. This will need special handling
// elsewhere in the game if we do choose to support the old co-op mod anyway. For now, assume that there is only one stage.
var stage = new StageDefinition { Columns = converter.TargetColumns };
var normalAction = ManiaAction.Key1; var normalAction = ManiaAction.Key1;
var specialAction = ManiaAction.Special1; var specialAction = ManiaAction.Special1;
@ -42,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Replays
while (activeColumns > 0) while (activeColumns > 0)
{ {
var isSpecial = stage.IsSpecialColumn(counter); var isSpecial = maniaBeatmap.Stages.First().IsSpecialColumn(counter);
if ((activeColumns & 1) > 0) if ((activeColumns & 1) > 0)
Actions.Add(isSpecial ? specialAction : normalAction); Actions.Add(isSpecial ? specialAction : normalAction);
@ -59,17 +54,15 @@ namespace osu.Game.Rulesets.Mania.Replays
public LegacyReplayFrame ToLegacy(IBeatmap beatmap) public LegacyReplayFrame ToLegacy(IBeatmap beatmap)
{ {
var maniaBeatmap = (ManiaBeatmap)beatmap;
int keys = 0; int keys = 0;
var converter = new ManiaBeatmapConverter(beatmap, new ManiaRuleset());
var stage = new StageDefinition { Columns = converter.TargetColumns };
var specialColumns = new List<int>(); var specialColumns = new List<int>();
for (int i = 0; i < converter.TargetColumns; i++) for (int i = 0; i < maniaBeatmap.TotalColumns; i++)
{ {
if (stage.IsSpecialColumn(i)) if (maniaBeatmap.Stages.First().IsSpecialColumn(i))
specialColumns.Add(i); specialColumns.Add(i);
} }

View File

@ -0,0 +1,41 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Skinning
{
/// <summary>
/// A <see cref="CompositeDrawable"/> which is placed somewhere within a <see cref="Column"/>.
/// </summary>
public class LegacyManiaColumnElement : CompositeDrawable
{
[Resolved(CanBeNull = true)]
[CanBeNull]
protected ManiaStage Stage { get; private set; }
[Resolved]
protected Column Column { get; private set; }
/// <summary>
/// The column index to use for texture lookups, in the case of no user-provided configuration.
/// </summary>
protected int FallbackColumnIndex { get; private set; }
[BackgroundDependencyLoader]
private void load()
{
if (Stage == null)
FallbackColumnIndex = Column.Index % 2 + 1;
else
{
int dist = Math.Min(Column.Index, Stage.Columns.Count - Column.Index - 1);
FallbackColumnIndex = dist % 2 + 1;
}
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
@ -15,9 +16,19 @@ namespace osu.Game.Rulesets.Mania.Skinning
{ {
private readonly ISkin source; private readonly ISkin source;
public ManiaLegacySkinTransformer(ISkin source) private Lazy<bool> isLegacySkin;
public ManiaLegacySkinTransformer(ISkinSource source)
{ {
this.source = source; this.source = source;
source.SourceChanged += sourceChanged;
sourceChanged();
}
private void sourceChanged()
{
isLegacySkin = new Lazy<bool>(() => source.GetConfig<LegacySkinConfiguration.LegacySetting, decimal>(LegacySkinConfiguration.LegacySetting.Version) != null);
} }
public Drawable GetDrawableComponent(ISkinComponent component) public Drawable GetDrawableComponent(ISkinComponent component)
@ -26,6 +37,12 @@ namespace osu.Game.Rulesets.Mania.Skinning
{ {
case GameplaySkinComponent<HitResult> resultComponent: case GameplaySkinComponent<HitResult> resultComponent:
return getResult(resultComponent); return getResult(resultComponent);
case ManiaSkinComponent _:
if (!isLegacySkin.Value)
return null;
break;
} }
return null; return null;

View File

@ -19,6 +19,7 @@ using osuTK;
namespace osu.Game.Rulesets.Mania.UI namespace osu.Game.Rulesets.Mania.UI
{ {
[Cached]
public class Column : ScrollingPlayfield, IKeyBindingHandler<ManiaAction>, IHasAccentColour public class Column : ScrollingPlayfield, IKeyBindingHandler<ManiaAction>, IHasAccentColour
{ {
public const float COLUMN_WIDTH = 80; public const float COLUMN_WIDTH = 80;

View File

@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Mania.UI
/// <summary> /// <summary>
/// A collection of <see cref="Column"/>s. /// A collection of <see cref="Column"/>s.
/// </summary> /// </summary>
[Cached]
public class ManiaStage : ScrollingPlayfield public class ManiaStage : ScrollingPlayfield
{ {
public const float COLUMN_SPACING = 1; public const float COLUMN_SPACING = 1;

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

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -30,6 +31,22 @@ namespace osu.Game.Rulesets.Osu.Mods
Value = 5, Value = 5,
}; };
public override string SettingDescription
{
get
{
string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}";
string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}";
return string.Join(", ", new[]
{
circleSize,
base.SettingDescription,
approachRate
}.Where(s => !string.IsNullOrEmpty(s)));
}
}
protected override void TransferSettings(BeatmapDifficulty difficulty) protected override void TransferSettings(BeatmapDifficulty difficulty)
{ {
base.TransferSettings(difficulty); base.TransferSettings(difficulty);

View File

@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Mods
void handleHitCircle(DrawableHitCircle circle) void handleHitCircle(DrawableHitCircle circle)
{ {
if (!circle.IsHovered) if (!circle.HitArea.IsHovered)
return; return;
Debug.Assert(circle.HitObject.HitWindows != null); Debug.Assert(circle.HitObject.HitWindows != null);

View File

@ -2,21 +2,46 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModSpunOut : Mod public class OsuModSpunOut : Mod, IApplicableToDrawableHitObjects
{ {
public override string Name => "Spun Out"; public override string Name => "Spun Out";
public override string Acronym => "SO"; public override string Acronym => "SO";
public override IconUsage? Icon => OsuIcon.ModSpunout; 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 string Description => @"Spinners will be automatically completed.";
public override double ScoreMultiplier => 0.9; public override double ScoreMultiplier => 0.9;
public override bool Ranked => true; public override bool Ranked => true;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot) }; 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; public readonly SkinnableDrawable CirclePiece;
private readonly Container scaleContainer; private readonly Container scaleContainer;
protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle;
public DrawableHitCircle(HitCircle h) public DrawableHitCircle(HitCircle h)
: base(h) : base(h)
{ {
@ -57,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return true; return true;
}, },
}, },
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.HitCircle), _ => new MainCirclePiece()), CirclePiece = new SkinnableDrawable(new OsuSkinComponent(CirclePieceComponent), _ => new MainCirclePiece()),
ApproachCircle = new ApproachCircle ApproachCircle = new ApproachCircle
{ {
Alpha = 0, Alpha = 0,

View File

@ -11,6 +11,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Scoring;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -196,6 +197,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ApplyResult(r => r.Type = r.Judgement.MaxResult); ApplyResult(r => r.Type = r.Judgement.MaxResult);
} }
public override void PlaySamples()
{
// rather than doing it this way, we should probably attach the sample to the tail circle.
// this can only be done after we stop using LegacyLastTick.
if (TailCircle.Result.Type != HitResult.Miss)
base.PlaySamples();
}
protected override void UpdateStateTransforms(ArmedState state) protected override void UpdateStateTransforms(ArmedState state)
{ {
base.UpdateStateTransforms(state); base.UpdateStateTransforms(state);

View File

@ -14,6 +14,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>(); private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>();
private readonly IBindable<int> pathVersion = new Bindable<int>(); private readonly IBindable<int> pathVersion = new Bindable<int>();
protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle;
private readonly Slider slider; private readonly Slider slider;
public DrawableSliderHead(Slider slider, HitCircle h) 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) 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; bool isRepeatAtEnd = sliderRepeat.RepeatIndex % 2 == 0;
List<Vector2> curve = ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve; 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() 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(); base.Update();
if (HandleUserInput)
Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false;
} }
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()
{ {
base.UpdateAfterChildren(); base.UpdateAfterChildren();
if (!SpmCounter.IsPresent && Disc.Tracking)
SpmCounter.FadeIn(HitObject.TimeFadeIn);
circle.Rotation = Disc.Rotation; circle.Rotation = Disc.Rotation;
Ticks.Rotation = Disc.Rotation; Ticks.Rotation = Disc.Rotation;
SpmCounter.SetRotation(Disc.RotationAbsolute); 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) protected override bool OnMouseMove(MouseMoveEvent e)
{ {
mousePosition = Parent.ToLocalSpace(e.ScreenSpaceMousePosition); mousePosition = Parent.ToLocalSpace(e.ScreenSpaceMousePosition);
@ -93,27 +98,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
var thisAngle = -MathUtils.RadiansToDegrees(MathF.Atan2(mousePosition.X - DrawSize.X / 2, mousePosition.Y - DrawSize.Y / 2)); 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 (tracking)
{ Rotate(delta);
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);
}
lastAngle = thisAngle; 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)); 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

@ -34,8 +34,6 @@ namespace osu.Game.Rulesets.Osu.Objects
public class SliderRepeatJudgement : OsuJudgement public class SliderRepeatJudgement : OsuJudgement
{ {
public override bool IsBonus => true;
protected override int NumericResultFor(HitResult result) => result == MaxResult ? 30 : 0; protected override int NumericResultFor(HitResult result) => result == MaxResult ? 30 : 0;
} }
} }

View File

@ -36,8 +36,6 @@ namespace osu.Game.Rulesets.Osu.Objects
public class SliderTickJudgement : OsuJudgement public class SliderTickJudgement : OsuJudgement
{ {
public override bool IsBonus => true;
protected override int NumericResultFor(HitResult result) => result == MaxResult ? 10 : 0; protected override int NumericResultFor(HitResult result) => result == MaxResult ? 10 : 0;
} }
} }

View File

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

View File

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

View File

@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -18,8 +19,12 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
public class LegacyMainCirclePiece : CompositeDrawable 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); Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
} }
@ -39,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
hitCircleSprite = new Sprite hitCircleSprite = new Sprite
{ {
Texture = skin.GetTexture("hitcircle"), Texture = getTextureWithFallback(string.Empty),
Colour = drawableObject.AccentColour.Value, Colour = drawableObject.AccentColour.Value,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -51,12 +56,17 @@ namespace osu.Game.Rulesets.Osu.Skinning
}, confineMode: ConfineMode.NoScaling), }, confineMode: ConfineMode.NoScaling),
new Sprite new Sprite
{ {
Texture = skin.GetTexture("hitcircleoverlay"), Texture = getTextureWithFallback("overlay"),
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
} }
}; };
bool overlayAboveNumber = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
if (!overlayAboveNumber)
ChangeInternalChildDepth(hitCircleText, -float.MaxValue);
state.BindTo(drawableObject.State); state.BindTo(drawableObject.State);
state.BindValueChanged(updateState, true); state.BindValueChanged(updateState, true);
@ -65,6 +75,16 @@ namespace osu.Game.Rulesets.Osu.Skinning
indexInCurrentCombo.BindTo(osuObject.IndexInCurrentComboBindable); indexInCurrentCombo.BindTo(osuObject.IndexInCurrentComboBindable);
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); 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) private void updateState(ValueChangedEvent<ArmedState> state)

View File

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

View File

@ -11,6 +11,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
SliderPathRadius, SliderPathRadius,
AllowSliderBallTint, AllowSliderBallTint,
CursorExpand, CursorExpand,
CursorRotate CursorRotate,
HitCircleOverlayAboveNumber
} }
} }

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,31 @@
osu file format v14
[Events]
//Background and Video events
0,0,"BG.jpg",0,0
Video,0,"video.avi"
//Break Periods
//Storyboard Layer 0 (Background)
//Storyboard Layer 1 (Fail)
//Storyboard Layer 2 (Pass)
//Storyboard Layer 3 (Foreground)
//Storyboard Layer 4 (Overlay)
//Storyboard Sound Samples
[TimingPoints]
1674,333.333333333333,4,2,1,70,1,0
1674,-100,4,2,1,70,0,0
3340,-100,4,2,1,70,0,0
3507,-100,4,2,1,70,0,0
3673,-100,4,2,1,70,0,0
[Colours]
Combo1 : 240,80,80
Combo2 : 171,252,203
Combo3 : 128,128,255
Combo4 : 249,254,186
[HitObjects]
148,303,1674,5,6,3:2:0:0:
378,252,1840,1,0,0:0:0:0:
389,270,2340,5,2,0:1:0:0:

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

@ -4,7 +4,6 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch;
@ -74,9 +73,6 @@ namespace osu.Game.Tests.Visual.Gameplay
Beatmap.Value = working; Beatmap.Value = working;
SelectedMods.Value = new[] { ruleset.GetAllMods().First(m => m is ModNoFail) }; SelectedMods.Value = new[] { ruleset.GetAllMods().First(m => m is ModNoFail) };
Player?.Exit();
Player = null;
Player = CreatePlayer(ruleset); Player = CreatePlayer(ruleset);
LoadScreen(Player); LoadScreen(Player);

View File

@ -3,8 +3,14 @@
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.Break;
using osu.Game.Screens.Ranking;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
@ -15,20 +21,38 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override Player CreatePlayer(Ruleset ruleset) protected override Player CreatePlayer(Ruleset ruleset)
{ {
SelectedMods.Value = SelectedMods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray(); SelectedMods.Value = new[] { ruleset.GetAutoplayMod() };
return new TestPlayer(false, false); return new TestPlayer(false);
} }
protected override void AddCheckSteps() protected override void AddCheckSteps()
{ {
AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0); AddUntilStep("score above zero", () => Player.ScoreProcessor.TotalScore.Value > 0);
AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2)); AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2));
AddStep("seek to break time", () => Player.GameplayClockContainer.Seek(Player.BreakOverlay.Breaks.First().StartTime)); seekToBreak(0);
AddUntilStep("wait for seek to complete", () => AddAssert("keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting);
Player.HUDOverlay.Progress.ReferenceClock.CurrentTime >= Player.BreakOverlay.Breaks.First().StartTime); AddAssert("overlay displays 100% accuracy", () => Player.BreakOverlay.ChildrenOfType<BreakInfo>().Single().AccuracyDisplay.Current.Value == 1);
AddAssert("test keys not counting", () => !Player.HUDOverlay.KeyCounter.IsCounting);
AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000)); AddStep("rewind", () => Player.GameplayClockContainer.Seek(-80000));
AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0)); AddUntilStep("key counter reset", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses == 0));
seekToBreak(0);
seekToBreak(1);
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
AddUntilStep("results displayed", () => getResultsScreen() != null);
AddAssert("score has combo", () => getResultsScreen().Score.Combo > 100);
AddAssert("score has no misses", () => getResultsScreen().Score.Statistics[HitResult.Miss] == 0);
ResultsScreen getResultsScreen() => Stack.CurrentScreen as ResultsScreen;
}
private void seekToBreak(int breakIndex)
{
AddStep($"seek to break {breakIndex}", () => Player.GameplayClockContainer.Seek(destBreak().StartTime));
AddUntilStep("wait for seek to complete", () => Player.HUDOverlay.Progress.ReferenceClock.CurrentTime >= destBreak().StartTime);
BreakPeriod destBreak() => Player.ChildrenOfType<BreakTracker>().First().Breaks.ElementAt(breakIndex);
} }
} }
} }

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -12,14 +13,16 @@ using osu.Game.Screens.Play;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
[TestFixture] [TestFixture]
public class TestSceneBreakOverlay : OsuTestScene public class TestSceneBreakTracker : OsuTestScene
{ {
public override IReadOnlyList<Type> RequiredTypes => new[] public override IReadOnlyList<Type> RequiredTypes => new[]
{ {
typeof(BreakOverlay), typeof(BreakOverlay),
}; };
private readonly TestBreakOverlay breakOverlay; private readonly BreakOverlay breakOverlay;
private readonly TestBreakTracker breakTracker;
private readonly IReadOnlyList<BreakPeriod> testBreaks = new List<BreakPeriod> private readonly IReadOnlyList<BreakPeriod> testBreaks = new List<BreakPeriod>
{ {
@ -35,9 +38,23 @@ namespace osu.Game.Tests.Visual.Gameplay
}, },
}; };
public TestSceneBreakOverlay() public TestSceneBreakTracker()
{ {
Add(breakOverlay = new TestBreakOverlay(true)); AddRange(new Drawable[]
{
breakTracker = new TestBreakTracker(),
breakOverlay = new BreakOverlay(true, null)
{
ProcessCustomClock = false,
}
});
}
protected override void Update()
{
base.Update();
breakOverlay.Clock = breakTracker.Clock;
} }
[Test] [Test]
@ -88,7 +105,7 @@ namespace osu.Game.Tests.Visual.Gameplay
loadBreaksStep("multiple breaks", testBreaks); loadBreaksStep("multiple breaks", testBreaks);
seekAndAssertBreak("seek to break start", testBreaks[1].StartTime, true); seekAndAssertBreak("seek to break start", testBreaks[1].StartTime, true);
AddAssert("is skipped to break #2", () => breakOverlay.CurrentBreakIndex == 1); AddAssert("is skipped to break #2", () => breakTracker.CurrentBreakIndex == 1);
seekAndAssertBreak("seek to break middle", testBreaks[1].StartTime + testBreaks[1].Duration / 2, true); seekAndAssertBreak("seek to break middle", testBreaks[1].StartTime + testBreaks[1].Duration / 2, true);
seekAndAssertBreak("seek to break end", testBreaks[1].EndTime, false); seekAndAssertBreak("seek to break end", testBreaks[1].EndTime, false);
@ -110,7 +127,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void addShowBreakStep(double seconds) private void addShowBreakStep(double seconds)
{ {
AddStep($"show '{seconds}s' break", () => breakOverlay.Breaks = new List<BreakPeriod> AddStep($"show '{seconds}s' break", () => breakOverlay.Breaks = breakTracker.Breaks = new List<BreakPeriod>
{ {
new BreakPeriod new BreakPeriod
{ {
@ -122,12 +139,12 @@ namespace osu.Game.Tests.Visual.Gameplay
private void setClock(bool useManual) private void setClock(bool useManual)
{ {
AddStep($"set {(useManual ? "manual" : "realtime")} clock", () => breakOverlay.SwitchClock(useManual)); AddStep($"set {(useManual ? "manual" : "realtime")} clock", () => breakTracker.SwitchClock(useManual));
} }
private void loadBreaksStep(string breakDescription, IReadOnlyList<BreakPeriod> breaks) private void loadBreaksStep(string breakDescription, IReadOnlyList<BreakPeriod> breaks)
{ {
AddStep($"load {breakDescription}", () => breakOverlay.Breaks = breaks); AddStep($"load {breakDescription}", () => breakOverlay.Breaks = breakTracker.Breaks = breaks);
seekAndAssertBreak("seek back to 0", 0, false); seekAndAssertBreak("seek back to 0", 0, false);
} }
@ -151,17 +168,18 @@ namespace osu.Game.Tests.Visual.Gameplay
private void seekAndAssertBreak(string seekStepDescription, double time, bool shouldBeBreak) private void seekAndAssertBreak(string seekStepDescription, double time, bool shouldBeBreak)
{ {
AddStep(seekStepDescription, () => breakOverlay.ManualClockTime = time); AddStep(seekStepDescription, () => breakTracker.ManualClockTime = time);
AddAssert($"is{(!shouldBeBreak ? " not" : string.Empty)} break time", () => AddAssert($"is{(!shouldBeBreak ? " not" : string.Empty)} break time", () =>
{ {
breakOverlay.ProgressTime(); breakTracker.ProgressTime();
return breakOverlay.IsBreakTime.Value == shouldBeBreak; return breakTracker.IsBreakTime.Value == shouldBeBreak;
}); });
} }
private class TestBreakOverlay : BreakOverlay private class TestBreakTracker : BreakTracker
{ {
private readonly FramedClock framedManualClock; public readonly FramedClock FramedManualClock;
private readonly ManualClock manualClock; private readonly ManualClock manualClock;
private IFrameBasedClock originalClock; private IFrameBasedClock originalClock;
@ -173,20 +191,19 @@ namespace osu.Game.Tests.Visual.Gameplay
set => manualClock.CurrentTime = value; set => manualClock.CurrentTime = value;
} }
public TestBreakOverlay(bool letterboxing) public TestBreakTracker()
: base(letterboxing)
{ {
framedManualClock = new FramedClock(manualClock = new ManualClock()); FramedManualClock = new FramedClock(manualClock = new ManualClock());
ProcessCustomClock = false; ProcessCustomClock = false;
} }
public void ProgressTime() public void ProgressTime()
{ {
framedManualClock.ProcessFrame(); FramedManualClock.ProcessFrame();
Update(); Update();
} }
public void SwitchClock(bool setManual) => Clock = setManual ? framedManualClock : originalClock; public void SwitchClock(bool setManual) => Clock = setManual ? FramedManualClock : originalClock;
protected override void LoadComplete() protected override void LoadComplete()
{ {

View File

@ -17,13 +17,12 @@ using osu.Game.Replays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Tests.Visual;
using osu.Game.Tests.Visual.UserInterface; using osu.Game.Tests.Visual.UserInterface;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
public class TestSceneReplayRecorder : OsuManualInputManagerTestScene public class TestSceneReplayRecorder : OsuManualInputManagerTestScene
{ {

View File

@ -13,12 +13,11 @@ using osu.Game.Replays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Tests.Visual;
using osu.Game.Tests.Visual.UserInterface; using osu.Game.Tests.Visual.UserInterface;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Tests.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
public class TestSceneReplayRecording : OsuTestScene public class TestSceneReplayRecording : OsuTestScene
{ {

View File

@ -9,8 +9,12 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Storyboards;
using osu.Game.Storyboards.Drawables; using osu.Game.Storyboards.Drawables;
using osu.Game.Tests.Resources;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
@ -54,7 +58,11 @@ namespace osu.Game.Tests.Visual.Gameplay
State = { Value = Visibility.Visible }, State = { Value = Visibility.Visible },
} }
}); });
}
[Test]
public void TestStoryboard()
{
AddStep("Restart", restart); AddStep("Restart", restart);
AddToggleStep("Passing", passing => AddToggleStep("Passing", passing =>
{ {
@ -62,6 +70,12 @@ namespace osu.Game.Tests.Visual.Gameplay
}); });
} }
[Test]
public void TestStoryboardMissingVideo()
{
AddStep("Load storyboard with missing video", loadStoryboardNoVideo);
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
@ -94,5 +108,28 @@ namespace osu.Game.Tests.Visual.Gameplay
storyboardContainer.Add(storyboard); storyboardContainer.Add(storyboard);
decoupledClock.ChangeSource(working.Track); decoupledClock.ChangeSource(working.Track);
} }
private void loadStoryboardNoVideo()
{
if (storyboard != null)
storyboardContainer.Remove(storyboard);
var decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true };
storyboardContainer.Clock = decoupledClock;
Storyboard sb;
using (var str = TestResources.OpenResource("storyboard_no_video.osu"))
using (var bfr = new LineBufferedReader(str))
{
var decoder = new LegacyStoryboardDecoder();
sb = decoder.Decode(bfr);
}
storyboard = sb.CreateDrawable(Beatmap.Value);
storyboardContainer.Add(storyboard);
decoupledClock.ChangeSource(Beatmap.Value.Track);
}
} }
} }

View File

@ -36,8 +36,6 @@ namespace osu.Game.Tests.Visual.Menus
[Test] [Test]
public void TestInstantLoad() public void TestInstantLoad()
{ {
// visual only, very impossible to test this using asserts.
AddStep("load immediately", () => AddStep("load immediately", () =>
{ {
loader = new TestLoader(); loader = new TestLoader();
@ -46,12 +44,17 @@ namespace osu.Game.Tests.Visual.Menus
LoadScreen(loader); LoadScreen(loader);
}); });
AddAssert("spinner did not display", () => loader.LoadingSpinner?.Alpha == 0); spinnerNotPresentOrHidden();
AddUntilStep("loaded", () => loader.ScreenLoaded); AddUntilStep("loaded", () => loader.ScreenLoaded);
AddUntilStep("not current", () => !loader.IsCurrentScreen()); AddUntilStep("not current", () => !loader.IsCurrentScreen());
spinnerNotPresentOrHidden();
} }
private void spinnerNotPresentOrHidden() =>
AddAssert("spinner did not display", () => loader.LoadingSpinner == null || loader.LoadingSpinner.Alpha == 0);
[Test] [Test]
public void TestDelayedLoad() public void TestDelayedLoad()
{ {

View File

@ -83,6 +83,82 @@ namespace osu.Game.Tests.Visual.SongSelect
waitForSelection(set_count, 3); waitForSelection(set_count, 3);
} }
[TestCase(true)]
[TestCase(false)]
public void TestTraversalBeyondVisible(bool forwards)
{
var sets = new List<BeatmapSetInfo>();
const int total_set_count = 200;
for (int i = 0; i < total_set_count; i++)
sets.Add(createTestBeatmapSet(i + 1));
loadBeatmaps(sets);
for (int i = 1; i < total_set_count; i += i)
selectNextAndAssert(i);
void selectNextAndAssert(int amount)
{
setSelected(forwards ? 1 : total_set_count, 1);
AddStep($"{(forwards ? "Next" : "Previous")} beatmap {amount} times", () =>
{
for (int i = 0; i < amount; i++)
{
carousel.SelectNext(forwards ? 1 : -1);
}
});
waitForSelection(forwards ? amount + 1 : total_set_count - amount);
}
}
[Test]
public void TestTraversalBeyondVisibleDifficulties()
{
var sets = new List<BeatmapSetInfo>();
const int total_set_count = 20;
for (int i = 0; i < total_set_count; i++)
sets.Add(createTestBeatmapSet(i + 1));
loadBeatmaps(sets);
// Selects next set once, difficulty index doesn't change
selectNextAndAssert(3, true, 2, 1);
// Selects next set 16 times (50 \ 3 == 16), difficulty index changes twice (50 % 3 == 2)
selectNextAndAssert(50, true, 17, 3);
// Travels around the carousel thrice (200 \ 60 == 3)
// continues to select 20 times (200 \ 60 == 20)
// selects next set 6 times (20 \ 3 == 6)
// difficulty index changes twice (20 % 3 == 2)
selectNextAndAssert(200, true, 7, 3);
// All same but in reverse
selectNextAndAssert(3, false, 19, 3);
selectNextAndAssert(50, false, 4, 1);
selectNextAndAssert(200, false, 14, 1);
void selectNextAndAssert(int amount, bool forwards, int expectedSet, int expectedDiff)
{
// Select very first or very last difficulty
setSelected(forwards ? 1 : 20, forwards ? 1 : 3);
AddStep($"{(forwards ? "Next" : "Previous")} difficulty {amount} times", () =>
{
for (int i = 0; i < amount; i++)
carousel.SelectNext(forwards ? 1 : -1, false);
});
waitForSelection(expectedSet, expectedDiff);
}
}
/// <summary> /// <summary>
/// Test filtering /// Test filtering
/// </summary> /// </summary>

View File

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

View File

@ -87,7 +87,7 @@ namespace osu.Game.Beatmaps
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz"; protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz";
protected override Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default)
{ {
if (archive != null) if (archive != null)
beatmapSet.Beatmaps = createBeatmapDifficulties(beatmapSet.Files); beatmapSet.Beatmaps = createBeatmapDifficulties(beatmapSet.Files);
@ -103,7 +103,19 @@ namespace osu.Game.Beatmaps
validateOnlineIds(beatmapSet); validateOnlineIds(beatmapSet);
return updateQueue.UpdateAsync(beatmapSet, cancellationToken); bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0);
await updateQueue.UpdateAsync(beatmapSet, cancellationToken);
// ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID.
if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0))
{
if (beatmapSet.OnlineBeatmapSetID != null)
{
beatmapSet.OnlineBeatmapSetID = null;
LogForModel(beatmapSet, "Disassociating beatmap set ID due to loss of all beatmap IDs");
}
}
} }
protected override void PreImport(BeatmapSetInfo beatmapSet) protected override void PreImport(BeatmapSetInfo beatmapSet)
@ -447,12 +459,15 @@ namespace osu.Game.Beatmaps
var res = req.Result; var res = req.Result;
beatmap.Status = res.Status; if (res != null)
beatmap.BeatmapSet.Status = res.BeatmapSet.Status; {
beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID; beatmap.Status = res.Status;
beatmap.OnlineBeatmapID = res.OnlineBeatmapID; beatmap.BeatmapSet.Status = res.BeatmapSet.Status;
beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID;
beatmap.OnlineBeatmapID = res.OnlineBeatmapID;
LogForModel(set, $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}."); LogForModel(set, $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}.");
}
} }
catch (Exception e) catch (Exception e)
{ {

View File

@ -41,6 +41,7 @@ namespace osu.Game.Beatmaps.Formats
section = Section.None; section = Section.None;
} }
OnBeginNewSection(section);
continue; 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); 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) protected virtual void ParseLine(T output, Section section, string line)
{ {
line = StripComments(line); line = StripComments(line);
@ -139,7 +148,8 @@ namespace osu.Game.Beatmaps.Formats
Colours, Colours,
HitObjects, HitObjects,
Variables, Variables,
Fonts Fonts,
Mania
} }
internal class LegacyDifficultyControlPoint : DifficultyControlPoint internal class LegacyDifficultyControlPoint : DifficultyControlPoint

View File

@ -40,7 +40,7 @@ namespace osu.Game.Graphics.Containers
/// <summary> /// <summary>
/// Whether player is in break time. /// Whether player is in break time.
/// Must be bound to <see cref="BreakOverlay.IsBreakTime"/> to allow for dim adjustments in gameplay. /// Must be bound to <see cref="BreakTracker.IsBreakTime"/> to allow for dim adjustments in gameplay.
/// </summary> /// </summary>
public readonly IBindable<bool> IsBreakTime = new Bindable<bool>(); public readonly IBindable<bool> IsBreakTime = new Bindable<bool>();

View File

@ -17,9 +17,9 @@ namespace osu.Game.IO
private readonly StreamReader streamReader; private readonly StreamReader streamReader;
private readonly Queue<string> lineBuffer; private readonly Queue<string> lineBuffer;
public LineBufferedReader(Stream stream) public LineBufferedReader(Stream stream, bool leaveOpen = false)
{ {
streamReader = new StreamReader(stream); streamReader = new StreamReader(stream, Encoding.UTF8, true, 1024, leaveOpen);
lineBuffer = new Queue<string>(); lineBuffer = new Queue<string>();
} }

View File

@ -12,11 +12,11 @@ namespace osu.Game.Online.API
/// An API request with a well-defined response type. /// An API request with a well-defined response type.
/// </summary> /// </summary>
/// <typeparam name="T">Type of the response (used for deserialisation).</typeparam> /// <typeparam name="T">Type of the response (used for deserialisation).</typeparam>
public abstract class APIRequest<T> : APIRequest public abstract class APIRequest<T> : APIRequest where T : class
{ {
protected override WebRequest CreateWebRequest() => new OsuJsonWebRequest<T>(Uri); protected override WebRequest CreateWebRequest() => new OsuJsonWebRequest<T>(Uri);
public T Result => ((OsuJsonWebRequest<T>)WebRequest).ResponseObject; public T Result => ((OsuJsonWebRequest<T>)WebRequest)?.ResponseObject;
protected APIRequest() protected APIRequest()
{ {

View File

@ -6,7 +6,7 @@ using osu.Game.Rulesets;
namespace osu.Game.Online.API.Requests namespace osu.Game.Online.API.Requests
{ {
public abstract class GetRankingsRequest<TModel> : APIRequest<TModel> public abstract class GetRankingsRequest<TModel> : APIRequest<TModel> where TModel : class
{ {
private readonly RulesetInfo ruleset; private readonly RulesetInfo ruleset;
private readonly int page; private readonly int page;

View File

@ -6,7 +6,7 @@ using osu.Framework.IO.Network;
namespace osu.Game.Online.API.Requests namespace osu.Game.Online.API.Requests
{ {
public abstract class PaginatedAPIRequest<T> : APIRequest<T> public abstract class PaginatedAPIRequest<T> : APIRequest<T> where T : class
{ {
private readonly int page; private readonly int page;
private readonly int itemsPerPage; private readonly int itemsPerPage;

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Configuration; using osu.Framework.Configuration;
@ -56,24 +57,32 @@ namespace osu.Game.Overlays.Settings.Sections.Input
}, },
}; };
rawInputToggle.ValueChanged += enabled => if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
{ {
// this is temporary until we support per-handler settings. rawInputToggle.Disabled = true;
const string raw_mouse_handler = @"OsuTKRawMouseHandler"; sensitivity.Bindable.Disabled = true;
const string standard_mouse_handler = @"OsuTKMouseHandler"; }
else
ignoredInputHandler.Value = enabled.NewValue ? standard_mouse_handler : raw_mouse_handler;
};
ignoredInputHandler = config.GetBindable<string>(FrameworkSetting.IgnoredInputHandlers);
ignoredInputHandler.ValueChanged += handler =>
{ {
bool raw = !handler.NewValue.Contains("Raw"); rawInputToggle.ValueChanged += enabled =>
rawInputToggle.Value = raw; {
sensitivity.Bindable.Disabled = !raw; // this is temporary until we support per-handler settings.
}; const string raw_mouse_handler = @"OsuTKRawMouseHandler";
const string standard_mouse_handler = @"OsuTKMouseHandler";
ignoredInputHandler.TriggerChange(); ignoredInputHandler.Value = enabled.NewValue ? standard_mouse_handler : raw_mouse_handler;
};
ignoredInputHandler = config.GetBindable<string>(FrameworkSetting.IgnoredInputHandlers);
ignoredInputHandler.ValueChanged += handler =>
{
bool raw = !handler.NewValue.Contains("Raw");
rawInputToggle.Value = raw;
sensitivity.Bindable.Disabled = !raw;
};
ignoredInputHandler.TriggerChange();
}
} }
private class SensitivitySetting : SettingsSlider<double, SensitivitySlider> private class SensitivitySetting : SettingsSlider<double, SensitivitySlider>

View File

@ -2,9 +2,15 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using osu.Game.IO.Serialization; using osu.Game.IO.Serialization;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
@ -42,6 +48,51 @@ namespace osu.Game.Rulesets.Mods
[JsonIgnore] [JsonIgnore]
public virtual string Description => string.Empty; public virtual string Description => string.Empty;
/// <summary>
/// The tooltip to display for this mod when used in a <see cref="ModIcon"/>.
/// </summary>
/// <remarks>
/// Differs from <see cref="Name"/>, as the value of attributes (AR, CS, etc) changeable via the mod
/// are displayed in the tooltip.
/// </remarks>
[JsonIgnore]
public string IconTooltip
{
get
{
string description = SettingDescription;
return string.IsNullOrEmpty(description) ? Name : $"{Name} ({description})";
}
}
/// <summary>
/// The description of editable settings of a mod to use in the <see cref="IconTooltip"/>.
/// </summary>
/// <remarks>
/// Parentheses are added to the tooltip, surrounding the value of this property. If this property is <c>string.Empty</c>,
/// the tooltip will not have parentheses.
/// </remarks>
public virtual string SettingDescription
{
get
{
var tooltipTexts = new List<string>();
foreach ((SettingSourceAttribute attr, PropertyInfo property) in this.GetOrderedSettingsSourceProperties())
{
object bindableObj = property.GetValue(this);
if ((bindableObj as IHasDefaultValue)?.IsDefault == true)
continue;
tooltipTexts.Add($"{attr.Label} {bindableObj}");
}
return string.Join(", ", tooltipTexts.Where(s => !string.IsNullOrEmpty(s)));
}
}
/// <summary> /// <summary>
/// The score multiplier of this mod. /// The score multiplier of this mod.
/// </summary> /// </summary>

View File

@ -7,6 +7,7 @@ using osu.Framework.Graphics.Sprites;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Configuration; using osu.Game.Configuration;
using System.Linq;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
@ -52,6 +53,21 @@ namespace osu.Game.Rulesets.Mods
Value = 5, Value = 5,
}; };
public override string SettingDescription
{
get
{
string drainRate = DrainRate.IsDefault ? string.Empty : $"HP {DrainRate.Value:N1}";
string overallDifficulty = OverallDifficulty.IsDefault ? string.Empty : $"OD {OverallDifficulty.Value:N1}";
return string.Join(", ", new[]
{
drainRate,
overallDifficulty
}.Where(s => !string.IsNullOrEmpty(s)));
}
}
private BeatmapDifficulty difficulty; private BeatmapDifficulty difficulty;
public void ReadFromDifficulty(BeatmapDifficulty difficulty) public void ReadFromDifficulty(BeatmapDifficulty difficulty)
@ -79,7 +95,7 @@ namespace osu.Game.Rulesets.Mods
/// <summary> /// <summary>
/// Transfer a setting from <see cref="BeatmapDifficulty"/> to a configuration bindable. /// Transfer a setting from <see cref="BeatmapDifficulty"/> to a configuration bindable.
/// Only performs the transfer if the user it not currently overriding.. /// Only performs the transfer if the user is not currently overriding.
/// </summary> /// </summary>
protected void TransferSetting<T>(BindableNumber<T> bindable, T beatmapDefault) protected void TransferSetting<T>(BindableNumber<T> bindable, T beatmapDefault)
where T : struct, IComparable<T>, IConvertible, IEquatable<T> where T : struct, IComparable<T>, IConvertible, IEquatable<T>

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using Humanizer;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -28,6 +29,8 @@ namespace osu.Game.Rulesets.Mods
MaxValue = 10 MaxValue = 10
}; };
public override string SettingDescription => Retries.IsDefault ? string.Empty : $"{"lives".ToQuantity(Retries.Value)}";
private int retries; private int retries;
private BindableNumber<double> health; private BindableNumber<double> health;

View File

@ -15,5 +15,7 @@ namespace osu.Game.Rulesets.Mods
{ {
track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange);
} }
public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N2}x";
} }
} }

View File

@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Mods
[SettingSource("Final rate", "The final speed to ramp to")] [SettingSource("Final rate", "The final speed to ramp to")]
public abstract BindableNumber<double> FinalRate { get; } public abstract BindableNumber<double> FinalRate { get; }
public override string SettingDescription => $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x";
private double finalRateTime; private double finalRateTime;
private double beginRampTime; private double beginRampTime;

View File

@ -38,6 +38,15 @@ namespace osu.Game.Rulesets.Objects.Drawables
private readonly Lazy<List<DrawableHitObject>> nestedHitObjects = new Lazy<List<DrawableHitObject>>(); private readonly Lazy<List<DrawableHitObject>> nestedHitObjects = new Lazy<List<DrawableHitObject>>();
public IReadOnlyList<DrawableHitObject> NestedHitObjects => nestedHitObjects.IsValueCreated ? nestedHitObjects.Value : (IReadOnlyList<DrawableHitObject>)Array.Empty<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> /// <summary>
/// Invoked when a <see cref="JudgementResult"/> has been applied by this <see cref="DrawableHitObject"/> or a nested <see cref="DrawableHitObject"/>. /// Invoked when a <see cref="JudgementResult"/> has been applied by this <see cref="DrawableHitObject"/> or a nested <see cref="DrawableHitObject"/>.
/// </summary> /// </summary>
@ -344,7 +353,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// Plays all the hit sounds for this <see cref="DrawableHitObject"/>. /// Plays all the hit sounds for this <see cref="DrawableHitObject"/>.
/// This is invoked automatically when this <see cref="DrawableHitObject"/> is hit. /// This is invoked automatically when this <see cref="DrawableHitObject"/> is hit.
/// </summary> /// </summary>
public void PlaySamples() => Samples?.Play(); public virtual void PlaySamples() => Samples?.Play();
protected override void Update() protected override void Update()
{ {

View File

@ -72,9 +72,9 @@ namespace osu.Game.Rulesets.UI
/// </summary> /// </summary>
public override Playfield Playfield => playfield.Value; public override Playfield Playfield => playfield.Value;
private Container overlays; public override Container Overlays { get; } = new Container { RelativeSizeAxes = Axes.Both };
public override Container Overlays => overlays; public override Container FrameStableComponents { get; } = new Container { RelativeSizeAxes = Axes.Both };
public override GameplayClock FrameStableClock => frameStabilityContainer.GameplayClock; public override GameplayClock FrameStableClock => frameStabilityContainer.GameplayClock;
@ -187,11 +187,12 @@ namespace osu.Game.Rulesets.UI
FrameStablePlayback = FrameStablePlayback, FrameStablePlayback = FrameStablePlayback,
Children = new Drawable[] Children = new Drawable[]
{ {
FrameStableComponents,
KeyBindingInputManager KeyBindingInputManager
.WithChild(CreatePlayfieldAdjustmentContainer() .WithChild(CreatePlayfieldAdjustmentContainer()
.WithChild(Playfield) .WithChild(Playfield)
), ),
overlays = new Container { RelativeSizeAxes = Axes.Both } Overlays,
} }
}, },
}; };
@ -406,10 +407,15 @@ namespace osu.Game.Rulesets.UI
public abstract Playfield Playfield { get; } public abstract Playfield Playfield { get; }
/// <summary> /// <summary>
/// Place to put drawables above hit objects but below UI. /// Content to be placed above hitobjects. Will be affected by frame stability.
/// </summary> /// </summary>
public abstract Container Overlays { get; } public abstract Container Overlays { get; }
/// <summary>
/// Components to be run potentially multiple times in line with frame-stable gameplay.
/// </summary>
public abstract Container FrameStableComponents { get; }
/// <summary> /// <summary>
/// The frame-stable clock which is being used for playfield display. /// The frame-stable clock which is being used for playfield display.
/// </summary> /// </summary>

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.UI
{ {
/// <summary> /// <summary>
/// A container which consumes a parent gameplay clock and standardises frame counts for children. /// A container which consumes a parent gameplay clock and standardises frame counts for children.
/// Will ensure a minimum of 40 frames per clock second is maintained, regardless of any system lag or seeks. /// Will ensure a minimum of 50 frames per clock second is maintained, regardless of any system lag or seeks.
/// </summary> /// </summary>
public class FrameStabilityContainer : Container, IHasReplayHandler public class FrameStabilityContainer : Container, IHasReplayHandler
{ {

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.UI
private readonly ModType type; private readonly ModType type;
public virtual string TooltipText { get; } public virtual string TooltipText => mod.IconTooltip;
private Mod mod; private Mod mod;
@ -48,8 +48,6 @@ namespace osu.Game.Rulesets.UI
type = mod.Type; type = mod.Type;
TooltipText = mod.Name;
Size = new Vector2(size); Size = new Vector2(size);
Children = new Drawable[] Children = new Drawable[]

View File

@ -28,10 +28,11 @@ namespace osu.Game.Scoring.Legacy
{ {
var score = new Score var score = new Score
{ {
ScoreInfo = new ScoreInfo(),
Replay = new Replay() Replay = new Replay()
}; };
WorkingBeatmap workingBeatmap;
using (SerializationReader sr = new SerializationReader(stream)) using (SerializationReader sr = new SerializationReader(stream))
{ {
currentRuleset = GetRuleset(sr.ReadByte()); currentRuleset = GetRuleset(sr.ReadByte());
@ -41,13 +42,10 @@ namespace osu.Game.Scoring.Legacy
var version = sr.ReadInt32(); var version = sr.ReadInt32();
var workingBeatmap = GetBeatmap(sr.ReadString()); workingBeatmap = GetBeatmap(sr.ReadString());
if (workingBeatmap is DummyWorkingBeatmap) if (workingBeatmap is DummyWorkingBeatmap)
throw new BeatmapNotFoundException(); throw new BeatmapNotFoundException();
currentBeatmap = workingBeatmap.Beatmap;
scoreInfo.Beatmap = currentBeatmap.BeatmapInfo;
scoreInfo.User = new User { Username = sr.ReadString() }; scoreInfo.User = new User { Username = sr.ReadString() };
// MD5Hash // MD5Hash
@ -68,6 +66,9 @@ namespace osu.Game.Scoring.Legacy
scoreInfo.Mods = currentRuleset.ConvertFromLegacyMods((LegacyMods)sr.ReadInt32()).ToArray(); scoreInfo.Mods = currentRuleset.ConvertFromLegacyMods((LegacyMods)sr.ReadInt32()).ToArray();
currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods);
scoreInfo.Beatmap = currentBeatmap.BeatmapInfo;
/* score.HpGraphString = */ /* score.HpGraphString = */
sr.ReadString(); sr.ReadString();
@ -113,6 +114,10 @@ namespace osu.Game.Scoring.Legacy
CalculateAccuracy(score.ScoreInfo); CalculateAccuracy(score.ScoreInfo);
// before returning for database import, we must restore the database-sourced BeatmapInfo.
// if not, the clone operation in GetPlayableBeatmap will cause a dereference and subsequent database exception.
score.ScoreInfo.Beatmap = workingBeatmap.BeatmapInfo;
return score; return score;
} }

View File

@ -2,8 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -16,8 +14,6 @@ namespace osu.Game.Screens.Play
{ {
public class BreakOverlay : Container public class BreakOverlay : Container
{ {
private readonly ScoreProcessor scoreProcessor;
/// <summary> /// <summary>
/// The duration of the break overlay fading. /// The duration of the break overlay fading.
/// </summary> /// </summary>
@ -37,10 +33,6 @@ namespace osu.Game.Screens.Play
{ {
breaks = value; breaks = value;
// reset index in case the new breaks list is smaller than last one
isBreakTime.Value = false;
CurrentBreakIndex = 0;
if (IsLoaded) if (IsLoaded)
initializeBreaks(); initializeBreaks();
} }
@ -48,27 +40,17 @@ namespace osu.Game.Screens.Play
public override bool RemoveCompletedTransforms => false; public override bool RemoveCompletedTransforms => false;
/// <summary>
/// Whether the gameplay is currently in a break.
/// </summary>
public IBindable<bool> IsBreakTime => isBreakTime;
protected int CurrentBreakIndex;
private readonly BindableBool isBreakTime = new BindableBool();
private readonly Container remainingTimeAdjustmentBox; private readonly Container remainingTimeAdjustmentBox;
private readonly Container remainingTimeBox; private readonly Container remainingTimeBox;
private readonly RemainingTimeCounter remainingTimeCounter; private readonly RemainingTimeCounter remainingTimeCounter;
private readonly BreakInfo info;
private readonly BreakArrows breakArrows; private readonly BreakArrows breakArrows;
private readonly double gameplayStartTime;
public BreakOverlay(bool letterboxing, double gameplayStartTime = 0, ScoreProcessor scoreProcessor = null) public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor)
{ {
this.gameplayStartTime = gameplayStartTime;
this.scoreProcessor = scoreProcessor;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
BreakInfo info;
Child = fadeContainer = new Container Child = fadeContainer = new Container
{ {
Alpha = 0, Alpha = 0,
@ -119,13 +101,11 @@ namespace osu.Game.Screens.Play
} }
}; };
if (scoreProcessor != null) bindProcessor(scoreProcessor); if (scoreProcessor != null)
} {
info.AccuracyDisplay.Current.BindTo(scoreProcessor.Accuracy);
[BackgroundDependencyLoader(true)] info.GradeDisplay.Current.BindTo(scoreProcessor.Rank);
private void load(GameplayClock clock) }
{
if (clock != null) Clock = clock;
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -134,42 +114,6 @@ namespace osu.Game.Screens.Play
initializeBreaks(); initializeBreaks();
} }
protected override void Update()
{
base.Update();
updateBreakTimeBindable();
}
private void updateBreakTimeBindable() =>
isBreakTime.Value = getCurrentBreak()?.HasEffect == true
|| Clock.CurrentTime < gameplayStartTime
|| scoreProcessor?.HasCompleted == true;
private BreakPeriod getCurrentBreak()
{
if (breaks?.Count > 0)
{
var time = Clock.CurrentTime;
if (time > breaks[CurrentBreakIndex].EndTime)
{
while (time > breaks[CurrentBreakIndex].EndTime && CurrentBreakIndex < breaks.Count - 1)
CurrentBreakIndex++;
}
else
{
while (time < breaks[CurrentBreakIndex].StartTime && CurrentBreakIndex > 0)
CurrentBreakIndex--;
}
var closest = breaks[CurrentBreakIndex];
return closest.Contains(time) ? closest : null;
}
return null;
}
private void initializeBreaks() private void initializeBreaks()
{ {
FinishTransforms(true); FinishTransforms(true);
@ -207,11 +151,5 @@ namespace osu.Game.Screens.Play
} }
} }
} }
private void bindProcessor(ScoreProcessor processor)
{
info.AccuracyDisplay.Current.BindTo(processor.Accuracy);
info.GradeDisplay.Current.BindTo(processor.Rank);
}
} }
} }

View File

@ -0,0 +1,82 @@
// 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 osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Screens.Play
{
public class BreakTracker : Component
{
private readonly ScoreProcessor scoreProcessor;
private readonly double gameplayStartTime;
/// <summary>
/// Whether the gameplay is currently in a break.
/// </summary>
public IBindable<bool> IsBreakTime => isBreakTime;
protected int CurrentBreakIndex;
private readonly BindableBool isBreakTime = new BindableBool();
private IReadOnlyList<BreakPeriod> breaks;
public IReadOnlyList<BreakPeriod> Breaks
{
get => breaks;
set
{
breaks = value;
// reset index in case the new breaks list is smaller than last one
isBreakTime.Value = false;
CurrentBreakIndex = 0;
}
}
public BreakTracker(double gameplayStartTime = 0, ScoreProcessor scoreProcessor = null)
{
this.gameplayStartTime = gameplayStartTime;
this.scoreProcessor = scoreProcessor;
}
protected override void Update()
{
base.Update();
isBreakTime.Value = getCurrentBreak()?.HasEffect == true
|| Clock.CurrentTime < gameplayStartTime
|| scoreProcessor?.HasCompleted == true;
}
private BreakPeriod getCurrentBreak()
{
if (breaks?.Count > 0)
{
var time = Clock.CurrentTime;
if (time > breaks[CurrentBreakIndex].EndTime)
{
while (time > breaks[CurrentBreakIndex].EndTime && CurrentBreakIndex < breaks.Count - 1)
CurrentBreakIndex++;
}
else
{
while (time < breaks[CurrentBreakIndex].StartTime && CurrentBreakIndex > 0)
CurrentBreakIndex--;
}
var closest = breaks[CurrentBreakIndex];
return closest.Contains(time) ? closest : null;
}
return null;
}
}
}

View File

@ -76,6 +76,8 @@ namespace osu.Game.Screens.Play
public BreakOverlay BreakOverlay; public BreakOverlay BreakOverlay;
private BreakTracker breakTracker;
protected ScoreProcessor ScoreProcessor { get; private set; } protected ScoreProcessor ScoreProcessor { get; private set; }
protected HealthProcessor HealthProcessor { get; private set; } protected HealthProcessor HealthProcessor { get; private set; }
@ -204,7 +206,7 @@ namespace osu.Game.Screens.Play
foreach (var mod in Mods.Value.OfType<IApplicableToHealthProcessor>()) foreach (var mod in Mods.Value.OfType<IApplicableToHealthProcessor>())
mod.ApplyToHealthProcessor(HealthProcessor); mod.ApplyToHealthProcessor(HealthProcessor);
BreakOverlay.IsBreakTime.BindValueChanged(onBreakTimeChanged, true); breakTracker.IsBreakTime.BindValueChanged(onBreakTimeChanged, true);
} }
private void addUnderlayComponents(Container target) private void addUnderlayComponents(Container target)
@ -231,12 +233,30 @@ namespace osu.Game.Screens.Play
DrawableRuleset, DrawableRuleset,
new ComboEffects(ScoreProcessor) new ComboEffects(ScoreProcessor)
}); });
DrawableRuleset.FrameStableComponents.AddRange(new Drawable[]
{
ScoreProcessor,
HealthProcessor,
breakTracker = new BreakTracker(DrawableRuleset.GameplayStartTime, ScoreProcessor)
{
Breaks = working.Beatmap.Breaks
}
});
HealthProcessor.IsBreakTime.BindTo(breakTracker.IsBreakTime);
} }
private void addOverlayComponents(Container target, WorkingBeatmap working) private void addOverlayComponents(Container target, WorkingBeatmap working)
{ {
target.AddRange(new[] target.AddRange(new[]
{ {
BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor)
{
Clock = DrawableRuleset.FrameStableClock,
ProcessCustomClock = false,
Breaks = working.Beatmap.Breaks
},
// display the cursor above some HUD elements. // display the cursor above some HUD elements.
DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), DrawableRuleset.Cursor?.CreateProxy() ?? new Container(),
DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(), DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(),
@ -293,20 +313,8 @@ namespace osu.Game.Screens.Play
performImmediateExit(); performImmediateExit();
}, },
}, },
failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, } failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, },
}); });
DrawableRuleset.Overlays.Add(BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, DrawableRuleset.GameplayStartTime, ScoreProcessor)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Breaks = working.Beatmap.Breaks
});
DrawableRuleset.Overlays.Add(ScoreProcessor);
DrawableRuleset.Overlays.Add(HealthProcessor);
HealthProcessor.IsBreakTime.BindTo(BreakOverlay.IsBreakTime);
} }
private void onBreakTimeChanged(ValueChangedEvent<bool> isBreakTime) private void onBreakTimeChanged(ValueChangedEvent<bool> isBreakTime)
@ -318,7 +326,7 @@ namespace osu.Game.Screens.Play
private void updatePauseOnFocusLostState() => private void updatePauseOnFocusLostState() =>
HUDOverlay.HoldToQuit.PauseOnFocusLost = PauseOnFocusLost HUDOverlay.HoldToQuit.PauseOnFocusLost = PauseOnFocusLost
&& !DrawableRuleset.HasReplayLoaded.Value && !DrawableRuleset.HasReplayLoaded.Value
&& !BreakOverlay.IsBreakTime.Value; && !breakTracker.IsBreakTime.Value;
private IBeatmap loadPlayableBeatmap() private IBeatmap loadPlayableBeatmap()
{ {
@ -540,7 +548,7 @@ namespace osu.Game.Screens.Play
PauseOverlay.Hide(); PauseOverlay.Hide();
// breaks and time-based conditions may allow instant resume. // breaks and time-based conditions may allow instant resume.
if (BreakOverlay.IsBreakTime.Value) if (breakTracker.IsBreakTime.Value)
completeResume(); completeResume();
else else
DrawableRuleset.RequestResume(completeResume); DrawableRuleset.RequestResume(completeResume);
@ -574,8 +582,8 @@ namespace osu.Game.Screens.Play
Background.BlurAmount.Value = 0; Background.BlurAmount.Value = 0;
// bind component bindables. // bind component bindables.
Background.IsBreakTime.BindTo(BreakOverlay.IsBreakTime); Background.IsBreakTime.BindTo(breakTracker.IsBreakTime);
DimmableStoryboard.IsBreakTime.BindTo(BreakOverlay.IsBreakTime); DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime);
Background.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); Background.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground);
DimmableStoryboard.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); DimmableStoryboard.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground);
@ -629,6 +637,39 @@ namespace osu.Game.Screens.Play
return base.OnExiting(next); return base.OnExiting(next);
} }
protected virtual void GotoRanking()
{
if (DrawableRuleset.ReplayScore != null)
{
// if a replay is present, we likely don't want to import into the local database.
this.Push(CreateResults(CreateScore()));
return;
}
LegacyByteArrayReader replayReader = null;
var score = new Score { ScoreInfo = CreateScore() };
if (recordingReplay?.Frames.Count > 0)
{
score.Replay = recordingReplay;
using (var stream = new MemoryStream())
{
new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream);
replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr");
}
}
scoreManager.Import(score.ScoreInfo, replayReader)
.ContinueWith(imported => Schedule(() =>
{
// screen may be in the exiting transition phase.
if (this.IsCurrentScreen())
this.Push(CreateResults(imported.Result));
}));
}
private void fadeOut(bool instant = false) private void fadeOut(bool instant = false)
{ {
float fadeOutDuration = instant ? 0 : 250; float fadeOutDuration = instant ? 0 : 250;
@ -641,36 +682,7 @@ namespace osu.Game.Screens.Play
private void scheduleGotoRanking() private void scheduleGotoRanking()
{ {
completionProgressDelegate?.Cancel(); completionProgressDelegate?.Cancel();
completionProgressDelegate = Schedule(delegate completionProgressDelegate = Schedule(GotoRanking);
{
if (DrawableRuleset.ReplayScore != null)
this.Push(CreateResults(DrawableRuleset.ReplayScore.ScoreInfo));
else
{
var score = new Score { ScoreInfo = CreateScore() };
LegacyByteArrayReader replayReader = null;
if (recordingReplay?.Frames.Count > 0)
{
score.Replay = recordingReplay;
using (var stream = new MemoryStream())
{
new LegacyScoreEncoder(score, gameplayBeatmap).Encode(stream);
replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr");
}
}
scoreManager.Import(score.ScoreInfo, replayReader)
.ContinueWith(imported => Schedule(() =>
{
// screen may be in the exiting transition phase.
if (this.IsCurrentScreen())
this.Push(CreateResults(imported.Result));
}));
}
});
} }
#endregion #endregion

View File

@ -1,7 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Screens;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
@ -23,6 +25,13 @@ namespace osu.Game.Screens.Play
DrawableRuleset?.SetReplayScore(score); DrawableRuleset?.SetReplayScore(score);
} }
protected override void GotoRanking()
{
this.Push(CreateResults(DrawableRuleset.ReplayScore.ScoreInfo));
}
protected override ResultsScreen CreateResults(ScoreInfo score) => new ResultsScreen(score, false);
protected override ScoreInfo CreateScore() => score.ScoreInfo; protected override ScoreInfo CreateScore() => score.ScoreInfo;
} }
} }

View File

@ -31,23 +31,28 @@ namespace osu.Game.Screens.Ranking
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private Player player { get; set; } private Player player { get; set; }
private readonly ScoreInfo score; public readonly ScoreInfo Score;
private readonly bool allowRetry;
private Drawable bottomPanel; private Drawable bottomPanel;
public ResultsScreen(ScoreInfo score) public ResultsScreen(ScoreInfo score, bool allowRetry = true)
{ {
this.score = score; Score = score;
this.allowRetry = allowRetry;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
FillFlowContainer buttons;
InternalChildren = new[] InternalChildren = new[]
{ {
new ResultsScrollContainer new ResultsScrollContainer
{ {
Child = new ScorePanel(score) Child = new ScorePanel(Score)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -68,7 +73,7 @@ namespace osu.Game.Screens.Ranking
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#333") Colour = Color4Extensions.FromHex("#333")
}, },
new FillFlowContainer buttons = new FillFlowContainer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -77,16 +82,17 @@ namespace osu.Game.Screens.Ranking
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Children = new Drawable[] Children = new Drawable[]
{ {
new ReplayDownloadButton(score) { Width = 300 }, new ReplayDownloadButton(Score) { Width = 300 },
new RetryButton { Width = 300 },
} }
} }
} }
} }
}; };
if (player != null) if (player != null && allowRetry)
{ {
buttons.Add(new RetryButton { Width = 300 });
AddInternal(new HotkeyRetryOverlay AddInternal(new HotkeyRetryOverlay
{ {
Action = () => Action = () =>

View File

@ -253,46 +253,37 @@ namespace osu.Game.Screens.Select
/// <param name="skipDifficulties">Whether to skip individual difficulties and only increment over full groups.</param> /// <param name="skipDifficulties">Whether to skip individual difficulties and only increment over full groups.</param>
public void SelectNext(int direction = 1, bool skipDifficulties = true) public void SelectNext(int direction = 1, bool skipDifficulties = true)
{ {
var visibleItems = Items.Where(s => !s.Item.Filtered.Value).ToList(); if (beatmapSets.All(s => s.Filtered.Value))
if (!visibleItems.Any())
return; return;
DrawableCarouselItem drawable = null; if (skipDifficulties)
selectNextSet(direction, true);
else
selectNextDifficulty(direction);
}
if (selectedBeatmap != null && (drawable = selectedBeatmap.Drawables.FirstOrDefault()) == null) private void selectNextSet(int direction, bool skipDifficulties)
// if the selected beatmap isn't present yet, we can't correctly change selection. {
// we can fix this by changing this method to not reference drawables / Items in the first place. var unfilteredSets = beatmapSets.Where(s => !s.Filtered.Value).ToList();
return;
int originalIndex = visibleItems.IndexOf(drawable); var nextSet = unfilteredSets[(unfilteredSets.IndexOf(selectedBeatmapSet) + direction + unfilteredSets.Count) % unfilteredSets.Count];
int currentIndex = originalIndex;
// local function to increment the index in the required direction, wrapping over extremities. if (skipDifficulties)
int incrementIndex() => currentIndex = (currentIndex + direction + visibleItems.Count) % visibleItems.Count; select(nextSet);
else
select(direction > 0 ? nextSet.Beatmaps.First(b => !b.Filtered.Value) : nextSet.Beatmaps.Last(b => !b.Filtered.Value));
}
while (incrementIndex() != originalIndex) private void selectNextDifficulty(int direction)
{ {
var item = visibleItems[currentIndex].Item; var unfilteredDifficulties = selectedBeatmapSet.Children.Where(s => !s.Filtered.Value).ToList();
if (item.Filtered.Value || item.State.Value == CarouselItemState.Selected) continue; int index = unfilteredDifficulties.IndexOf(selectedBeatmap);
switch (item) if (index + direction < 0 || index + direction >= unfilteredDifficulties.Count)
{ selectNextSet(direction, false);
case CarouselBeatmap beatmap: else
if (skipDifficulties) continue; select(unfilteredDifficulties[index + direction]);
select(beatmap);
return;
case CarouselBeatmapSet set:
if (skipDifficulties)
select(set);
else
select(direction > 0 ? set.Beatmaps.First(b => !b.Filtered.Value) : set.Beatmaps.Last(b => !b.Filtered.Value));
return;
}
}
} }
/// <summary> /// <summary>

View File

@ -9,6 +9,8 @@ namespace osu.Game.Skinning
{ {
public class LegacyBeatmapSkin : LegacySkin public class LegacyBeatmapSkin : LegacySkin
{ {
protected override bool AllowManiaSkin => false;
public LegacyBeatmapSkin(BeatmapInfo beatmap, IResourceStore<byte[]> storage, AudioManager audioManager) public LegacyBeatmapSkin(BeatmapInfo beatmap, IResourceStore<byte[]> storage, AudioManager audioManager)
: base(createSkinInfo(beatmap), new LegacySkinResourceStore<BeatmapSetFileInfo>(beatmap.BeatmapSet, storage), audioManager, beatmap.Path) : base(createSkinInfo(beatmap), new LegacySkinResourceStore<BeatmapSetFileInfo>(beatmap.BeatmapSet, storage), audioManager, beatmap.Path)
{ {

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,23 @@
// 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.Skinning
{
public class LegacyManiaSkinConfigurationLookup
{
public readonly int Keys;
public readonly LegacyManiaSkinConfigurationLookups Lookup;
public readonly int? TargetColumn;
public LegacyManiaSkinConfigurationLookup(int keys, LegacyManiaSkinConfigurationLookups lookup, int? targetColumn = null)
{
Keys = keys;
Lookup = lookup;
TargetColumn = targetColumn;
}
}
public enum LegacyManiaSkinConfigurationLookups
{
}
}

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;
}
}
}
}

View File

@ -26,12 +26,16 @@ namespace osu.Game.Skinning
[CanBeNull] [CanBeNull]
protected IResourceStore<SampleChannel> Samples; protected IResourceStore<SampleChannel> Samples;
protected virtual bool AllowManiaSkin => true;
public new LegacySkinConfiguration Configuration public new LegacySkinConfiguration Configuration
{ {
get => base.Configuration as LegacySkinConfiguration; get => base.Configuration as LegacySkinConfiguration;
set => base.Configuration = value; set => base.Configuration = value;
} }
private readonly Dictionary<int, LegacyManiaSkinConfiguration> maniaConfigurations = new Dictionary<int, LegacyManiaSkinConfiguration>();
public LegacySkin(SkinInfo skin, IResourceStore<byte[]> storage, AudioManager audioManager) public LegacySkin(SkinInfo skin, IResourceStore<byte[]> storage, AudioManager audioManager)
: this(skin, new LegacySkinResourceStore<SkinFileInfo>(skin, storage), audioManager, "skin.ini") : this(skin, new LegacySkinResourceStore<SkinFileInfo>(skin, storage), audioManager, "skin.ini")
{ {
@ -40,15 +44,26 @@ namespace osu.Game.Skinning
protected LegacySkin(SkinInfo skin, IResourceStore<byte[]> storage, AudioManager audioManager, string filename) protected LegacySkin(SkinInfo skin, IResourceStore<byte[]> storage, AudioManager audioManager, string filename)
: base(skin) : base(skin)
{ {
Stream stream = storage?.GetStream(filename); using (var stream = storage?.GetStream(filename))
if (stream != null)
{ {
using (LineBufferedReader reader = new LineBufferedReader(stream)) if (stream != null)
Configuration = new LegacySkinDecoder().Decode(reader); {
using (LineBufferedReader reader = new LineBufferedReader(stream, true))
Configuration = new LegacySkinDecoder().Decode(reader);
stream.Seek(0, SeekOrigin.Begin);
using (LineBufferedReader reader = new LineBufferedReader(stream))
{
var maniaList = new LegacyManiaSkinDecoder().Decode(reader);
foreach (var config in maniaList)
maniaConfigurations[config.Keys] = config;
}
}
else
Configuration = new LegacySkinConfiguration { LegacyVersion = LegacySkinConfiguration.LATEST_VERSION };
} }
else
Configuration = new LegacySkinConfiguration { LegacyVersion = LegacySkinConfiguration.LATEST_VERSION };
if (storage != null) if (storage != null)
{ {
@ -105,6 +120,15 @@ namespace osu.Game.Skinning
case SkinCustomColourLookup customColour: case SkinCustomColourLookup customColour:
return SkinUtils.As<TValue>(getCustomColour(customColour.Lookup.ToString())); return SkinUtils.As<TValue>(getCustomColour(customColour.Lookup.ToString()));
case LegacyManiaSkinConfigurationLookup legacy:
if (!AllowManiaSkin)
return null;
if (!maniaConfigurations.TryGetValue(legacy.Keys, out _))
maniaConfigurations[legacy.Keys] = new LegacyManiaSkinConfiguration(legacy.Keys);
break;
default: default:
// handles lookups like GlobalSkinConfiguration // handles lookups like GlobalSkinConfiguration

View File

@ -50,7 +50,7 @@ namespace osu.Game.Storyboards.Drawables
AddInternal(Content = new Container<DrawableStoryboardLayer> AddInternal(Content = new Container<DrawableStoryboardLayer>
{ {
Size = new Vector2(640, 480), RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}); });

View File

@ -8,7 +8,7 @@ using osu.Framework.Graphics.Containers;
namespace osu.Game.Storyboards.Drawables namespace osu.Game.Storyboards.Drawables
{ {
public class DrawableStoryboardLayer : LifetimeManagementContainer public class DrawableStoryboardLayer : CompositeDrawable
{ {
public StoryboardLayer Layer { get; } public StoryboardLayer Layer { get; }
public bool Enabled; public bool Enabled;
@ -23,17 +23,34 @@ namespace osu.Game.Storyboards.Drawables
Origin = Anchor.Centre; Origin = Anchor.Centre;
Enabled = layer.VisibleWhenPassing; Enabled = layer.VisibleWhenPassing;
Masking = layer.Masking; Masking = layer.Masking;
InternalChild = new LayerElementContainer(layer);
} }
[BackgroundDependencyLoader] private class LayerElementContainer : LifetimeManagementContainer
private void load(CancellationToken? cancellationToken)
{ {
foreach (var element in Layer.Elements) private readonly StoryboardLayer storyboardLayer;
{
cancellationToken?.ThrowIfCancellationRequested();
if (element.IsDrawable) public LayerElementContainer(StoryboardLayer layer)
AddInternal(element.CreateDrawable()); {
storyboardLayer = layer;
Width = 640;
Height = 480;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
}
[BackgroundDependencyLoader]
private void load(CancellationToken? cancellationToken)
{
foreach (var element in storyboardLayer.Elements)
{
cancellationToken?.ThrowIfCancellationRequested();
if (element.IsDrawable)
AddInternal(element.CreateDrawable());
}
} }
} }
} }

View File

@ -55,6 +55,8 @@ namespace osu.Game.Storyboards.Drawables
{ {
base.LoadComplete(); base.LoadComplete();
if (videoSprite == null) return;
using (videoSprite.BeginAbsoluteSequence(0)) using (videoSprite.BeginAbsoluteSequence(0))
videoSprite.FadeIn(500); videoSprite.FadeIn(500);
} }

View File

@ -26,16 +26,16 @@ namespace osu.Game.Tests.Visual
protected OsuManualInputManagerTestScene() protected OsuManualInputManagerTestScene()
{ {
MenuCursorContainer cursorContainer;
base.Content.AddRange(new Drawable[] base.Content.AddRange(new Drawable[]
{ {
InputManager = new ManualInputManager InputManager = new ManualInputManager
{ {
UseParentInput = true, UseParentInput = true,
Child = new GlobalActionContainer(null) Child = new GlobalActionContainer(null)
{ .WithChild((cursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both })
RelativeSizeAxes = Axes.Both, .WithChild(content = new OsuTooltipContainer(cursorContainer.Cursor) { RelativeSizeAxes = Axes.Both }))
Child = content = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }
},
}, },
new Container new Container
{ {