1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 17:23:22 +08:00

Merge branch 'master' into legacy-jugement-match-stable-transforms

This commit is contained in:
Bartłomiej Dach 2020-11-18 20:33:58 +01:00 committed by GitHub
commit 24d7fa03d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 425 additions and 357 deletions

17
.vscode/tasks.json vendored
View File

@ -9,7 +9,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Desktop", "osu.Desktop",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
"-m", "-m",
@ -24,7 +23,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Desktop", "osu.Desktop",
"-p:Configuration=Release", "-p:Configuration=Release",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
@ -40,7 +38,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Tests", "osu.Game.Tests",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
"-m", "-m",
@ -55,7 +52,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Tests", "osu.Game.Tests",
"-p:Configuration=Release", "-p:Configuration=Release",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
@ -71,7 +67,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Tournament.Tests", "osu.Game.Tournament.Tests",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
"-m", "-m",
@ -86,7 +81,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Tournament.Tests", "osu.Game.Tournament.Tests",
"-p:Configuration=Release", "-p:Configuration=Release",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
@ -102,7 +96,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Benchmarks", "osu.Game.Benchmarks",
"-p:Configuration=Release", "-p:Configuration=Release",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
@ -111,16 +104,6 @@
], ],
"group": "build", "group": "build",
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
},
{
"label": "Restore (netcoreapp3.1)",
"type": "shell",
"command": "dotnet",
"args": [
"restore",
"build/Desktop.proj"
],
"problemMatcher": []
} }
] ]
} }

View File

@ -75,7 +75,6 @@ git pull
Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this is provided [below](#contributing). Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this is provided [below](#contributing).
- Visual Studio / Rider users should load the project via one of the platform-specific `.slnf` files, rather than the main `.sln.` This will allow access to template run configurations. - Visual Studio / Rider users should load the project via one of the platform-specific `.slnf` files, rather than the main `.sln.` This will allow access to template run configurations.
- Visual Studio Code users must run the `Restore` task before any build attempt.
You can also build and run *osu!* from the command-line with a single command: You can also build and run *osu!* from the command-line with a single command:

View File

@ -52,6 +52,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1113.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2020.1118.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -139,7 +139,7 @@ namespace osu.Desktop
// SDL2 DesktopWindow // SDL2 DesktopWindow
case DesktopWindow desktopWindow: case DesktopWindow desktopWindow:
desktopWindow.CursorState.Value |= CursorState.Hidden; desktopWindow.CursorState |= CursorState.Hidden;
desktopWindow.SetIconFromStream(iconStream); desktopWindow.SetIconFromStream(iconStream);
desktopWindow.Title = Name; desktopWindow.Title = Name;
desktopWindow.DragDrop += f => fileDrop(new[] { f }); desktopWindow.DragDrop += f => fileDrop(new[] { f });

View File

@ -9,7 +9,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Rulesets.Catch.Tests.csproj", "osu.Game.Rulesets.Catch.Tests.csproj",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
"-m", "-m",
@ -24,7 +23,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Rulesets.Catch.Tests.csproj", "osu.Game.Rulesets.Catch.Tests.csproj",
"-p:Configuration=Release", "-p:Configuration=Release",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
@ -33,15 +31,6 @@
], ],
"group": "build", "group": "build",
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
},
{
"label": "Restore",
"type": "shell",
"command": "dotnet",
"args": [
"restore"
],
"problemMatcher": []
} }
] ]
} }

View File

@ -18,71 +18,42 @@ namespace osu.Game.Rulesets.Catch.Tests
base.LoadComplete(); base.LoadComplete();
foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation))) foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation)))
AddStep($"show {rep}", () => SetContents(() => createDrawable(rep))); AddStep($"show {rep}", () => SetContents(() => createDrawableFruit(rep)));
AddStep("show droplet", () => SetContents(() => createDrawableDroplet())); AddStep("show droplet", () => SetContents(() => createDrawableDroplet()));
AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet)); AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet));
foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation))) foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation)))
AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawable(rep, true))); AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawableFruit(rep, true)));
AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true))); AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true)));
} }
private Drawable createDrawableTinyDroplet() private Drawable createDrawableFruit(FruitVisualRepresentation rep, bool hyperdash = false) =>
setProperties(new DrawableFruit(new TestCatchFruit(rep)), hyperdash);
private Drawable createDrawableDroplet(bool hyperdash = false) => setProperties(new DrawableDroplet(new Droplet()), hyperdash);
private Drawable createDrawableTinyDroplet() => setProperties(new DrawableTinyDroplet(new TinyDroplet()));
private DrawableCatchHitObject setProperties(DrawableCatchHitObject d, bool hyperdash = false)
{ {
var droplet = new TestCatchTinyDroplet var hitObject = d.HitObject;
{ hitObject.StartTime = 1000000000000;
Scale = 1.5f, hitObject.Scale = 1.5f;
};
return new DrawableTinyDroplet(droplet) if (hyperdash)
{ hitObject.HyperDashTarget = new Banana();
Anchor = Anchor.Centre,
RelativePositionAxes = Axes.None,
Position = Vector2.Zero,
Alpha = 1,
LifetimeStart = double.NegativeInfinity,
LifetimeEnd = double.PositiveInfinity,
};
}
private Drawable createDrawableDroplet(bool hyperdash = false) d.Anchor = Anchor.Centre;
{ d.RelativePositionAxes = Axes.None;
var droplet = new TestCatchDroplet d.Position = Vector2.Zero;
d.HitObjectApplied += _ =>
{ {
Scale = 1.5f, d.LifetimeStart = double.NegativeInfinity;
HyperDashTarget = hyperdash ? new Banana() : null d.LifetimeEnd = double.PositiveInfinity;
};
return new DrawableDroplet(droplet)
{
Anchor = Anchor.Centre,
RelativePositionAxes = Axes.None,
Position = Vector2.Zero,
Alpha = 1,
LifetimeStart = double.NegativeInfinity,
LifetimeEnd = double.PositiveInfinity,
};
}
private Drawable createDrawable(FruitVisualRepresentation rep, bool hyperdash = false)
{
Fruit fruit = new TestCatchFruit(rep)
{
Scale = 1.5f,
HyperDashTarget = hyperdash ? new Banana() : null
};
return new DrawableFruit(fruit)
{
Anchor = Anchor.Centre,
RelativePositionAxes = Axes.None,
Position = Vector2.Zero,
Alpha = 1,
LifetimeStart = double.NegativeInfinity,
LifetimeEnd = double.PositiveInfinity,
}; };
return d;
} }
public class TestCatchFruit : Fruit public class TestCatchFruit : Fruit
@ -90,26 +61,9 @@ namespace osu.Game.Rulesets.Catch.Tests
public TestCatchFruit(FruitVisualRepresentation rep) public TestCatchFruit(FruitVisualRepresentation rep)
{ {
VisualRepresentation = rep; VisualRepresentation = rep;
StartTime = 1000000000000;
} }
public override FruitVisualRepresentation VisualRepresentation { get; } public override FruitVisualRepresentation VisualRepresentation { get; }
} }
public class TestCatchDroplet : Droplet
{
public TestCatchDroplet()
{
StartTime = 1000000000000;
}
}
public class TestCatchTinyDroplet : TinyDroplet
{
public TestCatchTinyDroplet()
{
StartTime = 1000000000000;
}
}
} }
} }

View File

@ -9,7 +9,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Rulesets.Mania.Tests.csproj", "osu.Game.Rulesets.Mania.Tests.csproj",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
"-m", "-m",
@ -24,7 +23,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Rulesets.Mania.Tests.csproj", "osu.Game.Rulesets.Mania.Tests.csproj",
"-p:Configuration=Release", "-p:Configuration=Release",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
@ -33,15 +31,6 @@
], ],
"group": "build", "group": "build",
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
},
{
"label": "Restore",
"type": "shell",
"command": "dotnet",
"args": [
"restore"
],
"problemMatcher": []
} }
] ]
} }

View File

@ -1,7 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; 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.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
@ -15,8 +15,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
/// </summary> /// </summary>
public abstract class ManiaHitObjectTestScene : ManiaSkinnableTestScene public abstract class ManiaHitObjectTestScene : ManiaSkinnableTestScene
{ {
[BackgroundDependencyLoader] [SetUp]
private void load() public void SetUp() => Schedule(() =>
{ {
SetContents(() => new FillFlowContainer SetContents(() => new FillFlowContainer
{ {
@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
}, },
} }
}); });
} });
protected abstract DrawableManiaHitObject CreateHitObject(); protected abstract DrawableManiaHitObject CreateHitObject();
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -26,6 +27,18 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
}); });
} }
[Test]
public void TestFadeOnMiss()
{
AddStep("miss tick", () =>
{
foreach (var holdNote in holdNotes)
holdNote.ChildrenOfType<DrawableHoldNoteHead>().First().MissForcefully();
});
}
private IEnumerable<DrawableHoldNote> holdNotes => CreatedDrawables.SelectMany(d => d.ChildrenOfType<DrawableHoldNote>());
protected override DrawableManiaHitObject CreateHitObject() protected override DrawableManiaHitObject CreateHitObject()
{ {
var note = new HoldNote { Duration = 1000 }; var note = new HoldNote { Duration = 1000 };

View File

@ -51,9 +51,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public double? HoldStartTime { get; private set; } public double? HoldStartTime { get; private set; }
/// <summary> /// <summary>
/// Whether the hold note has been released too early and shouldn't give full score for the release. /// Time at which the hold note has been broken, i.e. released too early, resulting in a reduced score.
/// </summary> /// </summary>
public bool HasBroken { get; private set; } public double? HoldBrokenTime { get; private set; }
/// <summary> /// <summary>
/// Whether the hold note has been released potentially without having caused a break. /// Whether the hold note has been released potentially without having caused a break.
@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
} }
if (Tail.Judged && !Tail.IsHit) if (Tail.Judged && !Tail.IsHit)
HasBroken = true; HoldBrokenTime = Time.Current;
} }
public bool OnPressed(ManiaAction action) public bool OnPressed(ManiaAction action)
@ -298,7 +298,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
// If the key has been released too early, the user should not receive full score for the release // If the key has been released too early, the user should not receive full score for the release
if (!Tail.IsHit) if (!Tail.IsHit)
HasBroken = true; HoldBrokenTime = Time.Current;
releaseTime = Time.Current; releaseTime = Time.Current;
} }

View File

@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
ApplyResult(r => ApplyResult(r =>
{ {
// If the head wasn't hit or the hold note was broken, cap the max score to Meh. // If the head wasn't hit or the hold note was broken, cap the max score to Meh.
if (result > HitResult.Meh && (!holdNote.Head.IsHit || holdNote.HasBroken)) if (result > HitResult.Meh && (!holdNote.Head.IsHit || holdNote.HoldBrokenTime != null))
result = HitResult.Meh; result = HitResult.Meh;
r.Type = result; r.Type = result;

View File

@ -16,8 +16,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary> /// </summary>
public class DrawableHoldNoteTick : DrawableManiaHitObject<HoldNoteTick> public class DrawableHoldNoteTick : DrawableManiaHitObject<HoldNoteTick>
{ {
public override bool DisplayResult => false;
/// <summary> /// <summary>
/// References the time at which the user started holding the hold note. /// References the time at which the user started holding the hold note.
/// </summary> /// </summary>

View File

@ -18,9 +18,17 @@ namespace osu.Game.Rulesets.Mania.Skinning
{ {
public class LegacyBodyPiece : LegacyManiaColumnElement public class LegacyBodyPiece : LegacyManiaColumnElement
{ {
private DrawableHoldNote holdNote;
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>();
/// <summary>
/// Stores the start time of the fade animation that plays when any of the nested
/// hitobjects of the hold note are missed.
/// </summary>
private readonly Bindable<double?> missFadeTime = new Bindable<double?>();
[CanBeNull] [CanBeNull]
private Drawable bodySprite; private Drawable bodySprite;
@ -38,6 +46,8 @@ namespace osu.Game.Rulesets.Mania.Skinning
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ISkinSource skin, IScrollingInfo scrollingInfo, DrawableHitObject drawableObject) private void load(ISkinSource skin, IScrollingInfo scrollingInfo, DrawableHitObject drawableObject)
{ {
holdNote = (DrawableHoldNote)drawableObject;
string imageName = GetColumnSkinConfig<string>(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value string imageName = GetColumnSkinConfig<string>(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value
?? $"mania-note{FallbackColumnIndex}L"; ?? $"mania-note{FallbackColumnIndex}L";
@ -92,11 +102,26 @@ namespace osu.Game.Rulesets.Mania.Skinning
InternalChild = bodySprite; InternalChild = bodySprite;
direction.BindTo(scrollingInfo.Direction); direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
var holdNote = (DrawableHoldNote)drawableObject;
isHitting.BindTo(holdNote.IsHitting); isHitting.BindTo(holdNote.IsHitting);
}
protected override void LoadComplete()
{
base.LoadComplete();
direction.BindValueChanged(onDirectionChanged, true);
isHitting.BindValueChanged(onIsHittingChanged, true); isHitting.BindValueChanged(onIsHittingChanged, true);
missFadeTime.BindValueChanged(onMissFadeTimeChanged, true);
holdNote.ApplyCustomUpdateState += applyCustomUpdateState;
applyCustomUpdateState(holdNote, holdNote.State.Value);
}
private void applyCustomUpdateState(DrawableHitObject hitObject, ArmedState state)
{
// ensure that the hold note is also faded out when the head/tail/any tick is missed.
if (state == ArmedState.Miss)
missFadeTime.Value ??= hitObject.HitStateUpdateTime;
} }
private void onIsHittingChanged(ValueChangedEvent<bool> isHitting) private void onIsHittingChanged(ValueChangedEvent<bool> isHitting)
@ -158,10 +183,38 @@ namespace osu.Game.Rulesets.Mania.Skinning
} }
} }
private void onMissFadeTimeChanged(ValueChangedEvent<double?> missFadeTimeChange)
{
if (missFadeTimeChange.NewValue == null)
return;
// this update could come from any nested object of the hold note (or even from an input).
// make sure the transforms are consistent across all affected parts.
using (BeginAbsoluteSequence(missFadeTimeChange.NewValue.Value))
{
// colour and duration matches stable
// transforms not applied to entire hold note in order to not affect hit lighting
const double fade_duration = 60;
holdNote.Head.FadeColour(Colour4.DarkGray, fade_duration);
holdNote.Tail.FadeColour(Colour4.DarkGray, fade_duration);
bodySprite?.FadeColour(Colour4.DarkGray, fade_duration);
}
}
protected override void Update()
{
base.Update();
missFadeTime.Value ??= holdNote.HoldBrokenTime;
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
if (holdNote != null)
holdNote.ApplyCustomUpdateState -= applyCustomUpdateState;
lightContainer?.Expire(); lightContainer?.Expire();
} }
} }

View File

@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Mania.UI
if (result.IsHit) if (result.IsHit)
hitPolicy.HandleHit(judgedObject); hitPolicy.HandleHit(judgedObject);
if (!result.IsHit || !DisplayJudgements.Value) if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value)
return; return;
HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result))); HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result)));

View File

@ -167,6 +167,10 @@ namespace osu.Game.Rulesets.Mania.UI
if (!judgedObject.DisplayResult || !DisplayJudgements.Value) if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
return; return;
// Tick judgements should not display text.
if (judgedObject is DrawableHoldNoteTick)
return;
judgements.Clear(false); judgements.Clear(false);
judgements.Add(judgementPool.Get(j => judgements.Add(judgementPool.Get(j =>
{ {

View File

@ -9,7 +9,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Rulesets.Osu.Tests.csproj", "osu.Game.Rulesets.Osu.Tests.csproj",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
"-m", "-m",
@ -24,7 +23,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Rulesets.Osu.Tests.csproj", "osu.Game.Rulesets.Osu.Tests.csproj",
"-p:Configuration=Release", "-p:Configuration=Release",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
@ -33,15 +31,6 @@
], ],
"group": "build", "group": "build",
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
},
{
"label": "Restore",
"type": "shell",
"command": "dotnet",
"args": [
"restore"
],
"problemMatcher": []
} }
] ]
} }

View File

@ -1,20 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
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 osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using osu.Game.Skinning;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
{ {
public class TestSceneSliderApplication : OsuTestScene public class TestSceneSliderApplication : OsuTestScene
{ {
[Resolved]
private SkinManager skinManager { get; set; }
[Test] [Test]
public void TestApplyNewSlider() public void TestApplyNewSlider()
{ {
@ -50,6 +60,41 @@ namespace osu.Game.Rulesets.Osu.Tests
}), null)); }), null));
} }
[Test]
public void TestBallTintChangedOnAccentChange()
{
DrawableSlider dho = null;
AddStep("create slider", () =>
{
var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.Info);
tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1";
Child = new SkinProvidingContainer(tintingSkin)
{
RelativeSizeAxes = Axes.Both,
Child = dho = new DrawableSlider(prepareObject(new Slider
{
Position = new Vector2(256, 192),
IndexInCurrentCombo = 0,
StartTime = Time.Current,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(150, 100),
new Vector2(300, 0),
})
}))
};
});
AddStep("set accent white", () => dho.AccentColour.Value = Color4.White);
AddAssert("ball is white", () => dho.ChildrenOfType<SliderBall>().Single().AccentColour == Color4.White);
AddStep("set accent red", () => dho.AccentColour.Value = Color4.Red);
AddAssert("ball is red", () => dho.ChildrenOfType<SliderBall>().Single().AccentColour == Color4.Red);
}
private Slider prepareObject(Slider slider) private Slider prepareObject(Slider slider)
{ {
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private readonly Path path; private readonly Path path;
private readonly Slider slider; private readonly Slider slider;
private readonly int controlPointIndex; public int ControlPointIndex { get; set; }
private IBindable<Vector2> sliderPosition; private IBindable<Vector2> sliderPosition;
private IBindable<int> pathVersion; private IBindable<int> pathVersion;
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public PathControlPointConnectionPiece(Slider slider, int controlPointIndex) public PathControlPointConnectionPiece(Slider slider, int controlPointIndex)
{ {
this.slider = slider; this.slider = slider;
this.controlPointIndex = controlPointIndex; ControlPointIndex = controlPointIndex;
Origin = Anchor.Centre; Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
path.ClearVertices(); path.ClearVertices();
int nextIndex = controlPointIndex + 1; int nextIndex = ControlPointIndex + 1;
if (nextIndex == 0 || nextIndex >= slider.Path.ControlPoints.Count) if (nextIndex == 0 || nextIndex >= slider.Path.ControlPoints.Count)
return; return;

View File

@ -66,6 +66,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
switch (e.Action) switch (e.Action)
{ {
case NotifyCollectionChangedAction.Add: case NotifyCollectionChangedAction.Add:
// If inserting in the path (not appending),
// update indices of existing connections after insert location
if (e.NewStartingIndex < Pieces.Count)
{
foreach (var connection in Connections)
{
if (connection.ControlPointIndex >= e.NewStartingIndex)
connection.ControlPointIndex += e.NewItems.Count;
}
}
for (int i = 0; i < e.NewItems.Count; i++) for (int i = 0; i < e.NewItems.Count; i++)
{ {
var point = (PathControlPoint)e.NewItems[i]; var point = (PathControlPoint)e.NewItems[i];
@ -88,6 +99,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Connections.RemoveAll(c => c.ControlPoint == point); Connections.RemoveAll(c => c.ControlPoint == point);
} }
// If removing before the end of the path,
// update indices of connections after remove location
if (e.OldStartingIndex < Pieces.Count)
{
foreach (var connection in Connections)
{
if (connection.ControlPointIndex >= e.OldStartingIndex)
connection.ControlPointIndex -= e.OldItems.Count;
}
}
break; break;
} }
} }

View File

@ -180,6 +180,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
this.Delay(800).FadeOut(); this.Delay(800).FadeOut();
break; break;
} }
Expire();
} }
public Drawable ProxiedLayer => ApproachCircle; public Drawable ProxiedLayer => ApproachCircle;

View File

@ -11,6 +11,7 @@ using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
@ -60,6 +61,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
PositionBindable.BindTo(HitObject.PositionBindable); PositionBindable.BindTo(HitObject.PositionBindable);
StackHeightBindable.BindTo(HitObject.StackHeightBindable); StackHeightBindable.BindTo(HitObject.StackHeightBindable);
ScaleBindable.BindTo(HitObject.ScaleBindable); ScaleBindable.BindTo(HitObject.ScaleBindable);
// Manually set to reduce the number of future alive objects to a bare minimum.
LifetimeStart = HitObject.StartTime - HitObject.TimePreempt;
// Arbitrary lifetime end to prevent past objects in idle states remaining alive in non-frame-stable contexts.
// An extra 1000ms is added to always overestimate the true lifetime, and a more exact value is set by hit transforms and the following expiry.
LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss) + 1000;
} }
protected override void OnFree(HitObject hitObject) protected override void OnFree(HitObject hitObject)
@ -85,14 +93,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength); public virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength);
protected override void UpdateInitialTransforms()
{
base.UpdateInitialTransforms();
// Manually set to reduce the number of future alive objects to a bare minimum.
LifetimeStart = HitObject.StartTime - HitObject.TimePreempt;
}
/// <summary> /// <summary>
/// Causes this <see cref="DrawableOsuHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>. /// Causes this <see cref="DrawableOsuHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
/// </summary> /// </summary>

View File

@ -80,6 +80,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
foreach (var drawableHitObject in NestedHitObjects) foreach (var drawableHitObject in NestedHitObjects)
drawableHitObject.AccentColour.Value = colour.NewValue; drawableHitObject.AccentColour.Value = colour.NewValue;
updateBallTint();
}, true); }, true);
Tracking.BindValueChanged(updateSlidingSample); Tracking.BindValueChanged(updateSlidingSample);
@ -192,13 +193,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return base.CreateNestedHitObject(hitObject); return base.CreateNestedHitObject(hitObject);
} }
protected override void UpdateInitialTransforms()
{
base.UpdateInitialTransforms();
Body.FadeInFromZero(HitObject.TimeFadeIn);
}
public readonly Bindable<bool> Tracking = new Bindable<bool>(); public readonly Bindable<bool> Tracking = new Bindable<bool>();
protected override void Update() protected override void Update()
@ -244,7 +238,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
base.ApplySkin(skin, allowFallback); base.ApplySkin(skin, allowFallback);
bool allowBallTint = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false; updateBallTint();
}
private void updateBallTint()
{
if (CurrentSkin == null)
return;
bool allowBallTint = CurrentSkin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false;
Ball.AccentColour = allowBallTint ? AccentColour.Value : Color4.White; Ball.AccentColour = allowBallTint ? AccentColour.Value : Color4.White;
} }
@ -264,6 +266,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.PlaySamples(); base.PlaySamples();
} }
protected override void UpdateInitialTransforms()
{
base.UpdateInitialTransforms();
Body.FadeInFromZero(HitObject.TimeFadeIn);
}
protected override void UpdateStartTimeStateTransforms() protected override void UpdateStartTimeStateTransforms()
{ {
base.UpdateStartTimeStateTransforms(); base.UpdateStartTimeStateTransforms();
@ -288,7 +297,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
break; break;
} }
this.FadeOut(fade_out_time, Easing.OutQuint); this.FadeOut(fade_out_time, Easing.OutQuint).Expire();
} }
public Drawable ProxiedLayer => HeadCircle.ProxiedLayer; public Drawable ProxiedLayer => HeadCircle.ProxiedLayer;

View File

@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
base.UpdateHitStateTransforms(state); base.UpdateHitStateTransforms(state);
this.FadeOut(160); this.FadeOut(160).Expire();
// skin change does a rewind of transforms, which will stop the spinning sound from playing if it's currently in playback. // skin change does a rewind of transforms, which will stop the spinning sound from playing if it's currently in playback.
isSpinning?.TriggerChange(); isSpinning?.TriggerChange();

View File

@ -9,7 +9,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Rulesets.Taiko.Tests.csproj", "osu.Game.Rulesets.Taiko.Tests.csproj",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
"-m", "-m",
@ -24,7 +23,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Rulesets.Taiko.Tests.csproj", "osu.Game.Rulesets.Taiko.Tests.csproj",
"-p:Configuration=Release", "-p:Configuration=Release",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
@ -33,15 +31,6 @@
], ],
"group": "build", "group": "build",
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
},
{
"label": "Restore",
"type": "shell",
"command": "dotnet",
"args": [
"restore"
],
"problemMatcher": []
} }
] ]
} }

View File

@ -23,17 +23,15 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.PlayerSettings; using osu.Game.Screens.Play.PlayerSettings;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
public class TestScenePlayerLoader : OsuManualInputManagerTestScene public class TestScenePlayerLoader : ScreenTestScene
{ {
private TestPlayerLoader loader; private TestPlayerLoader loader;
private TestPlayerLoaderContainer container;
private TestPlayer player; private TestPlayer player;
private bool epilepsyWarning; private bool epilepsyWarning;
@ -44,21 +42,46 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved] [Resolved]
private SessionStatics sessionStatics { get; set; } private SessionStatics sessionStatics { get; set; }
[Cached]
private readonly NotificationOverlay notificationOverlay;
[Cached]
private readonly VolumeOverlay volumeOverlay;
private readonly ChangelogOverlay changelogOverlay;
public TestScenePlayerLoader()
{
AddRange(new Drawable[]
{
notificationOverlay = new NotificationOverlay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
},
volumeOverlay = new VolumeOverlay
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
},
changelogOverlay = new ChangelogOverlay()
});
}
[SetUp]
public void Setup() => Schedule(() =>
{
player = null;
audioManager.Volume.SetDefault();
});
/// <summary> /// <summary>
/// Sets the input manager child to a new test player loader container instance. /// Sets the input manager child to a new test player loader container instance.
/// </summary> /// </summary>
/// <param name="interactive">If the test player should behave like the production one.</param> /// <param name="interactive">If the test player should behave like the production one.</param>
/// <param name="beforeLoadAction">An action to run before player load but after bindable leases are returned.</param> /// <param name="beforeLoadAction">An action to run before player load but after bindable leases are returned.</param>
public void ResetPlayer(bool interactive, Action beforeLoadAction = null) private void resetPlayer(bool interactive, Action beforeLoadAction = null)
{ {
player = null;
audioManager.Volume.SetDefault();
InputManager.Clear();
container = new TestPlayerLoaderContainer(loader = new TestPlayerLoader(() => player = new TestPlayer(interactive, interactive)));
beforeLoadAction?.Invoke(); beforeLoadAction?.Invoke();
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
@ -67,13 +90,13 @@ namespace osu.Game.Tests.Visual.Gameplay
foreach (var mod in SelectedMods.Value.OfType<IApplicableToTrack>()) foreach (var mod in SelectedMods.Value.OfType<IApplicableToTrack>())
mod.ApplyToTrack(Beatmap.Value.Track); mod.ApplyToTrack(Beatmap.Value.Track);
InputManager.Child = container; LoadScreen(loader = new TestPlayerLoader(() => player = new TestPlayer(interactive, interactive)));
} }
[Test] [Test]
public void TestEarlyExitBeforePlayerConstruction() public void TestEarlyExitBeforePlayerConstruction()
{ {
AddStep("load dummy beatmap", () => ResetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() })); AddStep("load dummy beatmap", () => resetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() }));
AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddStep("exit loader", () => loader.Exit()); AddStep("exit loader", () => loader.Exit());
AddUntilStep("wait for not current", () => !loader.IsCurrentScreen()); AddUntilStep("wait for not current", () => !loader.IsCurrentScreen());
@ -90,7 +113,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestEarlyExitAfterPlayerConstruction() public void TestEarlyExitAfterPlayerConstruction()
{ {
AddStep("load dummy beatmap", () => ResetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() })); AddStep("load dummy beatmap", () => resetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() }));
AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddAssert("mod rate applied", () => Beatmap.Value.Track.Rate != 1); AddAssert("mod rate applied", () => Beatmap.Value.Track.Rate != 1);
AddUntilStep("wait for non-null player", () => player != null); AddUntilStep("wait for non-null player", () => player != null);
@ -104,7 +127,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestBlockLoadViaMouseMovement() public void TestBlockLoadViaMouseMovement()
{ {
AddStep("load dummy beatmap", () => ResetPlayer(false)); AddStep("load dummy beatmap", () => resetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddUntilStep("wait for load ready", () => AddUntilStep("wait for load ready", () =>
@ -129,20 +152,18 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestBlockLoadViaFocus() public void TestBlockLoadViaFocus()
{ {
OsuFocusedOverlayContainer overlay = null; AddStep("load dummy beatmap", () => resetPlayer(false));
AddStep("load dummy beatmap", () => ResetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddStep("show focused overlay", () => { container.Add(overlay = new ChangelogOverlay { State = { Value = Visibility.Visible } }); }); AddStep("show focused overlay", () => changelogOverlay.Show());
AddUntilStep("overlay visible", () => overlay.IsPresent); AddUntilStep("overlay visible", () => changelogOverlay.IsPresent);
AddUntilStep("wait for load ready", () => player.LoadState == LoadState.Ready); AddUntilStep("wait for load ready", () => player?.LoadState == LoadState.Ready);
AddRepeatStep("twiddle thumbs", () => { }, 20); AddRepeatStep("twiddle thumbs", () => { }, 20);
AddAssert("loader still active", () => loader.IsCurrentScreen()); AddAssert("loader still active", () => loader.IsCurrentScreen());
AddStep("hide overlay", () => overlay.Hide()); AddStep("hide overlay", () => changelogOverlay.Hide());
AddUntilStep("loads after idle", () => !loader.IsCurrentScreen()); AddUntilStep("loads after idle", () => !loader.IsCurrentScreen());
} }
@ -151,15 +172,9 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
SlowLoadPlayer slowPlayer = null; SlowLoadPlayer slowPlayer = null;
AddStep("load dummy beatmap", () => ResetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre));
AddUntilStep("wait for player to be current", () => player.IsCurrentScreen());
AddStep("load slow dummy beatmap", () => AddStep("load slow dummy beatmap", () =>
{ {
InputManager.Child = container = new TestPlayerLoaderContainer( LoadScreen(loader = new TestPlayerLoader(() => slowPlayer = new SlowLoadPlayer(false, false)));
loader = new TestPlayerLoader(() => slowPlayer = new SlowLoadPlayer(false, false)));
Scheduler.AddDelayed(() => slowPlayer.AllowLoad.Set(), 5000); Scheduler.AddDelayed(() => slowPlayer.AllowLoad.Set(), 5000);
}); });
@ -173,7 +188,7 @@ namespace osu.Game.Tests.Visual.Gameplay
TestMod playerMod1 = null; TestMod playerMod1 = null;
TestMod playerMod2 = null; TestMod playerMod2 = null;
AddStep("load player", () => { ResetPlayer(true, () => SelectedMods.Value = new[] { gameMod = new TestMod() }); }); AddStep("load player", () => { resetPlayer(true, () => SelectedMods.Value = new[] { gameMod = new TestMod() }); });
AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen()); AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen());
AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre)); AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre));
@ -201,7 +216,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
var testMod = new TestMod(); var testMod = new TestMod();
AddStep("load player", () => ResetPlayer(true)); AddStep("load player", () => resetPlayer(true));
AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen()); AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen());
AddStep("set test mod in loader", () => loader.Mods.Value = new[] { testMod }); AddStep("set test mod in loader", () => loader.Mods.Value = new[] { testMod });
@ -223,7 +238,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestMutedNotificationMuteButton() public void TestMutedNotificationMuteButton()
{ {
addVolumeSteps("mute button", () => container.VolumeOverlay.IsMuted.Value = true, () => !container.VolumeOverlay.IsMuted.Value); addVolumeSteps("mute button", () => volumeOverlay.IsMuted.Value = true, () => !volumeOverlay.IsMuted.Value);
} }
/// <remarks> /// <remarks>
@ -236,13 +251,13 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
AddStep("reset notification lock", () => sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce).Value = false); AddStep("reset notification lock", () => sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce).Value = false);
AddStep("load player", () => ResetPlayer(false, beforeLoad)); AddStep("load player", () => resetPlayer(false, beforeLoad));
AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready); AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready);
AddAssert("check for notification", () => container.NotificationOverlay.UnreadCount.Value == 1); AddAssert("check for notification", () => notificationOverlay.UnreadCount.Value == 1);
AddStep("click notification", () => AddStep("click notification", () =>
{ {
var scrollContainer = (OsuScrollContainer)container.NotificationOverlay.Children.Last(); var scrollContainer = (OsuScrollContainer)notificationOverlay.Children.Last();
var flowContainer = scrollContainer.Children.OfType<FillFlowContainer<NotificationSection>>().First(); var flowContainer = scrollContainer.Children.OfType<FillFlowContainer<NotificationSection>>().First();
var notification = flowContainer.First(); var notification = flowContainer.First();
@ -260,7 +275,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestEpilepsyWarning(bool warning) public void TestEpilepsyWarning(bool warning)
{ {
AddStep("change epilepsy warning", () => epilepsyWarning = warning); AddStep("change epilepsy warning", () => epilepsyWarning = warning);
AddStep("load dummy beatmap", () => ResetPlayer(false)); AddStep("load dummy beatmap", () => resetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddUntilStep("wait for current", () => loader.IsCurrentScreen());
@ -277,7 +292,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestEpilepsyWarningEarlyExit() public void TestEpilepsyWarningEarlyExit()
{ {
AddStep("set epilepsy warning", () => epilepsyWarning = true); AddStep("set epilepsy warning", () => epilepsyWarning = true);
AddStep("load dummy beatmap", () => ResetPlayer(false)); AddStep("load dummy beatmap", () => resetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddUntilStep("wait for current", () => loader.IsCurrentScreen());
@ -287,42 +302,6 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1);
} }
private class TestPlayerLoaderContainer : Container
{
[Cached]
public readonly NotificationOverlay NotificationOverlay;
[Cached]
public readonly VolumeOverlay VolumeOverlay;
public TestPlayerLoaderContainer(IScreen screen)
{
RelativeSizeAxes = Axes.Both;
OsuScreenStack stack;
InternalChildren = new Drawable[]
{
stack = new OsuScreenStack
{
RelativeSizeAxes = Axes.Both,
},
NotificationOverlay = new NotificationOverlay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
},
VolumeOverlay = new VolumeOverlay
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
}
};
stack.Push(screen);
}
}
private class TestPlayerLoader : PlayerLoader private class TestPlayerLoader : PlayerLoader
{ {
public new VisualSettings VisualSettings => base.VisualSettings; public new VisualSettings VisualSettings => base.VisualSettings;

View File

@ -9,7 +9,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Tournament.Tests.csproj", "osu.Game.Tournament.Tests.csproj",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
"-m", "-m",
@ -24,7 +23,6 @@
"command": "dotnet", "command": "dotnet",
"args": [ "args": [
"build", "build",
"--no-restore",
"osu.Game.Tournament.Tests.csproj", "osu.Game.Tournament.Tests.csproj",
"-p:Configuration=Release", "-p:Configuration=Release",
"-p:GenerateFullPaths=true", "-p:GenerateFullPaths=true",
@ -33,15 +31,6 @@
], ],
"group": "build", "group": "build",
"problemMatcher": "$msCompile" "problemMatcher": "$msCompile"
},
{
"label": "Restore",
"type": "shell",
"command": "dotnet",
"args": [
"restore"
],
"problemMatcher": []
} }
] ]
} }

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 System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@ -15,103 +14,93 @@ namespace osu.Game.Database
{ {
public class UserLookupCache : MemoryCachingComponent<int, User> public class UserLookupCache : MemoryCachingComponent<int, User>
{ {
private readonly HashSet<int> nextTaskIDs = new HashSet<int>();
[Resolved] [Resolved]
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; }
private readonly object taskAssignmentLock = new object();
private Task<List<User>> pendingRequest;
/// <summary>
/// Whether <see cref="pendingRequest"/> has already grabbed its IDs.
/// </summary>
private bool pendingRequestConsumedIDs;
public Task<User> GetUserAsync(int userId, CancellationToken token = default) => GetAsync(userId, token); public Task<User> GetUserAsync(int userId, CancellationToken token = default) => GetAsync(userId, token);
protected override async Task<User> ComputeValueAsync(int lookup, CancellationToken token = default) protected override async Task<User> ComputeValueAsync(int lookup, CancellationToken token = default)
{ => await queryUser(lookup);
var users = await getQueryTaskForUser(lookup);
return users.FirstOrDefault(u => u.Id == lookup);
}
/// <summary> private readonly Queue<(int id, TaskCompletionSource<User>)> pendingUserTasks = new Queue<(int, TaskCompletionSource<User>)>();
/// Return the task responsible for fetching the provided user. private Task pendingRequestTask;
/// This may be part of a larger batch lookup to reduce web requests. private readonly object taskAssignmentLock = new object();
/// </summary>
/// <param name="userId">The user to lookup.</param> private Task<User> queryUser(int userId)
/// <returns>The task responsible for the lookup.</returns>
private Task<List<User>> getQueryTaskForUser(int userId)
{ {
lock (taskAssignmentLock) lock (taskAssignmentLock)
{ {
nextTaskIDs.Add(userId); var tcs = new TaskCompletionSource<User>();
// if there's a pending request which hasn't been started yet (and is not yet full), we can wait on it. // Add to the queue.
if (pendingRequest != null && !pendingRequestConsumedIDs && nextTaskIDs.Count < 50) pendingUserTasks.Enqueue((userId, tcs));
return pendingRequest;
return queueNextTask(nextLookup); // Create a request task if there's not already one.
if (pendingRequestTask == null)
createNewTask();
return tcs.Task;
} }
}
List<User> nextLookup() private void performLookup()
{
// contains at most 50 unique user IDs from userTasks, which is used to perform the lookup.
var userTasks = new Dictionary<int, List<TaskCompletionSource<User>>>();
// Grab at most 50 unique user IDs from the queue.
lock (taskAssignmentLock)
{ {
int[] lookupItems; while (pendingUserTasks.Count > 0 && userTasks.Count < 50)
lock (taskAssignmentLock)
{ {
pendingRequestConsumedIDs = true; (int id, TaskCompletionSource<User> task) next = pendingUserTasks.Dequeue();
lookupItems = nextTaskIDs.ToArray();
nextTaskIDs.Clear();
if (lookupItems.Length == 0) // Perform a secondary check for existence, in case the user was queried in a previous batch.
if (CheckExists(next.id, out var existing))
next.task.SetResult(existing);
else
{ {
queueNextTask(null); if (userTasks.TryGetValue(next.id, out var tasks))
return new List<User>(); tasks.Add(next.task);
else
userTasks[next.id] = new List<TaskCompletionSource<User>> { next.task };
} }
} }
var request = new GetUsersRequest(lookupItems);
// rather than queueing, we maintain our own single-threaded request stream.
api.Perform(request);
return request.Result?.Users;
} }
}
/// <summary> // Query the users.
/// Queues new work at the end of the current work tasks. var request = new GetUsersRequest(userTasks.Keys.ToArray());
/// Ensures the provided work is eventually run.
/// </summary> // rather than queueing, we maintain our own single-threaded request stream.
/// <param name="work">The work to run. Can be null to signify the end of available work.</param> api.Perform(request);
/// <returns>The task tracking this work.</returns>
private Task<List<User>> queueNextTask(Func<List<User>> work) // Create a new request task if there's still more users to query.
{
lock (taskAssignmentLock) lock (taskAssignmentLock)
{ {
if (work == null) pendingRequestTask = null;
{ if (pendingUserTasks.Count > 0)
pendingRequest = null; createNewTask();
pendingRequestConsumedIDs = false; }
}
else if (pendingRequest == null)
{
// special case for the first request ever.
pendingRequest = Task.Run(work);
pendingRequestConsumedIDs = false;
}
else
{
// append the new request on to the last to be executed.
pendingRequest = pendingRequest.ContinueWith(_ => work());
pendingRequestConsumedIDs = false;
}
return pendingRequest; foreach (var user in request.Result.Users)
{
if (userTasks.TryGetValue(user.Id, out var tasks))
{
foreach (var task in tasks)
task.SetResult(user);
userTasks.Remove(user.Id);
}
}
// if any tasks remain which were not satisfied, return null.
foreach (var tasks in userTasks.Values)
{
foreach (var task in tasks)
task.SetResult(null);
} }
} }
private void createNewTask() => pendingRequestTask = Task.Run(performLookup);
} }
} }

View File

@ -194,6 +194,20 @@ namespace osu.Game
dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Audio, new NamespacedResourceStore<byte[]>(Resources, "Skins/Legacy"))); dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Audio, new NamespacedResourceStore<byte[]>(Resources, "Skins/Legacy")));
dependencies.CacheAs<ISkinSource>(SkinManager); dependencies.CacheAs<ISkinSource>(SkinManager);
// needs to be done here rather than inside SkinManager to ensure thread safety of CurrentSkinInfo.
SkinManager.ItemRemoved.BindValueChanged(weakRemovedInfo =>
{
if (weakRemovedInfo.NewValue.TryGetTarget(out var removedInfo))
{
Schedule(() =>
{
// check the removed skin is not the current user choice. if it is, switch back to default.
if (removedInfo.ID == SkinManager.CurrentSkinInfo.Value.ID)
SkinManager.CurrentSkinInfo.Value = SkinInfo.Default;
});
}
});
dependencies.CacheAs(API ??= new APIAccess(LocalConfig)); dependencies.CacheAs(API ??= new APIAccess(LocalConfig));
dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient()); dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient());

View File

@ -201,6 +201,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
// Copy any existing result from the entry (required for rewind / judgement revert). // Copy any existing result from the entry (required for rewind / judgement revert).
Result = lifetimeEntry.Result; Result = lifetimeEntry.Result;
} }
else
LifetimeStart = HitObject.StartTime - InitialLifetimeOffset;
// Ensure this DHO has a result. // Ensure this DHO has a result.
Result ??= CreateResult(HitObject.CreateJudgement()) Result ??= CreateResult(HitObject.CreateJudgement())
@ -646,6 +648,10 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// This is only used as an optimisation to delay the initial update of this <see cref="DrawableHitObject"/> and may be tuned more aggressively if required. /// This is only used as an optimisation to delay the initial update of this <see cref="DrawableHitObject"/> and may be tuned more aggressively if required.
/// It is indirectly used to decide the automatic transform offset provided to <see cref="UpdateInitialTransforms"/>. /// It is indirectly used to decide the automatic transform offset provided to <see cref="UpdateInitialTransforms"/>.
/// A more accurate <see cref="LifetimeStart"/> should be set for further optimisation (in <see cref="LoadComplete"/>, for example). /// A more accurate <see cref="LifetimeStart"/> should be set for further optimisation (in <see cref="LoadComplete"/>, for example).
/// <para>
/// Only has an effect if this <see cref="DrawableHitObject"/> is not being pooled.
/// For pooled <see cref="DrawableHitObject"/>s, use <see cref="HitObjectLifetimeEntry.InitialLifetimeOffset"/> instead.
/// </para>
/// </remarks> /// </remarks>
protected virtual double InitialLifetimeOffset => 10000; protected virtual double InitialLifetimeOffset => 10000;

View File

@ -10,14 +10,6 @@ namespace osu.Game.Rulesets.Objects
{ {
public static class SliderEventGenerator public static class SliderEventGenerator
{ {
[Obsolete("Use the overload with cancellation support instead.")] // can be removed 20201115
// ReSharper disable once RedundantOverload.Global
public static IEnumerable<SliderEventDescriptor> Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount,
double? legacyLastTickOffset)
{
return Generate(startTime, spanDuration, velocity, tickDistance, totalDistance, spanCount, legacyLastTickOffset, default);
}
// ReSharper disable once MethodOverloadWithOptionalParameter // ReSharper disable once MethodOverloadWithOptionalParameter
public static IEnumerable<SliderEventDescriptor> Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount, public static IEnumerable<SliderEventDescriptor> Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount,
double? legacyLastTickOffset, CancellationToken cancellationToken = default) double? legacyLastTickOffset, CancellationToken cancellationToken = default)

View File

@ -218,9 +218,6 @@ namespace osu.Game.Rulesets.UI
#region Pooling support #region Pooling support
[Resolved(CanBeNull = true)]
private IPooledHitObjectProvider parentPooledObjectProvider { get; set; }
private readonly Dictionary<Type, IDrawablePool> pools = new Dictionary<Type, IDrawablePool>(); private readonly Dictionary<Type, IDrawablePool> pools = new Dictionary<Type, IDrawablePool>();
/// <summary> /// <summary>
@ -320,10 +317,7 @@ namespace osu.Game.Rulesets.UI
} }
} }
if (pool == null) return (DrawableHitObject)pool?.Get(d =>
return parentPooledObjectProvider?.GetPooledDrawableRepresentation(hitObject);
return (DrawableHitObject)pool.Get(d =>
{ {
var dho = (DrawableHitObject)d; var dho = (DrawableHitObject)d;

View File

@ -118,8 +118,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
} }
protected virtual Container<SelectionBlueprint> CreateSelectionBlueprintContainer() => protected virtual Container<SelectionBlueprint> CreateSelectionBlueprintContainer() => new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both };
new Container<SelectionBlueprint> { RelativeSizeAxes = Axes.Both };
/// <summary> /// <summary>
/// Creates a <see cref="Components.SelectionHandler"/> which outlines <see cref="DrawableHitObject"/>s and handles movement of selections. /// Creates a <see cref="Components.SelectionHandler"/> which outlines <see cref="DrawableHitObject"/>s and handles movement of selections.
@ -338,7 +337,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <returns>Whether a selection was performed.</returns> /// <returns>Whether a selection was performed.</returns>
private bool beginClickSelection(MouseButtonEvent e) private bool beginClickSelection(MouseButtonEvent e)
{ {
foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren) // Iterate from the top of the input stack (blueprints closest to the front of the screen first).
foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse())
{ {
if (!blueprint.IsHovered) continue; if (!blueprint.IsHovered) continue;

View File

@ -0,0 +1,77 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Screens.Edit.Compose.Components
{
/// <summary>
/// A container for <see cref="SelectionBlueprint"/> ordered by their <see cref="HitObject"/> start times.
/// </summary>
public sealed class HitObjectOrderedSelectionContainer : Container<SelectionBlueprint>
{
public override void Add(SelectionBlueprint drawable)
{
base.Add(drawable);
bindStartTime(drawable);
}
public override bool Remove(SelectionBlueprint drawable)
{
if (!base.Remove(drawable))
return false;
unbindStartTime(drawable);
return true;
}
public override void Clear(bool disposeChildren)
{
base.Clear(disposeChildren);
unbindAllStartTimes();
}
private readonly Dictionary<SelectionBlueprint, IBindable> startTimeMap = new Dictionary<SelectionBlueprint, IBindable>();
private void bindStartTime(SelectionBlueprint blueprint)
{
var bindable = blueprint.HitObject.StartTimeBindable.GetBoundCopy();
bindable.BindValueChanged(_ =>
{
if (LoadState >= LoadState.Ready)
SortInternal();
});
startTimeMap[blueprint] = bindable;
}
private void unbindStartTime(SelectionBlueprint blueprint)
{
startTimeMap[blueprint].UnbindAll();
startTimeMap.Remove(blueprint);
}
private void unbindAllStartTimes()
{
foreach (var kvp in startTimeMap)
kvp.Value.UnbindAll();
startTimeMap.Clear();
}
protected override int Compare(Drawable x, Drawable y)
{
var xObj = (SelectionBlueprint)x;
var yObj = (SelectionBlueprint)y;
// Put earlier blueprints towards the end of the list, so they handle input first
int i = yObj.HitObject.StartTime.CompareTo(xObj.HitObject.StartTime);
return i == 0 ? CompareReverseChildID(x, y) : i;
}
}
}

View File

@ -201,7 +201,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
public TimelineSelectionBlueprintContainer() public TimelineSelectionBlueprintContainer()
{ {
AddInternal(new TimelinePart<SelectionBlueprint>(Content = new Container<SelectionBlueprint> { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both }); AddInternal(new TimelinePart<SelectionBlueprint>(Content = new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both });
} }
} }
} }

View File

@ -48,16 +48,6 @@ namespace osu.Game.Skinning
this.audio = audio; this.audio = audio;
this.legacyDefaultResources = legacyDefaultResources; this.legacyDefaultResources = legacyDefaultResources;
ItemRemoved.BindValueChanged(weakRemovedInfo =>
{
if (weakRemovedInfo.NewValue.TryGetTarget(out var removedInfo))
{
// check the removed skin is not the current user choice. if it is, switch back to default.
if (removedInfo.ID == CurrentSkinInfo.Value.ID)
CurrentSkinInfo.Value = SkinInfo.Default;
}
});
CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = GetSkin(skin.NewValue); CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = GetSkin(skin.NewValue);
CurrentSkin.ValueChanged += skin => CurrentSkin.ValueChanged += skin =>
{ {

View File

@ -26,7 +26,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1113.0" /> <PackageReference Include="ppy.osu.Framework" Version="2020.1118.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" />
<PackageReference Include="Sentry" Version="2.1.6" /> <PackageReference Include="Sentry" Version="2.1.6" />
<PackageReference Include="SharpCompress" Version="0.26.0" /> <PackageReference Include="SharpCompress" Version="0.26.0" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1113.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1118.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.1030.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) --> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@ -88,7 +88,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1113.0" /> <PackageReference Include="ppy.osu.Framework" Version="2020.1118.0" />
<PackageReference Include="SharpCompress" Version="0.26.0" /> <PackageReference Include="SharpCompress" Version="0.26.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />