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

Merge branch 'master' into fix-judgement-transform-logic

This commit is contained in:
Bartłomiej Dach 2020-11-18 19:20:11 +01:00 committed by GitHub
commit 5156de3a10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 434 additions and 358 deletions

17
.vscode/tasks.json vendored
View File

@ -9,7 +9,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Desktop",
"-p:GenerateFullPaths=true",
"-m",
@ -24,7 +23,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Desktop",
"-p:Configuration=Release",
"-p:GenerateFullPaths=true",
@ -40,7 +38,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Tests",
"-p:GenerateFullPaths=true",
"-m",
@ -55,7 +52,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Tests",
"-p:Configuration=Release",
"-p:GenerateFullPaths=true",
@ -71,7 +67,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Tournament.Tests",
"-p:GenerateFullPaths=true",
"-m",
@ -86,7 +81,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Tournament.Tests",
"-p:Configuration=Release",
"-p:GenerateFullPaths=true",
@ -102,7 +96,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Benchmarks",
"-p:Configuration=Release",
"-p:GenerateFullPaths=true",
@ -111,16 +104,6 @@
],
"group": "build",
"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).
- 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:

View File

@ -52,6 +52,6 @@
</ItemGroup>
<ItemGroup>
<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>
</Project>

View File

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

View File

@ -9,7 +9,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Rulesets.Catch.Tests.csproj",
"-p:GenerateFullPaths=true",
"-m",
@ -24,7 +23,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Rulesets.Catch.Tests.csproj",
"-p:Configuration=Release",
"-p:GenerateFullPaths=true",
@ -33,15 +31,6 @@
],
"group": "build",
"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();
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 tiny droplet", () => SetContents(createDrawableTinyDroplet));
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)));
}
private Drawable createDrawableTinyDroplet()
{
var droplet = new TestCatchTinyDroplet
{
Scale = 1.5f,
};
private Drawable createDrawableFruit(FruitVisualRepresentation rep, bool hyperdash = false) =>
setProperties(new DrawableFruit(new TestCatchFruit(rep)), hyperdash);
return new DrawableTinyDroplet(droplet)
{
Anchor = Anchor.Centre,
RelativePositionAxes = Axes.None,
Position = Vector2.Zero,
Alpha = 1,
LifetimeStart = double.NegativeInfinity,
LifetimeEnd = double.PositiveInfinity,
};
}
private Drawable createDrawableDroplet(bool hyperdash = false) => setProperties(new DrawableDroplet(new Droplet()), hyperdash);
private Drawable createDrawableDroplet(bool hyperdash = false)
{
var droplet = new TestCatchDroplet
{
Scale = 1.5f,
HyperDashTarget = hyperdash ? new Banana() : null
};
private Drawable createDrawableTinyDroplet() => setProperties(new DrawableTinyDroplet(new TinyDroplet()));
return new DrawableDroplet(droplet)
private DrawableCatchHitObject setProperties(DrawableCatchHitObject d, bool hyperdash = false)
{
Anchor = Anchor.Centre,
RelativePositionAxes = Axes.None,
Position = Vector2.Zero,
Alpha = 1,
LifetimeStart = double.NegativeInfinity,
LifetimeEnd = double.PositiveInfinity,
};
}
var hitObject = d.HitObject;
hitObject.StartTime = 1000000000000;
hitObject.Scale = 1.5f;
private Drawable createDrawable(FruitVisualRepresentation rep, bool hyperdash = false)
{
Fruit fruit = new TestCatchFruit(rep)
{
Scale = 1.5f,
HyperDashTarget = hyperdash ? new Banana() : null
};
if (hyperdash)
hitObject.HyperDashTarget = new Banana();
return new DrawableFruit(fruit)
d.Anchor = Anchor.Centre;
d.RelativePositionAxes = Axes.None;
d.Position = Vector2.Zero;
d.HitObjectApplied += _ =>
{
Anchor = Anchor.Centre,
RelativePositionAxes = Axes.None,
Position = Vector2.Zero,
Alpha = 1,
LifetimeStart = double.NegativeInfinity,
LifetimeEnd = double.PositiveInfinity,
d.LifetimeStart = double.NegativeInfinity;
d.LifetimeEnd = double.PositiveInfinity;
};
return d;
}
public class TestCatchFruit : Fruit
@ -90,26 +61,9 @@ namespace osu.Game.Rulesets.Catch.Tests
public TestCatchFruit(FruitVisualRepresentation rep)
{
VisualRepresentation = rep;
StartTime = 1000000000000;
}
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",
"args": [
"build",
"--no-restore",
"osu.Game.Rulesets.Mania.Tests.csproj",
"-p:GenerateFullPaths=true",
"-m",
@ -24,7 +23,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Rulesets.Mania.Tests.csproj",
"-p:Configuration=Release",
"-p:GenerateFullPaths=true",
@ -33,15 +31,6 @@
],
"group": "build",
"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.
// 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.Containers;
using osu.Game.Rulesets.Mania.Objects.Drawables;
@ -15,8 +15,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
/// </summary>
public abstract class ManiaHitObjectTestScene : ManiaSkinnableTestScene
{
[BackgroundDependencyLoader]
private void load()
[SetUp]
public void SetUp() => Schedule(() =>
{
SetContents(() => new FillFlowContainer
{
@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
},
}
});
}
});
protected abstract DrawableManiaHitObject CreateHitObject();
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
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()
{
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; }
/// <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>
public bool HasBroken { get; private set; }
public double? HoldBrokenTime { get; private set; }
/// <summary>
/// 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)
HasBroken = true;
HoldBrokenTime = Time.Current;
}
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 (!Tail.IsHit)
HasBroken = true;
HoldBrokenTime = Time.Current;
releaseTime = Time.Current;
}

View File

@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
ApplyResult(r =>
{
// 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;
r.Type = result;

View File

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

View File

@ -18,9 +18,17 @@ namespace osu.Game.Rulesets.Mania.Skinning
{
public class LegacyBodyPiece : LegacyManiaColumnElement
{
private DrawableHoldNote holdNote;
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
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]
private Drawable bodySprite;
@ -38,6 +46,8 @@ namespace osu.Game.Rulesets.Mania.Skinning
[BackgroundDependencyLoader]
private void load(ISkinSource skin, IScrollingInfo scrollingInfo, DrawableHitObject drawableObject)
{
holdNote = (DrawableHoldNote)drawableObject;
string imageName = GetColumnSkinConfig<string>(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value
?? $"mania-note{FallbackColumnIndex}L";
@ -92,11 +102,26 @@ namespace osu.Game.Rulesets.Mania.Skinning
InternalChild = bodySprite;
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
var holdNote = (DrawableHoldNote)drawableObject;
isHitting.BindTo(holdNote.IsHitting);
}
protected override void LoadComplete()
{
base.LoadComplete();
direction.BindValueChanged(onDirectionChanged, 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)
@ -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)
{
base.Dispose(isDisposing);
if (holdNote != null)
holdNote.ApplyCustomUpdateState -= applyCustomUpdateState;
lightContainer?.Expire();
}
}

View File

@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Mania.UI
if (result.IsHit)
hitPolicy.HandleHit(judgedObject);
if (!result.IsHit || !DisplayJudgements.Value)
if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
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)
return;
// Tick judgements should not display text.
if (judgedObject is DrawableHoldNoteTick)
return;
judgements.Clear(false);
judgements.Add(judgementPool.Get(j =>
{

View File

@ -9,7 +9,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Rulesets.Osu.Tests.csproj",
"-p:GenerateFullPaths=true",
"-m",
@ -24,7 +23,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Rulesets.Osu.Tests.csproj",
"-p:Configuration=Release",
"-p:GenerateFullPaths=true",
@ -33,15 +31,6 @@
],
"group": "build",
"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.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
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 osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneSliderApplication : OsuTestScene
{
[Resolved]
private SkinManager skinManager { get; set; }
[Test]
public void TestApplyNewSlider()
{
@ -50,6 +60,41 @@ namespace osu.Game.Rulesets.Osu.Tests
}), 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)
{
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 Slider slider;
private readonly int controlPointIndex;
public int ControlPointIndex { get; set; }
private IBindable<Vector2> sliderPosition;
private IBindable<int> pathVersion;
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public PathControlPointConnectionPiece(Slider slider, int controlPointIndex)
{
this.slider = slider;
this.controlPointIndex = controlPointIndex;
ControlPointIndex = controlPointIndex;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
path.ClearVertices();
int nextIndex = controlPointIndex + 1;
int nextIndex = ControlPointIndex + 1;
if (nextIndex == 0 || nextIndex >= slider.Path.ControlPoints.Count)
return;

View File

@ -66,6 +66,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
switch (e.Action)
{
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++)
{
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);
}
// 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;
}
}

View File

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

View File

@ -11,6 +11,7 @@ using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
@ -60,6 +61,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
PositionBindable.BindTo(HitObject.PositionBindable);
StackHeightBindable.BindTo(HitObject.StackHeightBindable);
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)
@ -85,14 +93,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
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>
/// Causes this <see cref="DrawableOsuHitObject"/> to get missed, disregarding all conditions in implementations of <see cref="DrawableHitObject.CheckForResult"/>.
/// </summary>

View File

@ -80,6 +80,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
foreach (var drawableHitObject in NestedHitObjects)
drawableHitObject.AccentColour.Value = colour.NewValue;
updateBallTint();
}, true);
Tracking.BindValueChanged(updateSlidingSample);
@ -192,13 +193,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return base.CreateNestedHitObject(hitObject);
}
protected override void UpdateInitialTransforms()
{
base.UpdateInitialTransforms();
Body.FadeInFromZero(HitObject.TimeFadeIn);
}
public readonly Bindable<bool> Tracking = new Bindable<bool>();
protected override void Update()
@ -244,7 +238,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
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;
}
@ -264,6 +266,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.PlaySamples();
}
protected override void UpdateInitialTransforms()
{
base.UpdateInitialTransforms();
Body.FadeInFromZero(HitObject.TimeFadeIn);
}
protected override void UpdateStartTimeStateTransforms()
{
base.UpdateStartTimeStateTransforms();
@ -288,7 +297,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
break;
}
this.FadeOut(fade_out_time, Easing.OutQuint);
this.FadeOut(fade_out_time, Easing.OutQuint).Expire();
}
public Drawable ProxiedLayer => HeadCircle.ProxiedLayer;

View File

@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
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.
isSpinning?.TriggerChange();

View File

@ -9,7 +9,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Rulesets.Taiko.Tests.csproj",
"-p:GenerateFullPaths=true",
"-m",
@ -24,7 +23,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Rulesets.Taiko.Tests.csproj",
"-p:Configuration=Release",
"-p:GenerateFullPaths=true",
@ -33,15 +31,6 @@
],
"group": "build",
"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.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.PlayerSettings;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestScenePlayerLoader : OsuManualInputManagerTestScene
public class TestScenePlayerLoader : ScreenTestScene
{
private TestPlayerLoader loader;
private TestPlayerLoaderContainer container;
private TestPlayer player;
private bool epilepsyWarning;
@ -44,21 +42,46 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved]
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>
/// Sets the input manager child to a new test player loader container instance.
/// </summary>
/// <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>
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();
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
@ -67,13 +90,13 @@ namespace osu.Game.Tests.Visual.Gameplay
foreach (var mod in SelectedMods.Value.OfType<IApplicableToTrack>())
mod.ApplyToTrack(Beatmap.Value.Track);
InputManager.Child = container;
LoadScreen(loader = new TestPlayerLoader(() => player = new TestPlayer(interactive, interactive)));
}
[Test]
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());
AddStep("exit loader", () => loader.Exit());
AddUntilStep("wait for not current", () => !loader.IsCurrentScreen());
@ -90,7 +113,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
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());
AddAssert("mod rate applied", () => Beatmap.Value.Track.Rate != 1);
AddUntilStep("wait for non-null player", () => player != null);
@ -104,7 +127,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestBlockLoadViaMouseMovement()
{
AddStep("load dummy beatmap", () => ResetPlayer(false));
AddStep("load dummy beatmap", () => resetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddUntilStep("wait for load ready", () =>
@ -129,20 +152,18 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestBlockLoadViaFocus()
{
OsuFocusedOverlayContainer overlay = null;
AddStep("load dummy beatmap", () => ResetPlayer(false));
AddStep("load dummy beatmap", () => resetPlayer(false));
AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddStep("show focused overlay", () => { container.Add(overlay = new ChangelogOverlay { State = { Value = Visibility.Visible } }); });
AddUntilStep("overlay visible", () => overlay.IsPresent);
AddStep("show focused overlay", () => changelogOverlay.Show());
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);
AddAssert("loader still active", () => loader.IsCurrentScreen());
AddStep("hide overlay", () => overlay.Hide());
AddStep("hide overlay", () => changelogOverlay.Hide());
AddUntilStep("loads after idle", () => !loader.IsCurrentScreen());
}
@ -151,15 +172,9 @@ namespace osu.Game.Tests.Visual.Gameplay
{
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", () =>
{
InputManager.Child = container = new TestPlayerLoaderContainer(
loader = new TestPlayerLoader(() => slowPlayer = new SlowLoadPlayer(false, false)));
LoadScreen(loader = new TestPlayerLoader(() => slowPlayer = new SlowLoadPlayer(false, false)));
Scheduler.AddDelayed(() => slowPlayer.AllowLoad.Set(), 5000);
});
@ -173,7 +188,7 @@ namespace osu.Game.Tests.Visual.Gameplay
TestMod playerMod1 = 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());
AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre));
@ -201,7 +216,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
var testMod = new TestMod();
AddStep("load player", () => ResetPlayer(true));
AddStep("load player", () => resetPlayer(true));
AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen());
AddStep("set test mod in loader", () => loader.Mods.Value = new[] { testMod });
@ -223,7 +238,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
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>
@ -236,13 +251,13 @@ namespace osu.Game.Tests.Visual.Gameplay
{
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);
AddAssert("check for notification", () => container.NotificationOverlay.UnreadCount.Value == 1);
AddAssert("check for notification", () => notificationOverlay.UnreadCount.Value == 1);
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 notification = flowContainer.First();
@ -260,7 +275,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestEpilepsyWarning(bool 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());
@ -277,7 +292,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestEpilepsyWarningEarlyExit()
{
AddStep("set epilepsy warning", () => epilepsyWarning = true);
AddStep("load dummy beatmap", () => ResetPlayer(false));
AddStep("load dummy beatmap", () => resetPlayer(false));
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);
}
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
{
public new VisualSettings VisualSettings => base.VisualSettings;

View File

@ -9,7 +9,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Tournament.Tests.csproj",
"-p:GenerateFullPaths=true",
"-m",
@ -24,7 +23,6 @@
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Tournament.Tests.csproj",
"-p:Configuration=Release",
"-p:GenerateFullPaths=true",
@ -33,15 +31,6 @@
],
"group": "build",
"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.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@ -15,103 +14,93 @@ namespace osu.Game.Database
{
public class UserLookupCache : MemoryCachingComponent<int, User>
{
private readonly HashSet<int> nextTaskIDs = new HashSet<int>();
[Resolved]
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);
protected override async Task<User> ComputeValueAsync(int lookup, CancellationToken token = default)
{
var users = await getQueryTaskForUser(lookup);
return users.FirstOrDefault(u => u.Id == lookup);
}
=> await queryUser(lookup);
/// <summary>
/// Return the task responsible for fetching the provided user.
/// This may be part of a larger batch lookup to reduce web requests.
/// </summary>
/// <param name="userId">The user to lookup.</param>
/// <returns>The task responsible for the lookup.</returns>
private Task<List<User>> getQueryTaskForUser(int userId)
private readonly Queue<(int id, TaskCompletionSource<User>)> pendingUserTasks = new Queue<(int, TaskCompletionSource<User>)>();
private Task pendingRequestTask;
private readonly object taskAssignmentLock = new object();
private Task<User> queryUser(int userId)
{
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.
if (pendingRequest != null && !pendingRequestConsumedIDs && nextTaskIDs.Count < 50)
return pendingRequest;
// Add to the queue.
pendingUserTasks.Enqueue((userId, tcs));
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()
{
int[] lookupItems;
// 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)
{
pendingRequestConsumedIDs = true;
lookupItems = nextTaskIDs.ToArray();
nextTaskIDs.Clear();
if (lookupItems.Length == 0)
while (pendingUserTasks.Count > 0 && userTasks.Count < 50)
{
queueNextTask(null);
return new List<User>();
(int id, TaskCompletionSource<User> task) next = pendingUserTasks.Dequeue();
// 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
{
if (userTasks.TryGetValue(next.id, out var tasks))
tasks.Add(next.task);
else
userTasks[next.id] = new List<TaskCompletionSource<User>> { next.task };
}
}
}
var request = new GetUsersRequest(lookupItems);
// Query the users.
var request = new GetUsersRequest(userTasks.Keys.ToArray());
// rather than queueing, we maintain our own single-threaded request stream.
api.Perform(request);
return request.Result?.Users;
}
}
/// <summary>
/// Queues new work at the end of the current work tasks.
/// Ensures the provided work is eventually run.
/// </summary>
/// <param name="work">The work to run. Can be null to signify the end of available work.</param>
/// <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)
{
if (work == null)
{
pendingRequest = null;
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;
pendingRequestTask = null;
if (pendingUserTasks.Count > 0)
createNewTask();
}
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.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(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).
Result = lifetimeEntry.Result;
}
else
LifetimeStart = HitObject.StartTime - InitialLifetimeOffset;
// Ensure this DHO has a result.
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.
/// 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).
/// <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>
protected virtual double InitialLifetimeOffset => 10000;

View File

@ -10,14 +10,6 @@ namespace osu.Game.Rulesets.Objects
{
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
public static IEnumerable<SliderEventDescriptor> Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount,
double? legacyLastTickOffset, CancellationToken cancellationToken = default)

View File

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

View File

@ -118,8 +118,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
}
protected virtual Container<SelectionBlueprint> CreateSelectionBlueprintContainer() =>
new Container<SelectionBlueprint> { RelativeSizeAxes = Axes.Both };
protected virtual Container<SelectionBlueprint> CreateSelectionBlueprintContainer() => new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both };
/// <summary>
/// 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>
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;

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()
{
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.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);
CurrentSkin.ValueChanged += skin =>
{

View File

@ -24,7 +24,15 @@ namespace osu.Game.Skinning
{
}
protected override Drawable CreateDefault(ISkinComponent component) => new Sprite { Texture = textures.Get(component.LookupName) };
protected override Drawable CreateDefault(ISkinComponent component)
{
var texture = textures.Get(component.LookupName);
if (texture == null)
return null;
return new Sprite { Texture = texture };
}
private class SpriteComponent : ISkinComponent
{

View File

@ -26,7 +26,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<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="Sentry" Version="2.1.6" />
<PackageReference Include="SharpCompress" Version="0.26.0" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<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" />
</ItemGroup>
<!-- 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.Core" Version="2.2.6" />
<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="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />