1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-13 02:32:55 +08:00

Merge branch 'master' into game-overlay-activation-mode

This commit is contained in:
Dan Balasescu 2020-09-03 13:34:22 +09:00 committed by GitHub
commit 84b3604192
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
132 changed files with 1490 additions and 717 deletions

View File

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

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Skinning namespace osu.Game.Rulesets.Catch.Skinning
{ {
@ -61,7 +62,12 @@ namespace osu.Game.Rulesets.Catch.Skinning
switch (lookup) switch (lookup)
{ {
case CatchSkinColour colour: case CatchSkinColour colour:
return Source.GetConfig<SkinCustomColourLookup, TValue>(new SkinCustomColourLookup(colour)); var result = (Bindable<Color4>)Source.GetConfig<SkinCustomColourLookup, TValue>(new SkinCustomColourLookup(colour));
if (result == null)
return null;
result.Value = LegacyColourCompatibility.DisallowZeroAlpha(result.Value);
return (IBindable<TValue>)result;
} }
return Source.GetConfig<TLookup, TValue>(lookup); return Source.GetConfig<TLookup, TValue>(lookup);

View File

@ -40,7 +40,6 @@ namespace osu.Game.Rulesets.Catch.Skinning
colouredSprite = new Sprite colouredSprite = new Sprite
{ {
Texture = skin.GetTexture(lookupName), Texture = skin.GetTexture(lookupName),
Colour = drawableObject.AccentColour.Value,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}, },
@ -76,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Skinning
{ {
base.LoadComplete(); base.LoadComplete();
accentColour.BindValueChanged(colour => colouredSprite.Colour = colour.NewValue, true); accentColour.BindValueChanged(colour => colouredSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
} }
} }
} }

View File

@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })]
[TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime), typeof(ManiaModPerfect) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime), typeof(ManiaModPerfect) })]
[TestCase(LegacyMods.Random | LegacyMods.SuddenDeath, new[] { typeof(ManiaModRandom), typeof(ManiaModSuddenDeath) })] [TestCase(LegacyMods.Random | LegacyMods.SuddenDeath, new[] { typeof(ManiaModRandom), typeof(ManiaModSuddenDeath) })]
[TestCase(LegacyMods.Flashlight | LegacyMods.Mirror, new[] { typeof(ManiaModFlashlight), typeof(ManiaModMirror) })]
public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods); public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods);
protected override Ruleset CreateRuleset() => new ManiaRuleset(); protected override Ruleset CreateRuleset() => new ManiaRuleset();

View File

@ -22,18 +22,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
[Cached] [Cached]
private readonly Column column; private readonly Column column;
public ColumnTestContainer(int column, ManiaAction action) public ColumnTestContainer(int column, ManiaAction action, bool showColumn = false)
{ {
this.column = new Column(column) InternalChildren = new[]
{ {
Action = { Value = action }, this.column = new Column(column)
AccentColour = Color4.Orange, {
ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd Action = { Value = action },
}; AccentColour = Color4.Orange,
ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd,
InternalChild = content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4) Alpha = showColumn ? 1 : 0
{ },
RelativeSizeAxes = Axes.Both content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4)
{
RelativeSizeAxes = Axes.Both
},
this.column.TopLevelContainer.CreateProxy()
}; };
} }
} }

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Children = new Drawable[] Children = new Drawable[]
{ {
new ColumnTestContainer(0, ManiaAction.Key1) new ColumnTestContainer(0, ManiaAction.Key1, true)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
})); }));
}) })
}, },
new ColumnTestContainer(1, ManiaAction.Key2) new ColumnTestContainer(1, ManiaAction.Key2, true)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -0,0 +1,117 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
public class TestSceneOutOfOrderHits : RateAdjustedBeatmapTestScene
{
[Test]
public void TestPreviousHitWindowDoesNotExtendPastNextObject()
{
var objects = new List<ManiaHitObject>();
var frames = new List<ReplayFrame>();
for (int i = 0; i < 7; i++)
{
double time = 1000 + i * 100;
objects.Add(new Note { StartTime = time });
if (i > 0)
{
frames.Add(new ManiaReplayFrame(time + 10, ManiaAction.Key1));
frames.Add(new ManiaReplayFrame(time + 11));
}
}
performTest(objects, frames);
addJudgementAssert(objects[0], HitResult.Miss);
for (int i = 1; i < 7; i++)
{
addJudgementAssert(objects[i], HitResult.Perfect);
addJudgementOffsetAssert(objects[i], 10);
}
}
private void addJudgementAssert(ManiaHitObject hitObject, HitResult result)
{
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
() => judgementResults.Single(r => r.HitObject == hitObject).Type == result);
}
private void addJudgementOffsetAssert(ManiaHitObject hitObject, double offset)
{
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}",
() => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100));
}
private ScoreAccessibleReplayPlayer currentPlayer;
private List<JudgementResult> judgementResults;
private void performTest(List<ManiaHitObject> hitObjects, List<ReplayFrame> frames)
{
AddStep("load player", () =>
{
Beatmap.Value = CreateWorkingBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 })
{
HitObjects = hitObjects,
BeatmapInfo =
{
Ruleset = new ManiaRuleset().RulesetInfo
},
});
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>
{
p.ScoreProcessor.NewJudgement += result =>
{
if (currentPlayer == p) judgementResults.Add(result);
};
};
LoadScreen(currentPlayer = p);
judgementResults = new List<JudgementResult>();
});
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
}
private class ScoreAccessibleReplayPlayer : ReplayPlayer
{
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
protected override bool PauseOnFocusLost => false;
public ScoreAccessibleReplayPlayer(Score score)
: base(score, false, false)
{
}
}
}
}

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -126,6 +126,9 @@ namespace osu.Game.Rulesets.Mania
if (mods.HasFlag(LegacyMods.Random)) if (mods.HasFlag(LegacyMods.Random))
yield return new ManiaModRandom(); yield return new ManiaModRandom();
if (mods.HasFlag(LegacyMods.Mirror))
yield return new ManiaModMirror();
} }
public override LegacyMods ConvertToLegacyMods(Mod[] mods) public override LegacyMods ConvertToLegacyMods(Mod[] mods)
@ -175,6 +178,10 @@ namespace osu.Game.Rulesets.Mania
case ManiaModFadeIn _: case ManiaModFadeIn _:
value |= LegacyMods.FadeIn; value |= LegacyMods.FadeIn;
break; break;
case ManiaModMirror _:
value |= LegacyMods.Mirror;
break;
} }
} }
@ -326,6 +333,16 @@ namespace osu.Game.Rulesets.Mania
Height = 250 Height = 250
}), }),
} }
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
new UnstableRate(score.HitEvents)
}))
}
} }
}; };
} }

View File

@ -238,7 +238,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected override void CheckForResult(bool userTriggered, double timeOffset) protected override void CheckForResult(bool userTriggered, double timeOffset)
{ {
if (Tail.AllJudged) if (Tail.AllJudged)
{
ApplyResult(r => r.Type = HitResult.Perfect); ApplyResult(r => r.Type = HitResult.Perfect);
endHold();
}
if (Tail.Result.Type == HitResult.Miss) if (Tail.Result.Type == HitResult.Miss)
HasBroken = true; HasBroken = true;
@ -252,6 +255,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (action != Action.Value) if (action != Action.Value)
return false; return false;
if (CheckHittable?.Invoke(this, Time.Current) == false)
return false;
// The tail has a lenience applied to it which is factored into the miss window (i.e. the miss judgement will be delayed). // The tail has a lenience applied to it which is factored into the miss window (i.e. the miss judgement will be delayed).
// But the hold cannot ever be started within the late-lenience window, so we should skip trying to begin the hold during that time. // But the hold cannot ever be started within the late-lenience window, so we should skip trying to begin the hold during that time.
// Note: Unlike below, we use the tail's start time to determine the time offset. // Note: Unlike below, we use the tail's start time to determine the time offset.

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 JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -8,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects.Drawables namespace osu.Game.Rulesets.Mania.Objects.Drawables
{ {
@ -34,6 +36,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
} }
} }
/// <summary>
/// Whether this <see cref="DrawableManiaHitObject"/> can be hit, given a time value.
/// If non-null, judgements will be ignored whilst the function returns false.
/// </summary>
public Func<DrawableHitObject, double, bool> CheckHittable;
protected DrawableManiaHitObject(ManiaHitObject hitObject) protected DrawableManiaHitObject(ManiaHitObject hitObject)
: base(hitObject) : base(hitObject)
{ {
@ -124,6 +132,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
break; break;
} }
} }
/// <summary>
/// Causes this <see cref="DrawableManiaHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
/// </summary>
public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss);
} }
public abstract class DrawableManiaHitObject<TObject> : DrawableManiaHitObject public abstract class DrawableManiaHitObject<TObject> : DrawableManiaHitObject

View File

@ -64,6 +64,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (action != Action.Value) if (action != Action.Value)
return false; return false;
if (CheckHittable?.Invoke(this, Time.Current) == false)
return false;
return UpdateResult(true); return UpdateResult(true);
} }

View File

@ -0,0 +1,46 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mania.Skinning
{
public class HitTargetInsetContainer : Container
{
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
protected override Container<Drawable> Content => content;
private readonly Container content;
private float hitPosition;
public HitTargetInsetContainer()
{
RelativeSizeAxes = Axes.Both;
InternalChild = content = new Container { RelativeSizeAxes = Axes.Both };
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
{
hitPosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? Stage.HIT_TARGET_POSITION;
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
}
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
{
content.Padding = direction.NewValue == ScrollingDirection.Up
? new MarginPadding { Top = hitPosition }
: new MarginPadding { Bottom = hitPosition };
}
}
}

View File

@ -1,6 +1,8 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -19,7 +21,14 @@ namespace osu.Game.Rulesets.Mania.Skinning
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>(); private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private readonly IBindable<bool> isHitting = new Bindable<bool>(); private readonly IBindable<bool> isHitting = new Bindable<bool>();
private Drawable sprite; [CanBeNull]
private Drawable bodySprite;
[CanBeNull]
private Drawable lightContainer;
[CanBeNull]
private Drawable light;
public LegacyBodyPiece() public LegacyBodyPiece()
{ {
@ -32,7 +41,39 @@ namespace osu.Game.Rulesets.Mania.Skinning
string imageName = GetColumnSkinConfig<string>(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value string imageName = GetColumnSkinConfig<string>(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value
?? $"mania-note{FallbackColumnIndex}L"; ?? $"mania-note{FallbackColumnIndex}L";
sprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d => string lightImage = GetColumnSkinConfig<string>(skin, LegacyManiaSkinConfigurationLookups.HoldNoteLightImage)?.Value
?? "lightingL";
float lightScale = GetColumnSkinConfig<float>(skin, LegacyManiaSkinConfigurationLookups.HoldNoteLightScale)?.Value
?? 1;
// Create a temporary animation to retrieve the number of frames, in an effort to calculate the intended frame length.
// This animation is discarded and re-queried with the appropriate frame length afterwards.
var tmp = skin.GetAnimation(lightImage, true, false);
double frameLength = 0;
if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0)
frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount);
light = skin.GetAnimation(lightImage, true, true, frameLength: frameLength).With(d =>
{
if (d == null)
return;
d.Origin = Anchor.Centre;
d.Blending = BlendingParameters.Additive;
d.Scale = new Vector2(lightScale);
});
if (light != null)
{
lightContainer = new HitTargetInsetContainer
{
Alpha = 0,
Child = light
};
}
bodySprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d =>
{ {
if (d == null) if (d == null)
return; return;
@ -47,8 +88,8 @@ namespace osu.Game.Rulesets.Mania.Skinning
// Todo: Wrap // Todo: Wrap
}); });
if (sprite != null) if (bodySprite != null)
InternalChild = sprite; InternalChild = bodySprite;
direction.BindTo(scrollingInfo.Direction); direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true); direction.BindValueChanged(onDirectionChanged, true);
@ -60,28 +101,68 @@ namespace osu.Game.Rulesets.Mania.Skinning
private void onIsHittingChanged(ValueChangedEvent<bool> isHitting) private void onIsHittingChanged(ValueChangedEvent<bool> isHitting)
{ {
if (!(sprite is TextureAnimation animation)) if (bodySprite is TextureAnimation bodyAnimation)
{
bodyAnimation.GotoFrame(0);
bodyAnimation.IsPlaying = isHitting.NewValue;
}
if (lightContainer == null)
return; return;
animation.GotoFrame(0); if (isHitting.NewValue)
animation.IsPlaying = isHitting.NewValue; {
// Clear the fade out and, more importantly, the removal.
lightContainer.ClearTransforms();
// Only add the container if the removal has taken place.
if (lightContainer.Parent == null)
Column.TopLevelContainer.Add(lightContainer);
// The light must be seeked only after being loaded, otherwise a nullref occurs (https://github.com/ppy/osu-framework/issues/3847).
if (light is TextureAnimation lightAnimation)
lightAnimation.GotoFrame(0);
lightContainer.FadeIn(80);
}
else
{
lightContainer.FadeOut(120)
.OnComplete(d => Column.TopLevelContainer.Remove(d));
}
} }
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction) private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
{ {
if (sprite == null)
return;
if (direction.NewValue == ScrollingDirection.Up) if (direction.NewValue == ScrollingDirection.Up)
{ {
sprite.Origin = Anchor.BottomCentre; if (bodySprite != null)
sprite.Scale = new Vector2(1, -1); {
bodySprite.Origin = Anchor.BottomCentre;
bodySprite.Scale = new Vector2(1, -1);
}
if (light != null)
light.Anchor = Anchor.TopCentre;
} }
else else
{ {
sprite.Origin = Anchor.TopCentre; if (bodySprite != null)
sprite.Scale = Vector2.One; {
bodySprite.Origin = Anchor.TopCentre;
bodySprite.Scale = Vector2.One;
}
if (light != null)
light.Anchor = Anchor.BottomCentre;
} }
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
lightContainer?.Expire();
}
} }
} }

View File

@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
{ {
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
Colour = lightColour, Colour = LegacyColourCompatibility.DisallowZeroAlpha(lightColour),
Texture = skin.GetTexture(lightImage), Texture = skin.GetTexture(lightImage),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Width = 1, Width = 1,

View File

@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Mania.Skinning
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = 1, Height = 1,
Colour = lineColour, Colour = LegacyColourCompatibility.DisallowZeroAlpha(lineColour),
Alpha = showJudgementLine ? 0.9f : 0 Alpha = showJudgementLine ? 0.9f : 0
} }
} }

View File

@ -65,6 +65,9 @@ namespace osu.Game.Rulesets.Mania.Skinning
direction.BindTo(scrollingInfo.Direction); direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true); direction.BindValueChanged(onDirectionChanged, true);
if (GetColumnSkinConfig<bool>(skin, LegacyManiaSkinConfigurationLookups.KeysUnderNotes)?.Value ?? false)
Column.UnderlayElements.Add(CreateProxy());
} }
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction) private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)

View File

@ -2,14 +2,12 @@
// 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.Allocation; 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;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -108,75 +106,43 @@ namespace osu.Game.Rulesets.Mania.Skinning
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new Container LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both
Child = new Box }, backgroundColour),
{
RelativeSizeAxes = Axes.Both,
Colour = backgroundColour
},
},
new HitTargetInsetContainer new HitTargetInsetContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new[] Children = new[]
{ {
new Box new Container
{ {
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Width = leftLineWidth, Width = leftLineWidth,
Scale = new Vector2(0.740f, 1), Scale = new Vector2(0.740f, 1),
Colour = lineColour, Alpha = hasLeftLine ? 1 : 0,
Alpha = hasLeftLine ? 1 : 0 Child = LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box
{
RelativeSizeAxes = Axes.Both
}, lineColour)
}, },
new Box new Container
{ {
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Width = rightLineWidth, Width = rightLineWidth,
Scale = new Vector2(0.740f, 1), Scale = new Vector2(0.740f, 1),
Colour = lineColour, Alpha = hasRightLine ? 1 : 0,
Alpha = hasRightLine ? 1 : 0 Child = LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box
{
RelativeSizeAxes = Axes.Both
}, lineColour)
}, },
} }
} }
}; };
} }
} }
private class HitTargetInsetContainer : Container
{
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
protected override Container<Drawable> Content => content;
private readonly Container content;
private float hitPosition;
public HitTargetInsetContainer()
{
RelativeSizeAxes = Axes.Both;
InternalChild = content = new Container { RelativeSizeAxes = Axes.Both };
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin, IScrollingInfo scrollingInfo)
{
hitPosition = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? Stage.HIT_TARGET_POSITION;
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
}
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
{
content.Padding = direction.NewValue == ScrollingDirection.Up
? new MarginPadding { Top = hitPosition }
: new MarginPadding { Bottom = hitPosition };
}
}
} }
} }

View File

@ -17,6 +17,7 @@ using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.UI namespace osu.Game.Rulesets.Mania.UI
{ {
@ -36,6 +37,7 @@ namespace osu.Game.Rulesets.Mania.UI
public readonly ColumnHitObjectArea HitObjectArea; public readonly ColumnHitObjectArea HitObjectArea;
internal readonly Container TopLevelContainer; internal readonly Container TopLevelContainer;
private readonly DrawablePool<PoolableHitExplosion> hitExplosionPool; private readonly DrawablePool<PoolableHitExplosion> hitExplosionPool;
private readonly OrderedHitPolicy hitPolicy;
public Container UnderlayElements => HitObjectArea.UnderlayElements; public Container UnderlayElements => HitObjectArea.UnderlayElements;
@ -65,6 +67,8 @@ namespace osu.Game.Rulesets.Mania.UI
TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both } TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
}; };
hitPolicy = new OrderedHitPolicy(HitObjectContainer);
TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy()); TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy());
} }
@ -90,6 +94,9 @@ namespace osu.Game.Rulesets.Mania.UI
hitObject.AccentColour.Value = AccentColour; hitObject.AccentColour.Value = AccentColour;
hitObject.OnNewResult += OnNewResult; hitObject.OnNewResult += OnNewResult;
DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)hitObject;
maniaObject.CheckHittable = hitPolicy.IsHittable;
HitObjectContainer.Add(hitObject); HitObjectContainer.Add(hitObject);
} }
@ -104,6 +111,9 @@ namespace osu.Game.Rulesets.Mania.UI
internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result)
{ {
if (result.IsHit)
hitPolicy.HandleHit(judgedObject);
if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value) if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value)
return; return;

View File

@ -0,0 +1,78 @@
// 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 osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania.UI
{
/// <summary>
/// Ensures that only the most recent <see cref="HitObject"/> is hittable, affectionately known as "note lock".
/// </summary>
public class OrderedHitPolicy
{
private readonly HitObjectContainer hitObjectContainer;
public OrderedHitPolicy(HitObjectContainer hitObjectContainer)
{
this.hitObjectContainer = hitObjectContainer;
}
/// <summary>
/// Determines whether a <see cref="DrawableHitObject"/> can be hit at a point in time.
/// </summary>
/// <remarks>
/// Only the most recent <see cref="DrawableHitObject"/> can be hit, a previous hitobject's window cannot extend past the next one.
/// </remarks>
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to check.</param>
/// <param name="time">The time to check.</param>
/// <returns>Whether <paramref name="hitObject"/> can be hit at the given <paramref name="time"/>.</returns>
public bool IsHittable(DrawableHitObject hitObject, double time)
{
var nextObject = hitObjectContainer.AliveObjects.GetNext(hitObject);
return nextObject == null || time < nextObject.HitObject.StartTime;
}
/// <summary>
/// Handles a <see cref="HitObject"/> being hit to potentially miss all earlier <see cref="HitObject"/>s.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> that was hit.</param>
public void HandleHit(DrawableHitObject hitObject)
{
if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset))
throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!");
foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
{
if (obj.Judged)
continue;
((DrawableManiaHitObject)obj).MissForcefully();
}
}
private IEnumerable<DrawableHitObject> enumerateHitObjectsUpTo(double targetTime)
{
foreach (var obj in hitObjectContainer.AliveObjects)
{
if (obj.HitObject.GetEndTime() >= targetTime)
yield break;
yield return obj;
foreach (var nestedObj in obj.NestedHitObjects)
{
if (nestedObj.HitObject.GetEndTime() >= targetTime)
break;
yield return nestedObj;
}
}
}
}
}

View File

@ -79,7 +79,6 @@ namespace osu.Game.Rulesets.Mania.UI
columnFlow = new ColumnFlow<Column>(definition) columnFlow = new ColumnFlow<Column>(definition)
{ {
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Padding = new MarginPadding { Left = COLUMN_SPACING, Right = COLUMN_SPACING },
}, },
new Container new Container
{ {

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -22,7 +22,6 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using osu.Game.Storyboards; using osu.Game.Storyboards;
using osuTK; using osuTK;
using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
{ {
@ -32,8 +31,6 @@ namespace osu.Game.Rulesets.Osu.Tests
[Resolved] [Resolved]
private AudioManager audioManager { get; set; } private AudioManager audioManager { get; set; }
private TrackVirtualManual track;
protected override bool Autoplay => autoplay; protected override bool Autoplay => autoplay;
private bool autoplay; private bool autoplay;
@ -44,11 +41,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private const double fade_in_modifier = -1200; private const double fade_in_modifier = -1200;
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{ => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
track = (TrackVirtualManual)working.Track;
return working;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(RulesetConfigCache configCache) private void load(RulesetConfigCache configCache)
@ -72,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
AddStep("enable autoplay", () => autoplay = true); AddStep("enable autoplay", () => autoplay = true);
base.SetUpSteps(); base.SetUpSteps();
AddUntilStep("wait for track to start running", () => track.IsRunning); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
double startTime = hitObjects[sliderIndex].StartTime; double startTime = hitObjects[sliderIndex].StartTime;
retrieveDrawableSlider(sliderIndex); retrieveDrawableSlider(sliderIndex);
@ -97,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
AddStep("have autoplay", () => autoplay = true); AddStep("have autoplay", () => autoplay = true);
base.SetUpSteps(); base.SetUpSteps();
AddUntilStep("wait for track to start running", () => track.IsRunning); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
double startTime = hitObjects[sliderIndex].StartTime; double startTime = hitObjects[sliderIndex].StartTime;
retrieveDrawableSlider(sliderIndex); retrieveDrawableSlider(sliderIndex);
@ -201,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void addSeekStep(double time) private void addSeekStep(double time)
{ {
AddStep($"seek to {time}", () => track.Seek(time)); AddStep($"seek to {time}", () => MusicController.SeekTo(time));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
} }

View File

@ -9,6 +9,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
{ {
@ -62,7 +63,8 @@ namespace osu.Game.Rulesets.Osu.Tests
drawableSpinner = new TestDrawableSpinner(spinner, auto) drawableSpinner = new TestDrawableSpinner(spinner, auto)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Depth = depthIndex++ Depth = depthIndex++,
Scale = new Vector2(0.75f)
}; };
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>()) foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObjects>())

View File

@ -25,7 +25,6 @@ using osu.Game.Scoring;
using osu.Game.Storyboards; using osu.Game.Storyboards;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
{ {
@ -34,18 +33,12 @@ namespace osu.Game.Rulesets.Osu.Tests
[Resolved] [Resolved]
private AudioManager audioManager { get; set; } private AudioManager audioManager { get; set; }
private TrackVirtualManual track;
protected override bool Autoplay => true; protected override bool Autoplay => true;
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer(); protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer();
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{ => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
track = (TrackVirtualManual)working.Track;
return working;
}
private DrawableSpinner drawableSpinner; private DrawableSpinner drawableSpinner;
private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType<SpriteIcon>().Single(); private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType<SpriteIcon>().Single();
@ -55,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
base.SetUpSteps(); base.SetUpSteps();
AddUntilStep("wait for track to start running", () => track.IsRunning); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
AddStep("retrieve spinner", () => drawableSpinner = (DrawableSpinner)Player.DrawableRuleset.Playfield.AllHitObjects.First()); AddStep("retrieve spinner", () => drawableSpinner = (DrawableSpinner)Player.DrawableRuleset.Playfield.AllHitObjects.First());
} }
@ -201,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Tests
addSeekStep(0); addSeekStep(0);
AddStep("adjust track rate", () => track.AddAdjustment(AdjustableProperty.Tempo, new BindableDouble(rate))); AddStep("adjust track rate", () => MusicController.CurrentTrack.AddAdjustment(AdjustableProperty.Tempo, new BindableDouble(rate)));
// autoplay replay frames use track time; // autoplay replay frames use track time;
// if a spin takes 1000ms in track time and we're playing with a 2x rate adjustment, the spin will take 500ms of *real* time. // if a spin takes 1000ms in track time and we're playing with a 2x rate adjustment, the spin will take 500ms of *real* time.
// therefore we need to apply the rate adjustment to the replay itself to change from track time to real time, // therefore we need to apply the rate adjustment to the replay itself to change from track time to real time,
@ -230,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void addSeekStep(double time) private void addSeekStep(double time)
{ {
AddStep($"seek to {time}", () => track.Seek(time)); AddStep($"seek to {time}", () => MusicController.SeekTo(time));
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
} }

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override float SamplePlaybackPosition => HitObject.X / OsuPlayfield.BASE_SIZE.X; protected override float SamplePlaybackPosition => HitObject.X / OsuPlayfield.BASE_SIZE.X;
/// <summary> /// <summary>
/// Whether this <see cref="DrawableOsuHitObject"/> can be hit. /// Whether this <see cref="DrawableOsuHitObject"/> can be hit, given a time value.
/// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false. /// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false.
/// </summary> /// </summary>
public Func<DrawableHitObject, double, bool> CheckHittable; public Func<DrawableHitObject, double, bool> CheckHittable;

View File

@ -193,30 +193,46 @@ namespace osu.Game.Rulesets.Osu
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{ {
new StatisticRow var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList();
return new[]
{ {
Columns = new[] new StatisticRow
{ {
new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList()) Columns = new[]
{ {
RelativeSizeAxes = Axes.X, new StatisticItem("Timing Distribution",
Height = 250 new HitEventTimingDistributionGraph(timedHitEvents)
}), {
} RelativeSizeAxes = Axes.X,
}, Height = 250
new StatisticRow }),
{ }
Columns = new[] },
new StatisticRow
{ {
new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap) Columns = new[]
{ {
RelativeSizeAxes = Axes.X, new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap)
Height = 250 {
}), RelativeSizeAxes = Axes.X,
Height = 250
}),
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
new UnstableRate(timedHitEvents)
}))
}
} }
} };
}; }
} }
} }

View File

@ -154,8 +154,12 @@ namespace osu.Game.Rulesets.Osu.Replays
// The startPosition for the slider should not be its .Position, but the point on the circle whose tangent crosses the current cursor position // The startPosition for the slider should not be its .Position, but the point on the circle whose tangent crosses the current cursor position
// We also modify spinnerDirection so it spins in the direction it enters the spin circle, to make a smooth transition. // We also modify spinnerDirection so it spins in the direction it enters the spin circle, to make a smooth transition.
// TODO: Shouldn't the spinner always spin in the same direction? // TODO: Shouldn't the spinner always spin in the same direction?
if (h is Spinner) if (h is Spinner spinner)
{ {
// spinners with 0 spins required will auto-complete - don't bother
if (spinner.SpinsRequired == 0)
return;
calcSpinnerStartPosAndDirection(((OsuReplayFrame)Frames[^1]).Position, out startPosition, out spinnerDirection); calcSpinnerStartPosAndDirection(((OsuReplayFrame)Frames[^1]).Position, out startPosition, out spinnerDirection);
Vector2 spinCentreOffset = SPINNER_CENTRE - ((OsuReplayFrame)Frames[^1]).Position; Vector2 spinCentreOffset = SPINNER_CENTRE - ((OsuReplayFrame)Frames[^1]).Position;

View File

@ -59,7 +59,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
hitCircleSprite = new Sprite hitCircleSprite = new Sprite
{ {
Texture = getTextureWithFallback(string.Empty), Texture = getTextureWithFallback(string.Empty),
Colour = drawableObject.AccentColour.Value,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}, },
@ -107,7 +106,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
base.LoadComplete(); base.LoadComplete();
state.BindValueChanged(updateState, true); state.BindValueChanged(updateState, true);
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true); accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
} }

View File

@ -22,11 +22,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
private DrawableSpinner drawableSpinner; private DrawableSpinner drawableSpinner;
private Sprite disc; private Sprite disc;
private Sprite metreSprite;
private Container metre; private Container metre;
private const float background_y_offset = 20;
private const float sprite_scale = 1 / 1.6f; private const float sprite_scale = 1 / 1.6f;
private const float final_metre_height = 692 * sprite_scale;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISkinSource source, DrawableHitObject drawableObject) private void load(ISkinSource source, DrawableHitObject drawableObject)
@ -35,50 +35,58 @@ namespace osu.Game.Rulesets.Osu.Skinning
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[] InternalChild = new Container
{ {
new Sprite // the old-style spinner relied heavily on absolute screen-space coordinate values.
// wrap everything in a container simulating absolute coords to preserve alignment
// as there are skins that depend on it.
Width = 640,
Height = 480,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{ {
Anchor = Anchor.BottomCentre, new Sprite
Origin = Anchor.BottomCentre,
Texture = source.GetTexture("spinner-background"),
Y = background_y_offset,
Scale = new Vector2(sprite_scale)
},
disc = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-circle"),
Scale = new Vector2(sprite_scale)
},
metre = new Container
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Y = background_y_offset,
Masking = true,
Child = new Sprite
{ {
Texture = source.GetTexture("spinner-metre"), Anchor = Anchor.Centre,
Anchor = Anchor.BottomCentre, Origin = Anchor.Centre,
Origin = Anchor.BottomCentre, Texture = source.GetTexture("spinner-background"),
Scale = new Vector2(sprite_scale)
}, },
Scale = new Vector2(0.625f) disc = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-circle"),
Scale = new Vector2(sprite_scale)
},
metre = new Container
{
AutoSizeAxes = Axes.Both,
// this anchor makes no sense, but that's what stable uses.
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
// adjustment for stable (metre has additional offset)
Margin = new MarginPadding { Top = 20 },
Masking = true,
Child = metreSprite = new Sprite
{
Texture = source.GetTexture("spinner-metre"),
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Scale = new Vector2(0.625f)
}
}
} }
}; };
} }
private Vector2 metreFinalSize;
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
this.FadeOut(); this.FadeOut();
drawableSpinner.State.BindValueChanged(updateStateTransforms, true); drawableSpinner.State.BindValueChanged(updateStateTransforms, true);
metreFinalSize = metre.Size = metre.Child.Size;
} }
private void updateStateTransforms(ValueChangedEvent<ArmedState> state) private void updateStateTransforms(ValueChangedEvent<ArmedState> state)
@ -93,7 +101,16 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
base.Update(); base.Update();
disc.Rotation = drawableSpinner.RotationTracker.Rotation; disc.Rotation = drawableSpinner.RotationTracker.Rotation;
metre.Height = getMetreHeight(drawableSpinner.Progress);
// careful: need to call this exactly once for all calculations in a frame
// as the function has a random factor in it
var metreHeight = getMetreHeight(drawableSpinner.Progress);
// hack to make the metre blink up from below than down from above.
// move down the container to be able to apply masking for the metre,
// and then move the sprite back up the same amount to keep its position absolute.
metre.Y = final_metre_height - metreHeight;
metreSprite.Y = -metre.Y;
} }
private const int total_bars = 10; private const int total_bars = 10;
@ -108,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
if (RNG.NextBool(((int)progress % 10) / 10f)) if (RNG.NextBool(((int)progress % 10) / 10f))
barCount++; barCount++;
return (float)barCount / total_bars * metreFinalSize.Y; return (float)barCount / total_bars * final_metre_height;
} }
} }
} }

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISkinSource skin, DrawableHitObject drawableObject) private void load(ISkinSource skin, DrawableHitObject drawableObject)
{ {
animationContent.Colour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBall)?.Value ?? Color4.White; var ballColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBall)?.Value ?? Color4.White;
InternalChildren = new[] InternalChildren = new[]
{ {
@ -39,11 +39,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
Texture = skin.GetTexture("sliderb-nd"), Texture = skin.GetTexture("sliderb-nd"),
Colour = new Color4(5, 5, 5, 255), Colour = new Color4(5, 5, 5, 255),
}, },
animationContent.With(d => LegacyColourCompatibility.ApplyWithDoubledAlpha(animationContent.With(d =>
{ {
d.Anchor = Anchor.Centre; d.Anchor = Anchor.Centre;
d.Origin = Anchor.Centre; d.Origin = Anchor.Centre;
}), }), ballColour),
layerSpec = new Sprite layerSpec = new Sprite
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,

View File

@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
private void updateAccentColour() private void updateAccentColour()
{ {
backgroundLayer.Colour = accentColour; backgroundLayer.Colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour);
} }
} }
} }

View File

@ -76,9 +76,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning
private void updateAccentColour() private void updateAccentColour()
{ {
headCircle.AccentColour = accentColour; var colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour);
body.Colour = accentColour;
end.Colour = accentColour; headCircle.AccentColour = colour;
body.Colour = colour;
end.Colour = colour;
} }
} }
} }

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 osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game.Skinning;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.Skinning namespace osu.Game.Rulesets.Taiko.Skinning
@ -18,9 +19,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
AccentColour = component == TaikoSkinComponents.CentreHit AccentColour = LegacyColourCompatibility.DisallowZeroAlpha(
? new Color4(235, 69, 44, 255) component == TaikoSkinComponents.CentreHit
: new Color4(67, 142, 172, 255); ? new Color4(235, 69, 44, 255)
: new Color4(67, 142, 172, 255));
} }
} }
} }

View File

@ -161,19 +161,34 @@ namespace osu.Game.Rulesets.Taiko
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{ {
new StatisticRow var timedHitEvents = score.HitEvents.Where(e => e.HitObject is Hit).ToList();
return new[]
{ {
Columns = new[] new StatisticRow
{ {
new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is Hit).ToList()) Columns = new[]
{ {
RelativeSizeAxes = Axes.X, new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(timedHitEvents)
Height = 250 {
}), RelativeSizeAxes = Axes.X,
Height = 250
}),
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
new UnstableRate(timedHitEvents)
}))
}
} }
} };
}; }
} }
} }

View File

@ -0,0 +1,32 @@
// 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.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Tests.Beatmaps
{
[TestFixture]
public class BeatmapDifficultyManagerTest
{
[Test]
public void TestKeyEqualsWithDifferentModInstances()
{
var key1 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
var key2 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
Assert.That(key1, Is.EqualTo(key2));
}
[Test]
public void TestKeyEqualsWithDifferentModOrder()
{
var key1 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() });
var key2 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHidden(), new OsuModHardRock() });
Assert.That(key1, Is.EqualTo(key2));
}
}
}

View File

@ -106,7 +106,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
protected override Texture GetBackground() => throw new NotImplementedException(); protected override Texture GetBackground() => throw new NotImplementedException();
protected override Track GetTrack() => throw new NotImplementedException(); protected override Track GetBeatmapTrack() => throw new NotImplementedException();
} }
} }
} }

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.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Beatmaps.Formats
{
[TestFixture]
public class LegacyScoreDecoderTest
{
[Test]
public void TestDecodeManiaReplay()
{
var decoder = new TestLegacyScoreDecoder();
using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr"))
{
var score = decoder.Parse(resourceStream);
Assert.AreEqual(3, score.ScoreInfo.Ruleset.ID);
Assert.AreEqual(2, score.ScoreInfo.Statistics[HitResult.Great]);
Assert.AreEqual(1, score.ScoreInfo.Statistics[HitResult.Good]);
Assert.AreEqual(829_931, score.ScoreInfo.TotalScore);
Assert.AreEqual(3, score.ScoreInfo.MaxCombo);
Assert.IsTrue(Precision.AlmostEquals(0.8889, score.ScoreInfo.Accuracy, 0.0001));
Assert.AreEqual(ScoreRank.B, score.ScoreInfo.Rank);
Assert.That(score.Replay.Frames, Is.Not.Empty);
}
}
private class TestLegacyScoreDecoder : LegacyScoreDecoder
{
private static readonly Dictionary<int, Ruleset> rulesets = new Ruleset[]
{
new OsuRuleset(),
new TaikoRuleset(),
new CatchRuleset(),
new ManiaRuleset()
}.ToDictionary(ruleset => ((ILegacyRuleset)ruleset).LegacyID);
protected override Ruleset GetRuleset(int rulesetId) => rulesets[rulesetId];
protected override WorkingBeatmap GetBeatmap(string md5Hash) => new TestWorkingBeatmap(new Beatmap
{
BeatmapInfo = new BeatmapInfo
{
MD5Hash = md5Hash,
Ruleset = new OsuRuleset().RulesetInfo,
BaseDifficulty = new BeatmapDifficulty()
}
});
}
}
}

View File

@ -1,10 +1,8 @@
// 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 NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -19,7 +17,14 @@ namespace osu.Game.Tests.Gameplay
{ {
GameplayClockContainer gcc = null; GameplayClockContainer gcc = null;
AddStep("create container", () => Add(gcc = new GameplayClockContainer(CreateWorkingBeatmap(new OsuRuleset().RulesetInfo), Array.Empty<Mod>(), 0))); AddStep("create container", () =>
{
var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack();
Add(gcc = new GameplayClockContainer(working, 0));
});
AddStep("start track", () => gcc.Start()); AddStep("start track", () => gcc.Start());
AddUntilStep("elapsed greater than zero", () => gcc.GameplayClock.ElapsedFrameTime > 0); AddUntilStep("elapsed greater than zero", () => gcc.GameplayClock.ElapsedFrameTime > 0);
} }

View File

@ -59,7 +59,10 @@ namespace osu.Game.Tests.Gameplay
AddStep("create container", () => AddStep("create container", () =>
{ {
Add(gameplayContainer = new GameplayClockContainer(CreateWorkingBeatmap(new OsuRuleset().RulesetInfo), Array.Empty<Mod>(), 0)); var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
working.LoadTrack();
Add(gameplayContainer = new GameplayClockContainer(working, 0));
gameplayContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1)) gameplayContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1))
{ {
@ -103,7 +106,7 @@ namespace osu.Game.Tests.Gameplay
Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio); Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio);
SelectedMods.Value = new[] { testedMod }; SelectedMods.Value = new[] { testedMod };
Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, SelectedMods.Value, 0)); Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, 0));
gameplayContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1)) gameplayContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1))
{ {

View File

@ -0,0 +1,43 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Tests.NonVisual.Ranking
{
[TestFixture]
public class UnstableRateTest
{
[Test]
public void TestDistributedHits()
{
var events = Enumerable.Range(-5, 11)
.Select(t => new HitEvent(t - 5, HitResult.Great, new HitObject(), null, null));
var unstableRate = new UnstableRate(events);
Assert.IsTrue(Precision.AlmostEquals(unstableRate.Value, 10 * Math.Sqrt(10)));
}
[Test]
public void TestMissesAndEmptyWindows()
{
var events = new[]
{
new HitEvent(-100, HitResult.Miss, new HitObject(), null, null),
new HitEvent(0, HitResult.Great, new HitObject(), null, null),
new HitEvent(200, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null),
};
var unstableRate = new UnstableRate(events);
Assert.AreEqual(0, unstableRate.Value);
}
}
}

Binary file not shown.

View File

@ -1,5 +0,0 @@
[General]
Version: latest
[Colours]
Combo1: 255,255,255,0

View File

@ -108,15 +108,5 @@ namespace osu.Game.Tests.Skins
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))
Assert.That(decoder.Decode(stream).LegacyVersion, Is.EqualTo(1.0m)); Assert.That(decoder.Decode(stream).LegacyVersion, Is.EqualTo(1.0m));
} }
[Test]
public void TestDecodeColourWithZeroAlpha()
{
var decoder = new LegacySkinDecoder();
using (var resStream = TestResources.OpenResource("skin-zero-alpha-colour.ini"))
using (var stream = new LineBufferedReader(resStream))
Assert.That(decoder.Decode(stream).ComboColours[0].A, Is.EqualTo(1.0f));
}
} }
} }

View File

@ -26,6 +26,7 @@ namespace osu.Game.Tests.Skins
{ {
var imported = beatmaps.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-beatmap.osz"))).Result; var imported = beatmaps.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-beatmap.osz"))).Result;
beatmap = beatmaps.GetWorkingBeatmap(imported.Beatmaps[0]); beatmap = beatmaps.GetWorkingBeatmap(imported.Beatmaps[0]);
beatmap.LoadTrack();
} }
[Test] [Test]

View File

@ -4,7 +4,6 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
@ -18,8 +17,6 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public class TestSceneCompletionCancellation : OsuPlayerTestScene public class TestSceneCompletionCancellation : OsuPlayerTestScene
{ {
private Track track;
[Resolved] [Resolved]
private AudioManager audio { get; set; } private AudioManager audio { get; set; }
@ -34,7 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay
base.SetUpSteps(); base.SetUpSteps();
// Ensure track has actually running before attempting to seek // Ensure track has actually running before attempting to seek
AddUntilStep("wait for track to start running", () => track.IsRunning); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
} }
[Test] [Test]
@ -73,13 +70,13 @@ namespace osu.Game.Tests.Visual.Gameplay
private void complete() private void complete()
{ {
AddStep("seek to completion", () => track.Seek(5000)); AddStep("seek to completion", () => Beatmap.Value.Track.Seek(5000));
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
} }
private void cancel() private void cancel()
{ {
AddStep("rewind to cancel", () => track.Seek(4000)); AddStep("rewind to cancel", () => Beatmap.Value.Track.Seek(4000));
AddUntilStep("completion cleared by processor", () => !Player.ScoreProcessor.HasCompleted.Value); AddUntilStep("completion cleared by processor", () => !Player.ScoreProcessor.HasCompleted.Value);
} }
@ -91,11 +88,7 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{ => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audio);
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audio);
track = working.Track;
return working;
}
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{ {

View File

@ -5,7 +5,6 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -21,19 +20,13 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved] [Resolved]
private AudioManager audioManager { get; set; } private AudioManager audioManager { get; set; }
private Track track; protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) =>
new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
track = working.Track;
return working;
}
[Test] [Test]
public void TestNoJudgementsOnRewind() public void TestNoJudgementsOnRewind()
{ {
AddUntilStep("wait for track to start running", () => track.IsRunning); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
addSeekStep(3000); addSeekStep(3000);
AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged)); AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged));
AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses >= 7)); AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses >= 7));
@ -46,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void addSeekStep(double time) private void addSeekStep(double time)
{ {
AddStep($"seek to {time}", () => track.Seek(time)); AddStep($"seek to {time}", () => Beatmap.Value.Track.Seek(time));
// Allow a few frames of lenience // Allow a few frames of lenience
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100));

View File

@ -1,11 +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 System;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osuTK; using osuTK;
@ -32,7 +30,10 @@ namespace osu.Game.Tests.Visual.Gameplay
requestCount = 0; requestCount = 0;
increment = skip_time; increment = skip_time;
Child = gameplayClockContainer = new GameplayClockContainer(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), Array.Empty<Mod>(), 0) var working = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo));
working.LoadTrack();
Child = gameplayClockContainer = new GameplayClockContainer(working, 0)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]

View File

@ -22,19 +22,32 @@ namespace osu.Game.Tests.Visual.Gameplay
[TestFixture] [TestFixture]
public class TestSceneStoryboard : OsuTestScene public class TestSceneStoryboard : OsuTestScene
{ {
private readonly Container<DrawableStoryboard> storyboardContainer; private Container<DrawableStoryboard> storyboardContainer;
private DrawableStoryboard storyboard; private DrawableStoryboard storyboard;
[Cached] [Test]
private MusicController musicController = new MusicController(); public void TestStoryboard()
{
AddStep("Restart", restart);
AddToggleStep("Passing", passing =>
{
if (storyboard != null) storyboard.Passing = passing;
});
}
public TestSceneStoryboard() [Test]
public void TestStoryboardMissingVideo()
{
AddStep("Load storyboard with missing video", loadStoryboardNoVideo);
}
[BackgroundDependencyLoader]
private void load()
{ {
Clock = new FramedClock(); Clock = new FramedClock();
AddRange(new Drawable[] AddRange(new Drawable[]
{ {
musicController,
new Container new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -58,32 +71,11 @@ namespace osu.Game.Tests.Visual.Gameplay
State = { Value = Visibility.Visible }, State = { Value = Visibility.Visible },
} }
}); });
Beatmap.BindValueChanged(beatmapChanged, true);
} }
[Test] private void beatmapChanged(ValueChangedEvent<WorkingBeatmap> e) => loadStoryboard(e.NewValue);
public void TestStoryboard()
{
AddStep("Restart", restart);
AddToggleStep("Passing", passing =>
{
if (storyboard != null) storyboard.Passing = passing;
});
}
[Test]
public void TestStoryboardMissingVideo()
{
AddStep("Load storyboard with missing video", loadStoryboardNoVideo);
}
[BackgroundDependencyLoader]
private void load()
{
Beatmap.ValueChanged += beatmapChanged;
}
private void beatmapChanged(ValueChangedEvent<WorkingBeatmap> e)
=> loadStoryboard(e.NewValue);
private void restart() private void restart()
{ {

View File

@ -2,8 +2,8 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Audio.Track;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
namespace osu.Game.Tests.Visual.Menus namespace osu.Game.Tests.Visual.Menus
@ -15,11 +15,9 @@ namespace osu.Game.Tests.Visual.Menus
public TestSceneIntroWelcome() public TestSceneIntroWelcome()
{ {
AddUntilStep("wait for load", () => getTrack() != null); AddUntilStep("wait for load", () => MusicController.TrackLoaded);
AddAssert("correct track", () => Precision.AlmostEquals(MusicController.CurrentTrack.Length, 48000, 1));
AddAssert("check if menu music loops", () => getTrack().Looping); AddAssert("check if menu music loops", () => MusicController.CurrentTrack.Looping);
} }
private Track getTrack() => (IntroStack?.CurrentScreen as MainMenu)?.Track;
} }
} }

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -11,14 +10,10 @@ namespace osu.Game.Tests.Visual.Menus
{ {
public class TestSceneSongTicker : OsuTestScene public class TestSceneSongTicker : OsuTestScene
{ {
[Cached]
private MusicController musicController = new MusicController();
public TestSceneSongTicker() public TestSceneSongTicker()
{ {
AddRange(new Drawable[] AddRange(new Drawable[]
{ {
musicController,
new SongTicker new SongTicker
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,

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.Audio.Track;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -46,7 +45,6 @@ namespace osu.Game.Tests.Visual.Navigation
Player player = null; Player player = null;
WorkingBeatmap beatmap() => Game.Beatmap.Value; WorkingBeatmap beatmap() => Game.Beatmap.Value;
Track track() => beatmap().Track;
PushAndConfirm(() => new TestSongSelect()); PushAndConfirm(() => new TestSongSelect());
@ -62,30 +60,27 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null);
AddUntilStep("wait for fail", () => player.HasFailed); AddUntilStep("wait for fail", () => player.HasFailed);
AddUntilStep("wait for track stop", () => !track().IsRunning); AddUntilStep("wait for track stop", () => !Game.MusicController.IsPlaying);
AddAssert("Ensure time before preview point", () => track().CurrentTime < beatmap().Metadata.PreviewTime); AddAssert("Ensure time before preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().Metadata.PreviewTime);
pushEscape(); pushEscape();
AddUntilStep("wait for track playing", () => track().IsRunning); AddUntilStep("wait for track playing", () => Game.MusicController.IsPlaying);
AddAssert("Ensure time wasn't reset to preview point", () => track().CurrentTime < beatmap().Metadata.PreviewTime); AddAssert("Ensure time wasn't reset to preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().Metadata.PreviewTime);
} }
[Test] [Test]
public void TestMenuMakesMusic() public void TestMenuMakesMusic()
{ {
WorkingBeatmap beatmap() => Game.Beatmap.Value;
Track track() => beatmap().Track;
TestSongSelect songSelect = null; TestSongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestSongSelect()); PushAndConfirm(() => songSelect = new TestSongSelect());
AddUntilStep("wait for no track", () => track() is TrackVirtual); AddUntilStep("wait for no track", () => Game.MusicController.CurrentTrack.IsDummyDevice);
AddStep("return to menu", () => songSelect.Exit()); AddStep("return to menu", () => songSelect.Exit());
AddUntilStep("wait for track", () => !(track() is TrackVirtual) && track().IsRunning); AddUntilStep("wait for track", () => !Game.MusicController.CurrentTrack.IsDummyDevice && Game.MusicController.IsPlaying);
} }
[Test] [Test]
@ -140,12 +135,12 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("Wait for music controller", () => Game.MusicController.IsLoaded); AddUntilStep("Wait for music controller", () => Game.MusicController.IsLoaded);
AddStep("Seek close to end", () => AddStep("Seek close to end", () =>
{ {
Game.MusicController.SeekTo(Game.Beatmap.Value.Track.Length - 1000); Game.MusicController.SeekTo(Game.MusicController.CurrentTrack.Length - 1000);
Game.Beatmap.Value.Track.Completed += () => trackCompleted = true; Game.MusicController.CurrentTrack.Completed += () => trackCompleted = true;
}); });
AddUntilStep("Track was completed", () => trackCompleted); AddUntilStep("Track was completed", () => trackCompleted);
AddUntilStep("Track was restarted", () => Game.Beatmap.Value.Track.IsRunning); AddUntilStep("Track was restarted", () => Game.MusicController.IsPlaying);
} }
private void pushEscape() => private void pushEscape() =>

View File

@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
AddStep("Set activity", () => API.Activity.Value = new UserActivity.InLobby()); AddStep("Set activity", () => API.Activity.Value = new UserActivity.InLobby());
AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(null, null) AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null)
{ {
BeatmapInfo = { OnlineBeatmapID = hasOnlineId ? 1234 : (int?)null } BeatmapInfo = { OnlineBeatmapID = hasOnlineId ? 1234 : (int?)null }
}); });

View File

@ -35,6 +35,18 @@ namespace osu.Game.Tests.Visual.Ranking
createTest(new List<HitEvent>()); createTest(new List<HitEvent>());
} }
[Test]
public void TestMissesDontShow()
{
createTest(Enumerable.Range(0, 100).Select(i =>
{
if (i % 2 == 0)
return new HitEvent(0, HitResult.Perfect, new HitCircle(), new HitCircle(), null);
return new HitEvent(30, HitResult.Miss, new HitCircle(), new HitCircle(), null);
}).ToList());
}
private void createTest(List<HitEvent> events) => AddStep("create test", () => private void createTest(List<HitEvent> events) => AddStep("create test", () =>
{ {
Children = new Drawable[] Children = new Drawable[]

View File

@ -13,6 +13,7 @@ using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -212,6 +213,25 @@ namespace osu.Game.Tests.Visual.Ranking
AddAssert("expanded panel still on screen", () => this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded).ScreenSpaceDrawQuad.TopLeft.X > 0); AddAssert("expanded panel still on screen", () => this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded).ScreenSpaceDrawQuad.TopLeft.X > 0);
} }
[Test]
public void TestDownloadButtonInitiallyDisabled()
{
TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
AddAssert("download button is disabled", () => !screen.ChildrenOfType<DownloadButton>().Single().Enabled.Value);
AddStep("click contracted panel", () =>
{
var contractedPanel = this.ChildrenOfType<ScorePanel>().First(p => p.State == PanelState.Contracted && p.ScreenSpaceDrawQuad.TopLeft.X > screen.ScreenSpaceDrawQuad.TopLeft.X);
InputManager.MoveMouseTo(contractedPanel);
InputManager.Click(MouseButton.Left);
});
AddAssert("download button is enabled", () => screen.ChildrenOfType<DownloadButton>().Single().Enabled.Value);
}
private class TestResultsContainer : Container private class TestResultsContainer : Container
{ {
[Cached(typeof(Player))] [Cached(typeof(Player))]
@ -255,6 +275,7 @@ namespace osu.Game.Tests.Visual.Ranking
{ {
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo); var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
score.TotalScore += 10 - i; score.TotalScore += 10 - i;
score.Hash = $"test{i}";
scores.Add(score); scores.Add(score);
} }

View File

@ -13,7 +13,7 @@ using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Tests.Visual.Ranking namespace osu.Game.Tests.Visual.Ranking
{ {
public class TestSceneSimpleStatisticRow : OsuTestScene public class TestSceneSimpleStatisticTable : OsuTestScene
{ {
private Container container; private Container container;
@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Ranking
public void TestEmpty() public void TestEmpty()
{ {
AddStep("create with no items", AddStep("create with no items",
() => container.Add(new SimpleStatisticRow(2, Enumerable.Empty<SimpleStatisticItem>()))); () => container.Add(new SimpleStatisticTable(2, Enumerable.Empty<SimpleStatisticItem>())));
} }
[Test] [Test]
@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual.Ranking
Value = RNG.Next(100) Value = RNG.Next(100)
}); });
container.Add(new SimpleStatisticRow(columnCount, items)); container.Add(new SimpleStatisticTable(columnCount, items));
}); });
} }
} }

View File

@ -26,9 +26,6 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
private readonly NowPlayingOverlay np; private readonly NowPlayingOverlay np;
[Cached]
private MusicController musicController = new MusicController();
public TestSceneBeatSyncedContainer() public TestSceneBeatSyncedContainer()
{ {
Clock = new FramedClock(); Clock = new FramedClock();
@ -36,7 +33,6 @@ namespace osu.Game.Tests.Visual.UserInterface
AddRange(new Drawable[] AddRange(new Drawable[]
{ {
musicController,
new BeatContainer new BeatContainer
{ {
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
@ -71,6 +67,9 @@ namespace osu.Game.Tests.Visual.UserInterface
private readonly Box flashLayer; private readonly Box flashLayer;
[Resolved]
private MusicController musicController { get; set; }
public BeatContainer() public BeatContainer()
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
@ -165,7 +164,7 @@ namespace osu.Game.Tests.Visual.UserInterface
if (timingPoints.Count == 0) return 0; if (timingPoints.Count == 0) return 0;
if (timingPoints[^1] == current) if (timingPoints[^1] == current)
return (int)Math.Ceiling((Beatmap.Value.Track.Length - current.Time) / current.BeatLength); return (int)Math.Ceiling((musicController.CurrentTrack.Length - current.Time) / current.BeatLength);
return (int)Math.Ceiling((getNextTimingPoint(current).Time - current.Time) / current.BeatLength); return (int)Math.Ceiling((getNextTimingPoint(current).Time - current.Time) / current.BeatLength);
} }

View File

@ -6,6 +6,7 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -79,7 +80,7 @@ namespace osu.Game.Tests.Visual.UserInterface
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory)); dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory));
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, Audio, dependencies.Get<GameHost>(), Beatmap.Default)); dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get<AudioManager>(), dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory)); dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory));
beatmap = beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Result.Beatmaps[0]; beatmap = beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Result.Beatmaps[0];

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 NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
@ -11,6 +12,7 @@ using osu.Game.Beatmaps;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
{ {
@ -20,8 +22,6 @@ namespace osu.Game.Tests.Visual.UserInterface
[Cached] [Cached]
private MusicController musicController = new MusicController(); private MusicController musicController = new MusicController();
private WorkingBeatmap currentBeatmap;
private NowPlayingOverlay nowPlayingOverlay; private NowPlayingOverlay nowPlayingOverlay;
private RulesetStore rulesets; private RulesetStore rulesets;
@ -76,16 +76,21 @@ namespace osu.Game.Tests.Visual.UserInterface
} }
}).Wait(), 5); }).Wait(), 5);
AddStep(@"Next track", () => musicController.NextTrack()); WorkingBeatmap currentBeatmap = null;
AddStep("Store track", () => currentBeatmap = Beatmap.Value);
AddStep("import beatmap with track", () =>
{
var setWithTrack = manager.Import(TestResources.GetTestBeatmapForImport()).Result;
Beatmap.Value = currentBeatmap = manager.GetWorkingBeatmap(setWithTrack.Beatmaps.First());
});
AddStep(@"Seek track to 6 second", () => musicController.SeekTo(6000)); AddStep(@"Seek track to 6 second", () => musicController.SeekTo(6000));
AddUntilStep(@"Wait for current time to update", () => currentBeatmap.Track.CurrentTime > 5000); AddUntilStep(@"Wait for current time to update", () => musicController.CurrentTrack.CurrentTime > 5000);
AddStep(@"Set previous", () => musicController.PreviousTrack()); AddStep(@"Set previous", () => musicController.PreviousTrack());
AddAssert(@"Check beatmap didn't change", () => currentBeatmap == Beatmap.Value); AddAssert(@"Check beatmap didn't change", () => currentBeatmap == Beatmap.Value);
AddUntilStep("Wait for current time to update", () => currentBeatmap.Track.CurrentTime < 5000); AddUntilStep("Wait for current time to update", () => musicController.CurrentTrack.CurrentTime < 5000);
AddStep(@"Set previous", () => musicController.PreviousTrack()); AddStep(@"Set previous", () => musicController.PreviousTrack());
AddAssert(@"Check beatmap did change", () => currentBeatmap != Beatmap.Value); AddAssert(@"Check beatmap did change", () => currentBeatmap != Beatmap.Value);

View File

@ -52,7 +52,7 @@ namespace osu.Game.Tests
protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile)); protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile));
protected override Track GetTrack() => trackStore.Get(firstAudioFile); protected override Track GetBeatmapTrack() => trackStore.Get(firstAudioFile);
private string firstAudioFile private string firstAudioFile
{ {

View File

@ -3,7 +3,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="DeepEqual" Version="2.0.0" /> <PackageReference Include="DeepEqual" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

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 NUnit.Framework;
using osu.Game.Tests.Visual;
using osu.Game.Tournament.Components;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tournament.Tests.Components
{
public class TestSceneDateTextBox : OsuManualInputManagerTestScene
{
private DateTextBox textBox;
[SetUp]
public void Setup() => Schedule(() =>
{
Child = textBox = new DateTextBox
{
Width = 0.3f
};
});
[Test]
public void TestCommitWithoutSettingBindable()
{
AddStep("click textbox", () =>
{
InputManager.MoveMouseTo(textBox);
InputManager.Click(MouseButton.Left);
});
AddStep("unfocus", () =>
{
InputManager.MoveMouseTo(Vector2.Zero);
InputManager.Click(MouseButton.Left);
});
}
}
}

View File

@ -5,7 +5,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" /> <PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" /> <PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
</ItemGroup> </ItemGroup>

View File

@ -22,11 +22,12 @@ namespace osu.Game.Tournament.Components
} }
// hold a reference to the provided bindable so we don't have to in every settings section. // hold a reference to the provided bindable so we don't have to in every settings section.
private Bindable<DateTimeOffset> bindable; private Bindable<DateTimeOffset> bindable = new Bindable<DateTimeOffset>();
public DateTextBox() public DateTextBox()
{ {
base.Bindable = new Bindable<string>(); base.Bindable = new Bindable<string>();
((OsuTextBox)Control).OnCommit = (sender, newText) => ((OsuTextBox)Control).OnCommit = (sender, newText) =>
{ {
try try

View File

@ -26,6 +26,8 @@ namespace osu.Game.Tournament.Screens.Editors
[Cached] [Cached]
private LadderEditorInfo editorInfo = new LadderEditorInfo(); private LadderEditorInfo editorInfo = new LadderEditorInfo();
private WarningBox rightClickMessage;
protected override bool DrawLoserPaths => true; protected override bool DrawLoserPaths => true;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -37,6 +39,16 @@ namespace osu.Game.Tournament.Screens.Editors
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
Margin = new MarginPadding(5) Margin = new MarginPadding(5)
}); });
AddInternal(rightClickMessage = new WarningBox("Right click to place and link matches"));
LadderInfo.Matches.CollectionChanged += (_, __) => updateMessage();
updateMessage();
}
private void updateMessage()
{
rightClickMessage.Alpha = LadderInfo.Matches.Count > 0 ? 0 : 1;
} }
public void BeginJoin(TournamentMatch match, bool losers) public void BeginJoin(TournamentMatch match, bool losers)

View File

@ -87,30 +87,7 @@ namespace osu.Game.Tournament
}, },
} }
}, },
heightWarning = new Container heightWarning = new WarningBox("Please make the window wider"),
{
Masking = true,
CornerRadius = 5,
Depth = float.MinValue,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = Color4.Red,
RelativeSizeAxes = Axes.Both,
},
new TournamentSpriteText
{
Text = "Please make the window wider",
Font = OsuFont.Torus.With(weight: FontWeight.Bold),
Colour = Color4.White,
Padding = new MarginPadding(20)
}
}
},
new OsuContextMenuContainer new OsuContextMenuContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,

View File

@ -0,0 +1,40 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osuTK.Graphics;
namespace osu.Game.Tournament
{
internal class WarningBox : Container
{
public WarningBox(string text)
{
Masking = true;
CornerRadius = 5;
Depth = float.MinValue;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
{
new Box
{
Colour = Color4.Red,
RelativeSizeAxes = Axes.Both,
},
new TournamentSpriteText
{
Text = text,
Font = OsuFont.Torus.With(weight: FontWeight.Bold),
Colour = Color4.White,
Padding = new MarginPadding(20)
}
};
}
}
}

View File

@ -89,8 +89,14 @@ namespace osu.Game.Beatmaps
if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key))
return existing; return existing;
return await Task.Factory.StartNew(() => computeDifficulty(key, beatmapInfo, rulesetInfo), cancellationToken, return await Task.Factory.StartNew(() =>
TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); {
// Computation may have finished in a previous task.
if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out existing, out _))
return existing;
return computeDifficulty(key, beatmapInfo, rulesetInfo);
}, cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler);
} }
/// <summary> /// <summary>
@ -245,7 +251,7 @@ namespace osu.Game.Beatmaps
updateScheduler?.Dispose(); updateScheduler?.Dispose();
} }
private readonly struct DifficultyCacheLookup : IEquatable<DifficultyCacheLookup> public readonly struct DifficultyCacheLookup : IEquatable<DifficultyCacheLookup>
{ {
public readonly int BeatmapId; public readonly int BeatmapId;
public readonly int RulesetId; public readonly int RulesetId;
@ -261,7 +267,7 @@ namespace osu.Game.Beatmaps
public bool Equals(DifficultyCacheLookup other) public bool Equals(DifficultyCacheLookup other)
=> BeatmapId == other.BeatmapId => BeatmapId == other.BeatmapId
&& RulesetId == other.RulesetId && RulesetId == other.RulesetId
&& Mods.SequenceEqual(other.Mods); && Mods.Select(m => m.Acronym).SequenceEqual(other.Mods.Select(m => m.Acronym));
public override int GetHashCode() public override int GetHashCode()
{ {

View File

@ -9,6 +9,7 @@ using System.Linq.Expressions;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
@ -63,16 +64,16 @@ namespace osu.Game.Beatmaps
private readonly RulesetStore rulesets; private readonly RulesetStore rulesets;
private readonly BeatmapStore beatmaps; private readonly BeatmapStore beatmaps;
private readonly AudioManager audioManager; private readonly AudioManager audioManager;
private readonly GameHost host;
private readonly BeatmapOnlineLookupQueue onlineLookupQueue; private readonly BeatmapOnlineLookupQueue onlineLookupQueue;
private readonly TextureStore textureStore;
private readonly ITrackStore trackStore;
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, AudioManager audioManager, GameHost host = null, public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, GameHost host = null,
WorkingBeatmap defaultBeatmap = null) WorkingBeatmap defaultBeatmap = null)
: base(storage, contextFactory, api, new BeatmapStore(contextFactory), host) : base(storage, contextFactory, api, new BeatmapStore(contextFactory), host)
{ {
this.rulesets = rulesets; this.rulesets = rulesets;
this.audioManager = audioManager; this.audioManager = audioManager;
this.host = host;
DefaultBeatmap = defaultBeatmap; DefaultBeatmap = defaultBeatmap;
@ -83,6 +84,9 @@ namespace osu.Game.Beatmaps
beatmaps.ItemUpdated += removeWorkingCache; beatmaps.ItemUpdated += removeWorkingCache;
onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage); onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage);
textureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store));
trackStore = audioManager.GetTrackStore(Files.Store);
} }
protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => protected override ArchiveDownloadRequest<BeatmapSetInfo> CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) =>
@ -218,7 +222,7 @@ namespace osu.Game.Beatmaps
removeWorkingCache(info); removeWorkingCache(info);
} }
private readonly WeakList<WorkingBeatmap> workingCache = new WeakList<WorkingBeatmap>(); private readonly WeakList<BeatmapManagerWorkingBeatmap> workingCache = new WeakList<BeatmapManagerWorkingBeatmap>();
/// <summary> /// <summary>
/// Retrieve a <see cref="WorkingBeatmap"/> instance for the provided <see cref="BeatmapInfo"/> /// Retrieve a <see cref="WorkingBeatmap"/> instance for the provided <see cref="BeatmapInfo"/>
@ -246,16 +250,13 @@ namespace osu.Game.Beatmaps
lock (workingCache) lock (workingCache)
{ {
var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID); var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID);
if (working != null)
return working;
if (working == null) beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata;
{
beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata;
workingCache.Add(working = new BeatmapManagerWorkingBeatmap(Files.Store, workingCache.Add(working = new BeatmapManagerWorkingBeatmap(Files.Store, textureStore, trackStore, beatmapInfo, audioManager));
new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)), beatmapInfo, audioManager));
}
previous?.TransferTo(working);
return working; return working;
} }
} }
@ -459,7 +460,7 @@ namespace osu.Game.Beatmaps
protected override IBeatmap GetBeatmap() => beatmap; protected override IBeatmap GetBeatmap() => beatmap;
protected override Texture GetBackground() => null; protected override Texture GetBackground() => null;
protected override Track GetTrack() => null; protected override Track GetBeatmapTrack() => null;
} }
} }

View File

@ -17,15 +17,18 @@ namespace osu.Game.Beatmaps
{ {
public partial class BeatmapManager public partial class BeatmapManager
{ {
protected class BeatmapManagerWorkingBeatmap : WorkingBeatmap private class BeatmapManagerWorkingBeatmap : WorkingBeatmap
{ {
private readonly IResourceStore<byte[]> store; private readonly IResourceStore<byte[]> store;
private readonly TextureStore textureStore;
private readonly ITrackStore trackStore;
public BeatmapManagerWorkingBeatmap(IResourceStore<byte[]> store, TextureStore textureStore, BeatmapInfo beatmapInfo, AudioManager audioManager) public BeatmapManagerWorkingBeatmap(IResourceStore<byte[]> store, TextureStore textureStore, ITrackStore trackStore, BeatmapInfo beatmapInfo, AudioManager audioManager)
: base(beatmapInfo, audioManager) : base(beatmapInfo, audioManager)
{ {
this.store = store; this.store = store;
this.textureStore = textureStore; this.textureStore = textureStore;
this.trackStore = trackStore;
} }
protected override IBeatmap GetBeatmap() protected override IBeatmap GetBeatmap()
@ -44,10 +47,6 @@ namespace osu.Game.Beatmaps
private string getPathForFile(string filename) => BeatmapSetInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; private string getPathForFile(string filename) => BeatmapSetInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath;
private TextureStore textureStore;
private ITrackStore trackStore;
protected override bool BackgroundStillValid(Texture b) => false; // bypass lazy logic. we want to return a new background each time for refcounting purposes. protected override bool BackgroundStillValid(Texture b) => false; // bypass lazy logic. we want to return a new background each time for refcounting purposes.
protected override Texture GetBackground() protected override Texture GetBackground()
@ -66,11 +65,11 @@ namespace osu.Game.Beatmaps
} }
} }
protected override Track GetTrack() protected override Track GetBeatmapTrack()
{ {
try try
{ {
return (trackStore ??= AudioManager.GetTrackStore(store)).Get(getPathForFile(Metadata.AudioFile)); return trackStore.Get(getPathForFile(Metadata.AudioFile));
} }
catch (Exception e) catch (Exception e)
{ {
@ -79,22 +78,6 @@ namespace osu.Game.Beatmaps
} }
} }
public override void RecycleTrack()
{
base.RecycleTrack();
trackStore?.Dispose();
trackStore = null;
}
public override void TransferTo(WorkingBeatmap other)
{
base.TransferTo(other);
if (other is BeatmapManagerWorkingBeatmap owb && textureStore != null && BeatmapInfo.BackgroundEquals(other.BeatmapInfo))
owb.textureStore = textureStore;
}
protected override Waveform GetWaveform() protected override Waveform GetWaveform()
{ {
try try

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using JetBrains.Annotations;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
@ -19,7 +20,7 @@ namespace osu.Game.Beatmaps
{ {
private readonly TextureStore textures; private readonly TextureStore textures;
public DummyWorkingBeatmap(AudioManager audio, TextureStore textures) public DummyWorkingBeatmap([NotNull] AudioManager audio, TextureStore textures)
: base(new BeatmapInfo : base(new BeatmapInfo
{ {
Metadata = new BeatmapMetadata Metadata = new BeatmapMetadata
@ -44,7 +45,7 @@ namespace osu.Game.Beatmaps
protected override Texture GetBackground() => textures?.Get(@"Backgrounds/bg4"); protected override Texture GetBackground() => textures?.Get(@"Backgrounds/bg4");
protected override Track GetTrack() => GetVirtualTrack(); protected override Track GetBeatmapTrack() => GetVirtualTrack();
private class DummyRulesetInfo : RulesetInfo private class DummyRulesetInfo : RulesetInfo
{ {

View File

@ -104,10 +104,6 @@ namespace osu.Game.Beatmaps.Formats
try try
{ {
byte alpha = split.Length == 4 ? byte.Parse(split[3]) : (byte)255; byte alpha = split.Length == 4 ? byte.Parse(split[3]) : (byte)255;
if (alpha == 0)
alpha = 255;
colour = new Color4(byte.Parse(split[0]), byte.Parse(split[1]), byte.Parse(split[2]), alpha); colour = new Color4(byte.Parse(split[0]), byte.Parse(split[1]), byte.Parse(split[2]), alpha);
} }
catch catch

View File

@ -26,11 +26,6 @@ namespace osu.Game.Beatmaps
/// </summary> /// </summary>
Texture Background { get; } Texture Background { get; }
/// <summary>
/// Retrieves the audio track for this <see cref="WorkingBeatmap"/>.
/// </summary>
Track Track { get; }
/// <summary> /// <summary>
/// Retrieves the <see cref="Waveform"/> for the <see cref="Track"/> of this <see cref="WorkingBeatmap"/>. /// Retrieves the <see cref="Waveform"/> for the <see cref="Track"/> of this <see cref="WorkingBeatmap"/>.
/// </summary> /// </summary>
@ -59,5 +54,18 @@ namespace osu.Game.Beatmaps
/// <returns>The converted <see cref="IBeatmap"/>.</returns> /// <returns>The converted <see cref="IBeatmap"/>.</returns>
/// <exception cref="BeatmapInvalidForRulesetException">If <see cref="Beatmap"/> could not be converted to <paramref name="ruleset"/>.</exception> /// <exception cref="BeatmapInvalidForRulesetException">If <see cref="Beatmap"/> could not be converted to <paramref name="ruleset"/>.</exception>
IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList<Mod> mods = null, TimeSpan? timeout = null); IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList<Mod> mods = null, TimeSpan? timeout = null);
/// <summary>
/// Load a new audio track instance for this beatmap. This should be called once before accessing <see cref="Track"/>.
/// The caller of this method is responsible for the lifetime of the track.
/// </summary>
/// <remarks>
/// In a standard game context, the loading of the track is managed solely by MusicController, which will
/// automatically load the track of the current global IBindable WorkingBeatmap.
/// As such, this method should only be called in very special scenarios, such as external tests or apps which are
/// outside of the game context.
/// </remarks>
/// <returns>A fresh track instance, which will also be available via <see cref="Track"/>.</returns>
Track LoadTrack();
} }
} }

View File

@ -38,5 +38,6 @@ namespace osu.Game.Beatmaps.Legacy
Key1 = 1 << 26, Key1 = 1 << 26,
Key3 = 1 << 27, Key3 = 1 << 27,
Key2 = 1 << 28, Key2 = 1 << 28,
Mirror = 1 << 30,
} }
} }

View File

@ -7,6 +7,7 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
@ -40,7 +41,6 @@ namespace osu.Game.Beatmaps
BeatmapSetInfo = beatmapInfo.BeatmapSet; BeatmapSetInfo = beatmapInfo.BeatmapSet;
Metadata = beatmapInfo.Metadata ?? BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); Metadata = beatmapInfo.Metadata ?? BeatmapSetInfo?.Metadata ?? new BeatmapMetadata();
track = new RecyclableLazy<Track>(() => GetTrack() ?? GetVirtualTrack(1000));
background = new RecyclableLazy<Texture>(GetBackground, BackgroundStillValid); background = new RecyclableLazy<Texture>(GetBackground, BackgroundStillValid);
waveform = new RecyclableLazy<Waveform>(GetWaveform); waveform = new RecyclableLazy<Waveform>(GetWaveform);
storyboard = new RecyclableLazy<Storyboard>(GetStoryboard); storyboard = new RecyclableLazy<Storyboard>(GetStoryboard);
@ -259,10 +259,39 @@ namespace osu.Game.Beatmaps
protected abstract Texture GetBackground(); protected abstract Texture GetBackground();
private readonly RecyclableLazy<Texture> background; private readonly RecyclableLazy<Texture> background;
public virtual bool TrackLoaded => track.IsResultAvailable; private Track loadedTrack;
public Track Track => track.Value;
protected abstract Track GetTrack(); [NotNull]
private RecyclableLazy<Track> track; public Track LoadTrack() => loadedTrack = GetBeatmapTrack() ?? GetVirtualTrack(1000);
/// <summary>
/// Transfer a valid audio track into this working beatmap. Used as an optimisation to avoid reload / track swap
/// across difficulties in the same beatmap set.
/// </summary>
/// <param name="track">The track to transfer.</param>
public void TransferTrack([NotNull] Track track) => loadedTrack = track ?? throw new ArgumentNullException(nameof(track));
/// <summary>
/// Whether this beatmap's track has been loaded via <see cref="LoadTrack"/>.
/// </summary>
public virtual bool TrackLoaded => loadedTrack != null;
/// <summary>
/// Get the loaded audio track instance. <see cref="LoadTrack"/> must have first been called.
/// This generally happens via MusicController when changing the global beatmap.
/// </summary>
public Track Track
{
get
{
if (!TrackLoaded)
throw new InvalidOperationException($"Cannot access {nameof(Track)} without first calling {nameof(LoadTrack)}.");
return loadedTrack;
}
}
protected abstract Track GetBeatmapTrack();
public bool WaveformLoaded => waveform.IsResultAvailable; public bool WaveformLoaded => waveform.IsResultAvailable;
public Waveform Waveform => waveform.Value; public Waveform Waveform => waveform.Value;
@ -280,22 +309,6 @@ namespace osu.Game.Beatmaps
protected virtual ISkin GetSkin() => new DefaultSkin(); protected virtual ISkin GetSkin() => new DefaultSkin();
private readonly RecyclableLazy<ISkin> skin; private readonly RecyclableLazy<ISkin> skin;
/// <summary>
/// Transfer pieces of a beatmap to a new one, where possible, to save on loading.
/// </summary>
/// <param name="other">The new beatmap which is being switched to.</param>
public virtual void TransferTo(WorkingBeatmap other)
{
if (track.IsResultAvailable && Track != null && BeatmapInfo.AudioEquals(other.BeatmapInfo))
other.track = track;
}
/// <summary>
/// Eagerly dispose of the audio track associated with this <see cref="WorkingBeatmap"/> (if any).
/// Accessing track again will load a fresh instance.
/// </summary>
public virtual void RecycleTrack() => track.Recycle();
~WorkingBeatmap() ~WorkingBeatmap()
{ {
total_count.Value--; total_count.Value--;

View File

@ -47,7 +47,7 @@ namespace osu.Game.Graphics.Containers
protected override void Update() protected override void Update()
{ {
Track track = null; ITrack track = null;
IBeatmap beatmap = null; IBeatmap beatmap = null;
double currentTrackTime = 0; double currentTrackTime = 0;

View File

@ -40,10 +40,5 @@ namespace osu.Game.Graphics.UserInterface
protected override OsuSpriteText CreateSpriteText() protected override OsuSpriteText CreateSpriteText()
=> base.CreateSpriteText().With(s => s.Font = s.Font.With(size: 20f, fixedWidth: true)); => base.CreateSpriteText().With(s => s.Font = s.Font.With(size: 20f, fixedWidth: true));
public override void Increment(double amount)
{
Current.Value += amount;
}
} }
} }

View File

@ -57,20 +57,12 @@ namespace osu.Game.Graphics.UserInterface
} }
} }
public abstract void Increment(T amount);
/// <summary> /// <summary>
/// Skeleton of a numeric counter which value rolls over time. /// Skeleton of a numeric counter which value rolls over time.
/// </summary> /// </summary>
protected RollingCounter() protected RollingCounter()
{ {
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
Current.ValueChanged += val =>
{
if (IsLoaded)
TransformCount(DisplayedCount, val.NewValue);
};
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -81,6 +73,13 @@ namespace osu.Game.Graphics.UserInterface
Child = displayedCountSpriteText; Child = displayedCountSpriteText;
} }
protected override void LoadComplete()
{
base.LoadComplete();
Current.BindValueChanged(val => TransformCount(DisplayedCount, val.NewValue), true);
}
/// <summary> /// <summary>
/// Sets count value, bypassing rollover animation. /// Sets count value, bypassing rollover animation.
/// </summary> /// </summary>

View File

@ -51,10 +51,5 @@ namespace osu.Game.Graphics.UserInterface
protected override OsuSpriteText CreateSpriteText() protected override OsuSpriteText CreateSpriteText()
=> base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true)); => base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true));
public override void Increment(double amount)
{
Current.Value += amount;
}
} }
} }

View File

@ -33,11 +33,6 @@ namespace osu.Game.Graphics.UserInterface
return Math.Abs(currentValue - newValue) * RollingDuration * 100.0f; return Math.Abs(currentValue - newValue) * RollingDuration * 100.0f;
} }
public override void Increment(int amount)
{
Current.Value += amount;
}
protected override OsuSpriteText CreateSpriteText() protected override OsuSpriteText CreateSpriteText()
=> base.CreateSpriteText().With(s => s.Font = s.Font.With(size: 20f)); => base.CreateSpriteText().With(s => s.Font = s.Font.With(size: 20f));
} }

View File

@ -119,7 +119,7 @@ namespace osu.Game.Online.Chat
case "http": case "http":
case "https": case "https":
// length > 3 since all these links need another argument to work // length > 3 since all these links need another argument to work
if (args.Length > 3 && (args[1] == "osu.ppy.sh" || args[1] == "new.ppy.sh")) if (args.Length > 3 && args[1] == "osu.ppy.sh")
{ {
switch (args[2]) switch (args[2])
{ {

View File

@ -428,23 +428,7 @@ namespace osu.Game
updateModDefaults(); updateModDefaults();
var newBeatmap = beatmap.NewValue; beatmap.NewValue?.BeginAsyncLoad();
if (newBeatmap != null)
{
newBeatmap.Track.Completed += () => Scheduler.AddOnce(() => trackCompleted(newBeatmap));
newBeatmap.BeginAsyncLoad();
}
void trackCompleted(WorkingBeatmap b)
{
// the source of track completion is the audio thread, so the beatmap may have changed before firing.
if (Beatmap.Value != b)
return;
if (!Beatmap.Value.Track.Looping && !Beatmap.Disabled)
MusicController.NextTrack();
}
} }
private void modsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods) private void modsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
@ -618,8 +602,6 @@ namespace osu.Game
loadComponentSingleFile(new OnScreenDisplay(), Add, true); loadComponentSingleFile(new OnScreenDisplay(), Add, true);
loadComponentSingleFile(MusicController = new MusicController(), Add, true);
loadComponentSingleFile(notifications.With(d => loadComponentSingleFile(notifications.With(d =>
{ {
d.GetToolbarHeight = () => ToolbarOffset; d.GetToolbarHeight = () => ToolbarOffset;
@ -924,8 +906,6 @@ namespace osu.Game
private ScalingContainer screenContainer; private ScalingContainer screenContainer;
protected MusicController MusicController { get; private set; }
protected override bool OnExiting() protected override bool OnExiting()
{ {
if (ScreenStack.CurrentScreen is Loader) if (ScreenStack.CurrentScreen is Loader)

View File

@ -30,6 +30,7 @@ using osu.Game.Database;
using osu.Game.Input; using osu.Game.Input;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Overlays;
using osu.Game.Resources; using osu.Game.Resources;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -73,6 +74,8 @@ namespace osu.Game
protected MenuCursorContainer MenuCursorContainer; protected MenuCursorContainer MenuCursorContainer;
protected MusicController MusicController;
private Container content; private Container content;
protected override Container<Drawable> Content => content; protected override Container<Drawable> Content => content;
@ -238,16 +241,6 @@ namespace osu.Game
Beatmap = new NonNullableBindable<WorkingBeatmap>(defaultBeatmap); Beatmap = new NonNullableBindable<WorkingBeatmap>(defaultBeatmap);
// ScheduleAfterChildren is safety against something in the current frame accessing the previous beatmap's track
// and potentially causing a reload of it after just unloading.
// Note that the reason for this being added *has* been resolved, so it may be feasible to removed this if required.
Beatmap.BindValueChanged(b => ScheduleAfterChildren(() =>
{
// compare to last beatmap as sometimes the two may share a track representation (optimisation, see WorkingBeatmap.TransferTo)
if (b.OldValue?.TrackLoaded == true && b.OldValue?.Track != b.NewValue?.Track)
b.OldValue.RecycleTrack();
}));
dependencies.CacheAs<IBindable<WorkingBeatmap>>(Beatmap); dependencies.CacheAs<IBindable<WorkingBeatmap>>(Beatmap);
dependencies.CacheAs(Beatmap); dependencies.CacheAs(Beatmap);
@ -275,6 +268,9 @@ namespace osu.Game
dependencies.Cache(previewTrackManager = new PreviewTrackManager()); dependencies.Cache(previewTrackManager = new PreviewTrackManager());
Add(previewTrackManager); Add(previewTrackManager);
AddInternal(MusicController = new MusicController());
dependencies.CacheAs(MusicController);
Ruleset.BindValueChanged(onRulesetChanged); Ruleset.BindValueChanged(onRulesetChanged);
} }

View File

@ -116,7 +116,7 @@ namespace osu.Game.Overlays.Music
{ {
if (set.ID == (beatmap.Value?.BeatmapSetInfo?.ID ?? -1)) if (set.ID == (beatmap.Value?.BeatmapSetInfo?.ID ?? -1))
{ {
beatmap.Value?.Track?.Seek(0); beatmap.Value?.Track.Seek(0);
return; return;
} }

View File

@ -4,10 +4,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Framework.Threading; using osu.Framework.Threading;
@ -21,7 +25,7 @@ namespace osu.Game.Overlays
/// <summary> /// <summary>
/// Handles playback of the global music track. /// Handles playback of the global music track.
/// </summary> /// </summary>
public class MusicController : Component, IKeyBindingHandler<GlobalAction> public class MusicController : CompositeDrawable, IKeyBindingHandler<GlobalAction>
{ {
[Resolved] [Resolved]
private BeatmapManager beatmaps { get; set; } private BeatmapManager beatmaps { get; set; }
@ -61,6 +65,9 @@ namespace osu.Game.Overlays
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private OnScreenDisplay onScreenDisplay { get; set; } private OnScreenDisplay onScreenDisplay { get; set; }
[NotNull]
public DrawableTrack CurrentTrack { get; private set; } = new DrawableTrack(new TrackVirtual(1000));
private IBindable<WeakReference<BeatmapSetInfo>> managerUpdated; private IBindable<WeakReference<BeatmapSetInfo>> managerUpdated;
private IBindable<WeakReference<BeatmapSetInfo>> managerRemoved; private IBindable<WeakReference<BeatmapSetInfo>> managerRemoved;
@ -73,12 +80,9 @@ namespace osu.Game.Overlays
managerRemoved.BindValueChanged(beatmapRemoved); managerRemoved.BindValueChanged(beatmapRemoved);
beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets(IncludedDetails.Minimal, true).OrderBy(_ => RNG.Next())); beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets(IncludedDetails.Minimal, true).OrderBy(_ => RNG.Next()));
}
protected override void LoadComplete()
{
base.LoadComplete();
// Todo: These binds really shouldn't be here, but are unlikely to cause any issues for now.
// They are placed here for now since some tests rely on setting the beatmap _and_ their hierarchies inside their load(), which runs before the MusicController's load().
beatmap.BindValueChanged(beatmapChanged, true); beatmap.BindValueChanged(beatmapChanged, true);
mods.BindValueChanged(_ => ResetTrackAdjustments(), true); mods.BindValueChanged(_ => ResetTrackAdjustments(), true);
} }
@ -95,9 +99,14 @@ namespace osu.Game.Overlays
} }
/// <summary> /// <summary>
/// Returns whether the current beatmap track is playing. /// Returns whether the beatmap track is playing.
/// </summary> /// </summary>
public bool IsPlaying => current?.Track.IsRunning ?? false; public bool IsPlaying => CurrentTrack.IsRunning;
/// <summary>
/// Returns whether the beatmap track is loaded.
/// </summary>
public bool TrackLoaded => CurrentTrack.TrackLoaded;
private void beatmapUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakSet) private void beatmapUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakSet)
{ {
@ -130,7 +139,7 @@ namespace osu.Game.Overlays
seekDelegate = Schedule(() => seekDelegate = Schedule(() =>
{ {
if (!beatmap.Disabled) if (!beatmap.Disabled)
current?.Track.Seek(position); CurrentTrack.Seek(position);
}); });
} }
@ -142,9 +151,7 @@ namespace osu.Game.Overlays
{ {
if (IsUserPaused) return; if (IsUserPaused) return;
var track = current?.Track; if (CurrentTrack.IsDummyDevice)
if (track == null || track is TrackVirtual)
{ {
if (beatmap.Disabled) if (beatmap.Disabled)
return; return;
@ -163,17 +170,12 @@ namespace osu.Game.Overlays
/// <returns>Whether the operation was successful.</returns> /// <returns>Whether the operation was successful.</returns>
public bool Play(bool restart = false) public bool Play(bool restart = false)
{ {
var track = current?.Track;
IsUserPaused = false; IsUserPaused = false;
if (track == null)
return false;
if (restart) if (restart)
track.Restart(); CurrentTrack.Restart();
else if (!IsPlaying) else if (!IsPlaying)
track.Start(); CurrentTrack.Start();
return true; return true;
} }
@ -183,11 +185,9 @@ namespace osu.Game.Overlays
/// </summary> /// </summary>
public void Stop() public void Stop()
{ {
var track = current?.Track;
IsUserPaused = true; IsUserPaused = true;
if (track?.IsRunning == true) if (CurrentTrack.IsRunning)
track.Stop(); CurrentTrack.Stop();
} }
/// <summary> /// <summary>
@ -196,9 +196,7 @@ namespace osu.Game.Overlays
/// <returns>Whether the operation was successful.</returns> /// <returns>Whether the operation was successful.</returns>
public bool TogglePause() public bool TogglePause()
{ {
var track = current?.Track; if (CurrentTrack.IsRunning)
if (track?.IsRunning == true)
Stop(); Stop();
else else
Play(); Play();
@ -220,7 +218,7 @@ namespace osu.Game.Overlays
if (beatmap.Disabled) if (beatmap.Disabled)
return PreviousTrackResult.None; return PreviousTrackResult.None;
var currentTrackPosition = current?.Track.CurrentTime; var currentTrackPosition = CurrentTrack.CurrentTime;
if (currentTrackPosition >= restart_cutoff_point) if (currentTrackPosition >= restart_cutoff_point)
{ {
@ -234,9 +232,7 @@ namespace osu.Game.Overlays
if (playable != null) if (playable != null)
{ {
if (beatmap is Bindable<WorkingBeatmap> working) changeBeatmap(beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value));
working.Value = beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value);
restartTrack(); restartTrack();
return PreviousTrackResult.Previous; return PreviousTrackResult.Previous;
} }
@ -260,9 +256,7 @@ namespace osu.Game.Overlays
if (playable != null) if (playable != null)
{ {
if (beatmap is Bindable<WorkingBeatmap> working) changeBeatmap(beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value));
working.Value = beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value);
restartTrack(); restartTrack();
return true; return true;
} }
@ -274,21 +268,25 @@ namespace osu.Game.Overlays
{ {
// if not scheduled, the previously track will be stopped one frame later (see ScheduleAfterChildren logic in GameBase). // if not scheduled, the previously track will be stopped one frame later (see ScheduleAfterChildren logic in GameBase).
// we probably want to move this to a central method for switching to a new working beatmap in the future. // we probably want to move this to a central method for switching to a new working beatmap in the future.
Schedule(() => beatmap.Value.Track.Restart()); Schedule(() => CurrentTrack.Restart());
} }
private WorkingBeatmap current; private WorkingBeatmap current;
private TrackChangeDirection? queuedDirection; private TrackChangeDirection? queuedDirection;
private void beatmapChanged(ValueChangedEvent<WorkingBeatmap> beatmap) private void beatmapChanged(ValueChangedEvent<WorkingBeatmap> beatmap) => changeBeatmap(beatmap.NewValue);
private void changeBeatmap(WorkingBeatmap newWorking)
{ {
var lastWorking = current;
TrackChangeDirection direction = TrackChangeDirection.None; TrackChangeDirection direction = TrackChangeDirection.None;
bool audioEquals = newWorking?.BeatmapInfo?.AudioEquals(current?.BeatmapInfo) ?? false;
if (current != null) if (current != null)
{ {
bool audioEquals = beatmap.NewValue?.BeatmapInfo?.AudioEquals(current.BeatmapInfo) ?? false;
if (audioEquals) if (audioEquals)
direction = TrackChangeDirection.None; direction = TrackChangeDirection.None;
else if (queuedDirection.HasValue) else if (queuedDirection.HasValue)
@ -300,18 +298,74 @@ namespace osu.Game.Overlays
{ {
// figure out the best direction based on order in playlist. // figure out the best direction based on order in playlist.
var last = BeatmapSets.TakeWhile(b => b.ID != current.BeatmapSetInfo?.ID).Count(); var last = BeatmapSets.TakeWhile(b => b.ID != current.BeatmapSetInfo?.ID).Count();
var next = beatmap.NewValue == null ? -1 : BeatmapSets.TakeWhile(b => b.ID != beatmap.NewValue.BeatmapSetInfo?.ID).Count(); var next = newWorking == null ? -1 : BeatmapSets.TakeWhile(b => b.ID != newWorking.BeatmapSetInfo?.ID).Count();
direction = last > next ? TrackChangeDirection.Prev : TrackChangeDirection.Next; direction = last > next ? TrackChangeDirection.Prev : TrackChangeDirection.Next;
} }
} }
current = beatmap.NewValue; current = newWorking;
if (!audioEquals || CurrentTrack.IsDummyDevice)
{
changeTrack();
}
else
{
// transfer still valid track to new working beatmap
current.TransferTrack(lastWorking.Track);
}
TrackChanged?.Invoke(current, direction); TrackChanged?.Invoke(current, direction);
ResetTrackAdjustments(); ResetTrackAdjustments();
queuedDirection = null; queuedDirection = null;
// this will be a noop if coming from the beatmapChanged event.
// the exception is local operations like next/prev, where we want to complete loading the track before sending out a change.
if (beatmap.Value != current && beatmap is Bindable<WorkingBeatmap> working)
working.Value = current;
}
private void changeTrack()
{
var lastTrack = CurrentTrack;
var queuedTrack = new DrawableTrack(current.LoadTrack());
queuedTrack.Completed += () => onTrackCompleted(current);
CurrentTrack = queuedTrack;
// At this point we may potentially be in an async context from tests. This is extremely dangerous but we have to make do for now.
// CurrentTrack is immediately updated above for situations where a immediate knowledge about the new track is required,
// but the mutation of the hierarchy is scheduled to avoid exceptions.
Schedule(() =>
{
lastTrack.VolumeTo(0, 500, Easing.Out).Expire();
if (queuedTrack == CurrentTrack)
{
AddInternal(queuedTrack);
queuedTrack.VolumeTo(0).Then().VolumeTo(1, 300, Easing.Out);
}
else
{
// If the track has changed since the call to changeTrack, it is safe to dispose the
// queued track rather than consume it.
queuedTrack.Dispose();
}
});
}
private void onTrackCompleted(WorkingBeatmap workingBeatmap)
{
// the source of track completion is the audio thread, so the beatmap may have changed before firing.
if (current != workingBeatmap)
return;
if (!CurrentTrack.Looping && !beatmap.Disabled)
NextTrack();
} }
private bool allowRateAdjustments; private bool allowRateAdjustments;
@ -332,18 +386,20 @@ namespace osu.Game.Overlays
} }
} }
/// <summary>
/// Resets the speed adjustments currently applied on <see cref="CurrentTrack"/> and applies the mod adjustments if <see cref="AllowRateAdjustments"/> is <c>true</c>.
/// </summary>
/// <remarks>
/// Does not reset speed adjustments applied directly to the beatmap track.
/// </remarks>
public void ResetTrackAdjustments() public void ResetTrackAdjustments()
{ {
var track = current?.Track; CurrentTrack.ResetSpeedAdjustments();
if (track == null)
return;
track.ResetSpeedAdjustments();
if (allowRateAdjustments) if (allowRateAdjustments)
{ {
foreach (var mod in mods.Value.OfType<IApplicableToTrack>()) foreach (var mod in mods.Value.OfType<IApplicableToTrack>())
mod.ApplyToTrack(track); mod.ApplyToTrack(CurrentTrack);
} }
} }

View File

@ -234,9 +234,9 @@ namespace osu.Game.Overlays
pendingBeatmapSwitch = null; pendingBeatmapSwitch = null;
} }
var track = beatmap.Value?.TrackLoaded ?? false ? beatmap.Value.Track : null; var track = musicController.CurrentTrack;
if (track?.IsDummyDevice == false) if (!track.IsDummyDevice)
{ {
progressBar.EndTime = track.Length; progressBar.EndTime = track.Length;
progressBar.CurrentTime = track.CurrentTime; progressBar.CurrentTime = track.CurrentTime;

View File

@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Mods
/// </summary> /// </summary>
public interface IApplicableToTrack : IApplicableMod public interface IApplicableToTrack : IApplicableMod
{ {
void ApplyToTrack(Track track); void ApplyToTrack(ITrack track);
} }
} }

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mods
}, true); }, true);
} }
public override void ApplyToTrack(Track track) public override void ApplyToTrack(ITrack track)
{ {
// base.ApplyToTrack() intentionally not called (different tempo adjustment is applied) // base.ApplyToTrack() intentionally not called (different tempo adjustment is applied)
track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust);

View File

@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mods
}, true); }, true);
} }
public override void ApplyToTrack(Track track) public override void ApplyToTrack(ITrack track)
{ {
// base.ApplyToTrack() intentionally not called (different tempo adjustment is applied) // base.ApplyToTrack() intentionally not called (different tempo adjustment is applied)
track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust);

View File

@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Mods
{ {
public abstract BindableNumber<double> SpeedChange { get; } public abstract BindableNumber<double> SpeedChange { get; }
public virtual void ApplyToTrack(Track track) public virtual void ApplyToTrack(ITrack track)
{ {
track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange);
} }

View File

@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mods
Precision = 0.01, Precision = 0.01,
}; };
private Track track; private ITrack track;
protected ModTimeRamp() protected ModTimeRamp()
{ {
@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Mods
AdjustPitch.BindValueChanged(applyPitchAdjustment); AdjustPitch.BindValueChanged(applyPitchAdjustment);
} }
public void ApplyToTrack(Track track) public void ApplyToTrack(ITrack track)
{ {
this.track = track; this.track = track;

View File

@ -13,7 +13,6 @@ using osu.Game.Replays.Legacy;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Users; using osu.Game.Users;
using SharpCompress.Compressors.LZMA; using SharpCompress.Compressors.LZMA;
@ -123,12 +122,12 @@ namespace osu.Game.Scoring.Legacy
protected void CalculateAccuracy(ScoreInfo score) protected void CalculateAccuracy(ScoreInfo score)
{ {
score.Statistics.TryGetValue(HitResult.Miss, out int countMiss); int countMiss = score.GetCountMiss() ?? 0;
score.Statistics.TryGetValue(HitResult.Meh, out int count50); int count50 = score.GetCount50() ?? 0;
score.Statistics.TryGetValue(HitResult.Good, out int count100); int count100 = score.GetCount100() ?? 0;
score.Statistics.TryGetValue(HitResult.Great, out int count300); int count300 = score.GetCount300() ?? 0;
score.Statistics.TryGetValue(HitResult.Perfect, out int countGeki); int countGeki = score.GetCountGeki() ?? 0;
score.Statistics.TryGetValue(HitResult.Ok, out int countKatu); int countKatu = score.GetCountKatu() ?? 0;
switch (score.Ruleset.ID) switch (score.Ruleset.ID)
{ {
@ -241,12 +240,15 @@ namespace osu.Game.Scoring.Legacy
} }
var diff = Parsing.ParseFloat(split[0]); var diff = Parsing.ParseFloat(split[0]);
var mouseX = Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE);
var mouseY = Parsing.ParseFloat(split[2], Parsing.MAX_COORDINATE_VALUE);
lastTime += diff; lastTime += diff;
if (i == 0 && diff == 0) if (i < 2 && mouseX == 256 && mouseY == -500)
// osu-stable adds a zero-time frame before potentially valid negative user frames. // at the start of the replay, stable places two replay frames, at time 0 and SkipBoundary - 1, respectively.
// we need to ignore this. // both frames use a position of (256, -500).
// ignore these frames as they serve no real purpose (and can even mislead ruleset-specific handlers - see mania)
continue; continue;
// Todo: At some point we probably want to rewind and play back the negative-time frames // Todo: At some point we probably want to rewind and play back the negative-time frames
@ -255,8 +257,8 @@ namespace osu.Game.Scoring.Legacy
continue; continue;
currentFrame = convertFrame(new LegacyReplayFrame(lastTime, currentFrame = convertFrame(new LegacyReplayFrame(lastTime,
Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE), mouseX,
Parsing.ParseFloat(split[2], Parsing.MAX_COORDINATE_VALUE), mouseY,
(ReplayButtonState)Parsing.ParseInt(split[3])), currentFrame); (ReplayButtonState)Parsing.ParseInt(split[3])), currentFrame);
replay.Frames.Add(currentFrame); replay.Frames.Add(currentFrame);

View File

@ -26,6 +26,28 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved] [Resolved]
private EditorClock editorClock { get; set; } private EditorClock editorClock { get; set; }
/// <summary>
/// The timeline's scroll position in the last frame.
/// </summary>
private float lastScrollPosition;
/// <summary>
/// The track time in the last frame.
/// </summary>
private double lastTrackTime;
/// <summary>
/// Whether the user is currently dragging the timeline.
/// </summary>
private bool handlingDragInput;
/// <summary>
/// Whether the track was playing before a user drag event.
/// </summary>
private bool trackWasPlaying;
private Track track;
public Timeline() public Timeline()
{ {
ZoomDuration = 200; ZoomDuration = 200;
@ -59,6 +81,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
waveform.Waveform = b.NewValue.Waveform; waveform.Waveform = b.NewValue.Waveform;
track = b.NewValue.Track; track = b.NewValue.Track;
// todo: i don't think this is safe, the track may not be loaded yet.
if (track.Length > 0) if (track.Length > 0)
{ {
MaxZoom = getZoomLevelForVisibleMilliseconds(500); MaxZoom = getZoomLevelForVisibleMilliseconds(500);
@ -68,29 +91,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}, true); }, true);
} }
private float getZoomLevelForVisibleMilliseconds(double milliseconds) => (float)(track.Length / milliseconds); private float getZoomLevelForVisibleMilliseconds(double milliseconds) => Math.Max(1, (float)(track.Length / milliseconds));
/// <summary>
/// The timeline's scroll position in the last frame.
/// </summary>
private float lastScrollPosition;
/// <summary>
/// The track time in the last frame.
/// </summary>
private double lastTrackTime;
/// <summary>
/// Whether the user is currently dragging the timeline.
/// </summary>
private bool handlingDragInput;
/// <summary>
/// Whether the track was playing before a user drag event.
/// </summary>
private bool trackWasPlaying;
private Track track;
protected override void Update() protected override void Update()
{ {

View File

@ -284,7 +284,7 @@ namespace osu.Game.Screens.Edit
// this is a special case to handle the "pivot" scenario. // this is a special case to handle the "pivot" scenario.
// if we are precise scrolling in one direction then change our mind and scroll backwards, // if we are precise scrolling in one direction then change our mind and scroll backwards,
// the existing accumulation should be applied in the inverse direction to maintain responsiveness. // the existing accumulation should be applied in the inverse direction to maintain responsiveness.
if (Math.Sign(scrollAccumulation) != scrollDirection) if (scrollAccumulation != 0 && Math.Sign(scrollAccumulation) != scrollDirection)
scrollAccumulation = scrollDirection * (precision - Math.Abs(scrollAccumulation)); scrollAccumulation = scrollDirection * (precision - Math.Abs(scrollAccumulation));
scrollAccumulation += scrollComponent * (e.IsPrecise ? 0.1 : 1); scrollAccumulation += scrollComponent * (e.IsPrecise ? 0.1 : 1);

View File

@ -107,7 +107,7 @@ namespace osu.Game.Screens.Edit
protected override Texture GetBackground() => throw new NotImplementedException(); protected override Texture GetBackground() => throw new NotImplementedException();
protected override Track GetTrack() => throw new NotImplementedException(); protected override Track GetBeatmapTrack() => throw new NotImplementedException();
} }
} }
} }

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