1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 14:17:26 +08:00

Merge branch 'master' into fix-17143

This commit is contained in:
Dan Balasescu 2022-10-25 13:44:58 +09:00 committed by GitHub
commit 6177df2a24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
191 changed files with 3356 additions and 1041 deletions

View File

@ -15,6 +15,8 @@ M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Gen
M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks. M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks.
P:System.Threading.Tasks.Task`1.Result;Don't use Task.Result. Use Task.GetResultSafely() to ensure we avoid deadlocks. P:System.Threading.Tasks.Task`1.Result;Don't use Task.Result. Use Task.GetResultSafely() to ensure we avoid deadlocks.
M:System.Threading.ManualResetEventSlim.Wait();Specify a timeout to avoid waiting forever. M:System.Threading.ManualResetEventSlim.Wait();Specify a timeout to avoid waiting forever.
M:System.Char.ToLower(System.Char);char.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use char.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture.
M:System.Char.ToUpper(System.Char);char.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use char.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture.
M:System.String.ToLower();string.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString. M:System.String.ToLower();string.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString.
M:System.String.ToUpper();string.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString. M:System.String.ToUpper();string.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString.
M:Humanizer.InflectorExtensions.Pascalize(System.String);Humanizer's .Pascalize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToPascalCase() instead. M:Humanizer.InflectorExtensions.Pascalize(System.String);Humanizer's .Pascalize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToPascalCase() instead.

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1005.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.1021.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.1005.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.1022.1" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests.Mods
{
public class TestSceneCatchModFlashlight : ModTestScene
{
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
[TestCase(1f)]
[TestCase(0.5f)]
[TestCase(1.25f)]
[TestCase(1.5f)]
public void TestSizeMultiplier(float sizeMultiplier) => CreateModTest(new ModTestData { Mod = new CatchModFlashlight { SizeMultiplier = { Value = sizeMultiplier } }, PassCondition = () => true });
[Test]
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new CatchModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
}
}

View File

@ -1,10 +1,10 @@
// 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.
#nullable disable
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
@ -12,6 +12,8 @@ using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -19,15 +21,28 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
public class TestSceneComboCounter : CatchSkinnableTestScene public class TestSceneComboCounter : CatchSkinnableTestScene
{ {
private ScoreProcessor scoreProcessor; private ScoreProcessor scoreProcessor = null!;
private Color4 judgedObjectColour = Color4.White; private Color4 judgedObjectColour = Color4.White;
private readonly Bindable<bool> showHud = new Bindable<bool>(true);
[BackgroundDependencyLoader]
private void load()
{
Dependencies.CacheAs<Player>(new TestPlayer
{
ShowingOverlayComponents = { BindTarget = showHud },
});
}
[SetUp] [SetUp]
public void SetUp() => Schedule(() => public void SetUp() => Schedule(() =>
{ {
scoreProcessor = new ScoreProcessor(new CatchRuleset()); scoreProcessor = new ScoreProcessor(new CatchRuleset());
showHud.Value = true;
SetContents(_ => new CatchComboDisplay SetContents(_ => new CatchComboDisplay
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
@ -51,9 +66,15 @@ namespace osu.Game.Rulesets.Catch.Tests
1f 1f
); );
}); });
AddStep("set hud to never show", () => showHud.Value = false);
AddRepeatStep("perform hit", () => performJudgement(HitResult.Great), 5);
AddStep("set hud to show", () => showHud.Value = true);
AddRepeatStep("perform hit", () => performJudgement(HitResult.Great), 5);
} }
private void performJudgement(HitResult type, Judgement judgement = null) private void performJudgement(HitResult type, Judgement? judgement = null)
{ {
var judgedObject = new DrawableFruit(new Fruit()) { AccentColour = { Value = judgedObjectColour } }; var judgedObject = new DrawableFruit(new Fruit()) { AccentColour = { Value = judgedObjectColour } };

View File

@ -36,5 +36,7 @@ namespace osu.Game.Rulesets.Catch.Edit
return base.CreateHitObjectBlueprintFor(hitObject); return base.CreateHitObjectBlueprintFor(hitObject);
} }
protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
} }
} }

View File

@ -0,0 +1,49 @@
// 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 osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit
{
public class CatchEditorPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
{
protected override Container<Drawable> Content => content;
private readonly Container content;
public CatchEditorPlayfieldAdjustmentContainer()
{
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
Size = new Vector2(0.8f, 0.9f);
InternalChild = new ScalingContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Child = content = new Container { RelativeSizeAxes = Axes.Both },
};
}
private class ScalingContainer : Container
{
public ScalingContainer()
{
RelativeSizeAxes = Axes.Y;
Width = CatchPlayfield.WIDTH;
}
protected override void Update()
{
base.Update();
Scale = new Vector2(Math.Min(Parent.ChildSize.X / CatchPlayfield.WIDTH, Parent.ChildSize.Y / CatchPlayfield.HEIGHT));
Height = 1 / Scale.Y;
}
}
}
}

View File

@ -12,8 +12,10 @@ using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
@ -37,6 +39,12 @@ namespace osu.Game.Rulesets.Catch.Edit
private InputManager inputManager; private InputManager inputManager;
private readonly BindableDouble timeRangeMultiplier = new BindableDouble(1)
{
MinValue = 1,
MaxValue = 10,
};
public CatchHitObjectComposer(CatchRuleset ruleset) public CatchHitObjectComposer(CatchRuleset ruleset)
: base(ruleset) : base(ruleset)
{ {
@ -51,7 +59,10 @@ namespace osu.Game.Rulesets.Catch.Edit
LayerBelowRuleset.Add(new PlayfieldBorder LayerBelowRuleset.Add(new PlayfieldBorder
{ {
RelativeSizeAxes = Axes.Both, Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.X,
Height = CatchPlayfield.HEIGHT,
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners } PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
}); });
@ -77,8 +88,30 @@ namespace osu.Game.Rulesets.Catch.Edit
updateDistanceSnapGrid(); updateDistanceSnapGrid();
} }
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
switch (e.Action)
{
// Note that right now these are hard to use as the default key bindings conflict with existing editor key bindings.
// In the future we will want to expose this via UI and potentially change the key bindings to be editor-specific.
// May be worth considering standardising "zoom" behaviour with what the timeline uses (ie. alt-wheel) but that may cause new conflicts.
case GlobalAction.IncreaseScrollSpeed:
this.TransformBindableTo(timeRangeMultiplier, timeRangeMultiplier.Value - 1, 200, Easing.OutQuint);
break;
case GlobalAction.DecreaseScrollSpeed:
this.TransformBindableTo(timeRangeMultiplier, timeRangeMultiplier.Value + 1, 200, Easing.OutQuint);
break;
}
return base.OnPressed(e);
}
protected override DrawableRuleset<CatchHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => protected override DrawableRuleset<CatchHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) =>
new DrawableCatchEditorRuleset(ruleset, beatmap, mods); new DrawableCatchEditorRuleset(ruleset, beatmap, mods)
{
TimeRangeMultiplier = { BindTarget = timeRangeMultiplier, }
};
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[] protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
{ {

View File

@ -4,6 +4,7 @@
#nullable disable #nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Bindables;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -13,11 +14,24 @@ namespace osu.Game.Rulesets.Catch.Edit
{ {
public class DrawableCatchEditorRuleset : DrawableCatchRuleset public class DrawableCatchEditorRuleset : DrawableCatchRuleset
{ {
public readonly BindableDouble TimeRangeMultiplier = new BindableDouble(1);
public DrawableCatchEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) public DrawableCatchEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
: base(ruleset, beatmap, mods) : base(ruleset, beatmap, mods)
{ {
} }
protected override void Update()
{
base.Update();
double gamePlayTimeRange = GetTimeRange(Beatmap.Difficulty.ApproachRate);
float playfieldStretch = Playfield.DrawHeight / CatchPlayfield.HEIGHT;
TimeRange.Value = gamePlayTimeRange * TimeRangeMultiplier.Value * playfieldStretch;
}
protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.Difficulty); protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.Difficulty);
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchEditorPlayfieldAdjustmentContainer();
} }
} }

View File

@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
this.playfield = playfield; this.playfield = playfield;
FlashlightSize = new Vector2(0, GetSizeFor(0)); FlashlightSize = new Vector2(0, GetSize());
FlashlightSmoothness = 1.4f; FlashlightSmoothness = 1.4f;
} }
@ -66,9 +66,9 @@ namespace osu.Game.Rulesets.Catch.Mods
FlashlightPosition = playfield.CatcherArea.ToSpaceOfOtherDrawable(playfield.Catcher.DrawPosition, this); FlashlightPosition = playfield.CatcherArea.ToSpaceOfOtherDrawable(playfield.Catcher.DrawPosition, this);
} }
protected override void OnComboChange(ValueChangedEvent<int> e) protected override void UpdateFlashlightSize(float size)
{ {
this.TransformTo(nameof(FlashlightSize), new Vector2(0, GetSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); this.TransformTo(nameof(FlashlightSize), new Vector2(0, size), FLASHLIGHT_FADE_DURATION);
} }
protected override string FragmentShader => "CircularFlashlight"; protected override string FragmentShader => "CircularFlashlight";

View File

@ -19,17 +19,20 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
public override LocalisableString Description => @"Use the mouse to control the catcher."; public override LocalisableString Description => @"Use the mouse to control the catcher.";
private DrawableRuleset<CatchHitObject> drawableRuleset = null!; private DrawableCatchRuleset drawableRuleset = null!;
public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<CatchHitObject> drawableRuleset)
{ {
this.drawableRuleset = drawableRuleset; this.drawableRuleset = (DrawableCatchRuleset)drawableRuleset;
} }
public void ApplyToPlayer(Player player) public void ApplyToPlayer(Player player)
{ {
if (!drawableRuleset.HasReplayLoaded.Value) if (!drawableRuleset.HasReplayLoaded.Value)
drawableRuleset.Cursor.Add(new MouseInputHelper((CatchPlayfield)drawableRuleset.Playfield)); {
var catchPlayfield = (CatchPlayfield)drawableRuleset.Playfield;
catchPlayfield.CatcherArea.Add(new MouseInputHelper(catchPlayfield.CatcherArea));
}
} }
private class MouseInputHelper : Drawable, IKeyBindingHandler<CatchAction>, IRequireHighFrequencyMousePosition private class MouseInputHelper : Drawable, IKeyBindingHandler<CatchAction>, IRequireHighFrequencyMousePosition
@ -38,9 +41,10 @@ namespace osu.Game.Rulesets.Catch.Mods
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
public MouseInputHelper(CatchPlayfield playfield) public MouseInputHelper(CatcherArea catcherArea)
{ {
catcherArea = playfield.CatcherArea; this.catcherArea = catcherArea;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }

View File

@ -13,6 +13,10 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
{ {
public class CatchLegacySkinTransformer : LegacySkinTransformer public class CatchLegacySkinTransformer : LegacySkinTransformer
{ {
public override bool IsProvidingLegacyResources => base.IsProvidingLegacyResources || hasPear;
private bool hasPear => GetTexture("fruit-pear") != null;
/// <summary> /// <summary>
/// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default. /// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default.
/// </summary> /// </summary>
@ -49,7 +53,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
switch (catchSkinComponent.Component) switch (catchSkinComponent.Component)
{ {
case CatchSkinComponents.Fruit: case CatchSkinComponents.Fruit:
if (GetTexture("fruit-pear") != null) if (hasPear)
return new LegacyFruitPiece(); return new LegacyFruitPiece();
return null; return null;

View File

@ -1,8 +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.
#nullable disable
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;

View File

@ -1,12 +1,13 @@
// 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.
#nullable disable using osu.Framework.Allocation;
using osu.Framework.Bindables;
using JetBrains.Annotations; using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK.Graphics; using osuTK.Graphics;
@ -19,14 +20,29 @@ namespace osu.Game.Rulesets.Catch.UI
{ {
private int currentCombo; private int currentCombo;
[CanBeNull] public ICatchComboCounter? ComboCounter => Drawable as ICatchComboCounter;
public ICatchComboCounter ComboCounter => Drawable as ICatchComboCounter;
private readonly IBindable<bool> showCombo = new BindableBool(true);
public CatchComboDisplay() public CatchComboDisplay()
: base(new CatchSkinComponent(CatchSkinComponents.CatchComboCounter), _ => Empty()) : base(new CatchSkinComponent(CatchSkinComponents.CatchComboCounter), _ => Empty())
{ {
} }
[Resolved(canBeNull: true)]
private Player? player { get; set; }
protected override void LoadComplete()
{
base.LoadComplete();
if (player != null)
{
showCombo.BindTo(player.ShowingOverlayComponents);
showCombo.BindValueChanged(s => this.FadeTo(s.NewValue ? 1 : 0, HUDOverlay.FADE_DURATION, HUDOverlay.FADE_EASING), true);
}
}
protected override void SkinChanged(ISkinSource skin) protected override void SkinChanged(ISkinSource skin)
{ {
base.SkinChanged(skin); base.SkinChanged(skin);

View File

@ -23,6 +23,12 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary> /// </summary>
public const float WIDTH = 512; public const float WIDTH = 512;
/// <summary>
/// The height of the playfield.
/// This doesn't include the catcher area.
/// </summary>
public const float HEIGHT = 384;
/// <summary> /// <summary>
/// The center position of the playfield. /// The center position of the playfield.
/// </summary> /// </summary>

View File

@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.UI
: base(ruleset, beatmap, mods) : base(ruleset, beatmap, mods)
{ {
Direction.Value = ScrollingDirection.Down; Direction.Value = ScrollingDirection.Down;
TimeRange.Value = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450); TimeRange.Value = GetTimeRange(beatmap.Difficulty.ApproachRate);
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -42,6 +42,8 @@ namespace osu.Game.Rulesets.Catch.UI
KeyBindingInputManager.Add(new CatchTouchInputMapper()); KeyBindingInputManager.Add(new CatchTouchInputMapper());
} }
protected double GetTimeRange(float approachRate) => IBeatmapDifficultyInfo.DifficultyRange(approachRate, 1800, 1200, 450);
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay); protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay);
protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield); protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield);

View File

@ -3,9 +3,11 @@
#nullable disable #nullable disable
using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Input.Bindings;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests
@ -37,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{ {
} }
protected override void ReloadMappings() protected override void ReloadMappings(IQueryable<RealmKeyBinding> realmKeyBindings)
{ {
KeyBindings = DefaultKeyBindings; KeyBindings = DefaultKeyBindings;
} }

View File

@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Mods
{
public class TestSceneManiaModFlashlight : ModTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
[TestCase(1f)]
[TestCase(0.5f)]
[TestCase(1.5f)]
[TestCase(3f)]
public void TestSizeMultiplier(float sizeMultiplier) => CreateModTest(new ModTestData { Mod = new ManiaModFlashlight { SizeMultiplier = { Value = sizeMultiplier } }, PassCondition = () => true });
[Test]
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new ManiaModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
}
}

View File

@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok); countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
scoreAccuracy = customAccuracy; scoreAccuracy = calculateCustomAccuracy();
// Arbitrary initial value for scaling pp in order to standardize distributions across game modes. // Arbitrary initial value for scaling pp in order to standardize distributions across game modes.
// The specific number has no intrinsic meaning and can be adjusted as needed. // The specific number has no intrinsic meaning and can be adjusted as needed.
@ -73,6 +73,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty
/// <summary> /// <summary>
/// Accuracy used to weight judgements independently from the score's actual accuracy. /// Accuracy used to weight judgements independently from the score's actual accuracy.
/// </summary> /// </summary>
private double customAccuracy => (countPerfect * 320 + countGreat * 300 + countGood * 200 + countOk * 100 + countMeh * 50) / (totalHits * 320); private double calculateCustomAccuracy()
{
if (totalHits == 0)
return 0;
return (countPerfect * 320 + countGreat * 300 + countGood * 200 + countOk * 100 + countMeh * 50) / (totalHits * 320);
}
} }
} }

View File

@ -33,5 +33,7 @@ namespace osu.Game.Rulesets.Mania.Edit
} }
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new ManiaSelectionHandler(); protected override SelectionHandler<HitObject> CreateSelectionHandler() => new ManiaSelectionHandler();
protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
} }
} }

View File

@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public ManiaFlashlight(ManiaModFlashlight modFlashlight) public ManiaFlashlight(ManiaModFlashlight modFlashlight)
: base(modFlashlight) : base(modFlashlight)
{ {
FlashlightSize = new Vector2(DrawWidth, GetSizeFor(0)); FlashlightSize = new Vector2(DrawWidth, GetSize());
AddLayout(flashlightProperties); AddLayout(flashlightProperties);
} }
@ -54,9 +54,9 @@ namespace osu.Game.Rulesets.Mania.Mods
} }
} }
protected override void OnComboChange(ValueChangedEvent<int> e) protected override void UpdateFlashlightSize(float size)
{ {
this.TransformTo(nameof(FlashlightSize), new Vector2(DrawWidth, GetSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); this.TransformTo(nameof(FlashlightSize), new Vector2(DrawWidth, size), FLASHLIGHT_FADE_DURATION);
} }
protected override string FragmentShader => "RectangularFlashlight"; protected override string FragmentShader => "RectangularFlashlight";

View File

@ -4,12 +4,14 @@
#nullable disable #nullable disable
using System; using System;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Rulesets.Mania.Skinning.Default; using osu.Game.Rulesets.Mania.Skinning.Default;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -38,6 +40,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
private Container<DrawableHoldNoteTail> tailContainer; private Container<DrawableHoldNoteTail> tailContainer;
private Container<DrawableHoldNoteTick> tickContainer; private Container<DrawableHoldNoteTick> tickContainer;
private PausableSkinnableSound slidingSample;
/// <summary> /// <summary>
/// Contains the size of the hold note covering the whole head/tail bounds. The size of this container changes as the hold note is being pressed. /// Contains the size of the hold note covering the whole head/tail bounds. The size of this container changes as the hold note is being pressed.
/// </summary> /// </summary>
@ -108,6 +112,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
}, },
tickContainer = new Container<DrawableHoldNoteTick> { RelativeSizeAxes = Axes.Both }, tickContainer = new Container<DrawableHoldNoteTick> { RelativeSizeAxes = Axes.Both },
tailContainer = new Container<DrawableHoldNoteTail> { RelativeSizeAxes = Axes.Both }, tailContainer = new Container<DrawableHoldNoteTail> { RelativeSizeAxes = Axes.Both },
slidingSample = new PausableSkinnableSound { Looping = true }
}); });
maskedContents.AddRange(new[] maskedContents.AddRange(new[]
@ -118,6 +123,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
}); });
} }
protected override void LoadComplete()
{
base.LoadComplete();
isHitting.BindValueChanged(updateSlidingSample, true);
}
protected override void OnApply() protected override void OnApply()
{ {
base.OnApply(); base.OnApply();
@ -322,5 +334,38 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
HoldStartTime = null; HoldStartTime = null;
isHitting.Value = false; isHitting.Value = false;
} }
protected override void LoadSamples()
{
// Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being.
if (HitObject.SampleControlPoint == null)
{
throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
}
slidingSample.Samples = HitObject.CreateSlidingSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
}
public override void StopAllSamples()
{
base.StopAllSamples();
slidingSample?.Stop();
}
private void updateSlidingSample(ValueChangedEvent<bool> tracking)
{
if (tracking.NewValue)
slidingSample?.Play();
else
slidingSample?.Stop();
}
protected override void OnFree()
{
slidingSample.Samples = null;
base.OnFree();
}
} }
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -89,13 +90,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
Color4 colour; Color4 colour;
const int total_colours = 7;
if (stage.IsSpecialColumn(column)) if (stage.IsSpecialColumn(column))
colour = new Color4(159, 101, 255, 255); colour = new Color4(159, 101, 255, 255);
else else
{ {
switch (column % 8) switch (column % total_colours)
{ {
default: case 0:
colour = new Color4(240, 216, 0, 255); colour = new Color4(240, 216, 0, 255);
break; break;
@ -112,20 +115,19 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
break; break;
case 4: case 4:
colour = new Color4(178, 0, 240, 255);
break;
case 5:
colour = new Color4(0, 96, 240, 255); colour = new Color4(0, 96, 240, 255);
break; break;
case 6: case 5:
colour = new Color4(0, 226, 240, 255); colour = new Color4(0, 226, 240, 255);
break; break;
case 7: case 6:
colour = new Color4(0, 240, 96, 255); colour = new Color4(0, 240, 96, 255);
break; break;
default:
throw new ArgumentOutOfRangeException();
} }
} }

View File

@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{ {
public class ManiaLegacySkinTransformer : LegacySkinTransformer public class ManiaLegacySkinTransformer : LegacySkinTransformer
{ {
public override bool IsProvidingLegacyResources => base.IsProvidingLegacyResources || hasKeyTexture.Value;
/// <summary> /// <summary>
/// Mapping of <see cref="HitResult"/> to their corresponding /// Mapping of <see cref="HitResult"/> to their corresponding
/// <see cref="LegacyManiaSkinConfigurationLookups"/> value. /// <see cref="LegacyManiaSkinConfigurationLookups"/> value.

View File

@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Overlays;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Edit;
@ -33,6 +34,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[Cached(typeof(IBeatSnapProvider))] [Cached(typeof(IBeatSnapProvider))]
private readonly EditorBeatmap editorBeatmap; private readonly EditorBeatmap editorBeatmap;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
[Cached] [Cached]
private readonly EditorClock editorClock; private readonly EditorClock editorClock;

View File

@ -148,6 +148,37 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
}); });
} }
[Test]
public void TestFloatEdgeCaseConversion()
{
Slider slider = null;
AddStep("select first slider", () =>
{
slider = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider);
EditorClock.Seek(slider.StartTime);
EditorBeatmap.SelectedHitObjects.Add(slider);
});
AddStep("change to these specific circumstances", () =>
{
EditorBeatmap.Difficulty.SliderMultiplier = 1;
var timingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(slider.StartTime);
timingPoint.BeatLength = 352.941176470588;
slider.Path.ControlPoints[^1].Position = new Vector2(-110, 16);
slider.Path.ExpectedDistance.Value = 100;
});
convertToStream();
AddAssert("stream created", () => streamCreatedFor(slider,
(time: 0, pathPosition: 0),
(time: 0.25, pathPosition: 0.25),
(time: 0.5, pathPosition: 0.5),
(time: 0.75, pathPosition: 0.75),
(time: 1, pathPosition: 1)));
}
private bool streamCreatedFor(Slider slider, params (double time, double pathPosition)[] expectedCircles) private bool streamCreatedFor(Slider slider, params (double time, double pathPosition)[] expectedCircles)
{ {
if (EditorBeatmap.HitObjects.Contains(slider)) if (EditorBeatmap.HitObjects.Contains(slider))

View File

@ -0,0 +1,25 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModFlashlight : OsuModTestScene
{
[TestCase(600)]
[TestCase(120)]
[TestCase(1200)]
public void TestFollowDelay(double followDelay) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { FollowDelay = { Value = followDelay } }, PassCondition = () => true });
[TestCase(1f)]
[TestCase(0.5f)]
[TestCase(1.5f)]
[TestCase(2f)]
public void TestSizeMultiplier(float sizeMultiplier) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { SizeMultiplier = { Value = sizeMultiplier } }, PassCondition = () => true });
[Test]
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
}
}

View File

@ -4,6 +4,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -12,6 +13,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods namespace osu.Game.Rulesets.Osu.Tests.Mods
@ -145,6 +147,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
private bool isBreak() => Player.IsBreakTime.Value; private bool isBreak() => Player.IsBreakTime.Value;
private bool cursorAlphaAlmostEquals(float alpha) => Precision.AlmostEquals(Player.DrawableRuleset.Cursor.Alpha, alpha, 0.1f); private OsuPlayfield playfield => (OsuPlayfield)Player.DrawableRuleset.Playfield;
private bool cursorAlphaAlmostEquals(float alpha) =>
Precision.AlmostEquals(playfield.Cursor.AsNonNull().Alpha, alpha, 0.1f) &&
Precision.AlmostEquals(playfield.Smoke.Alpha, alpha, 0.1f);
} }
} }

View File

@ -1,21 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneOsuFlashlight : TestSceneOsuPlayer
{
protected override TestPlayer CreatePlayer(Ruleset ruleset)
{
SelectedMods.Value = new Mod[] { new OsuModAutoplay(), new OsuModFlashlight(), };
return base.CreatePlayer(ruleset);
}
}
}

View File

@ -14,6 +14,7 @@ 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.Skinning.Legacy;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osuTK; using osuTK;
@ -68,10 +69,8 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("create slider", () => AddStep("create slider", () =>
{ {
var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo()); var skin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo());
tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1"; var provider = Ruleset.Value.CreateInstance().CreateSkinTransformer(skin, Beatmap.Value.Beatmap);
var provider = Ruleset.Value.CreateInstance().CreateSkinTransformer(tintingSkin, Beatmap.Value.Beatmap);
Child = new SkinProvidingContainer(provider) Child = new SkinProvidingContainer(provider)
{ {
@ -92,10 +91,10 @@ namespace osu.Game.Rulesets.Osu.Tests
}); });
AddStep("set accent white", () => dho.AccentColour.Value = Color4.White); AddStep("set accent white", () => dho.AccentColour.Value = Color4.White);
AddAssert("ball is white", () => dho.ChildrenOfType<DrawableSliderBall>().Single().AccentColour == Color4.White); AddAssert("ball is white", () => dho.ChildrenOfType<LegacySliderBall>().Single().BallColour == Color4.White);
AddStep("set accent red", () => dho.AccentColour.Value = Color4.Red); AddStep("set accent red", () => dho.AccentColour.Value = Color4.Red);
AddAssert("ball is red", () => dho.ChildrenOfType<DrawableSliderBall>().Single().AccentColour == Color4.Red); AddAssert("ball is red", () => dho.ChildrenOfType<LegacySliderBall>().Single().BallColour == Color4.Red);
} }
private Slider prepareObject(Slider slider) private Slider prepareObject(Slider slider)

View File

@ -342,7 +342,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
double positionWithRepeats = (time - HitObject.StartTime) / HitObject.Duration * HitObject.SpanCount(); double positionWithRepeats = (time - HitObject.StartTime) / HitObject.Duration * HitObject.SpanCount();
double pathPosition = positionWithRepeats - (int)positionWithRepeats; double pathPosition = positionWithRepeats - (int)positionWithRepeats;
// every second span is in the reverse direction - need to reverse the path position. // every second span is in the reverse direction - need to reverse the path position.
if (Precision.AlmostBigger(positionWithRepeats % 2, 1)) if (positionWithRepeats % 2 >= 1)
pathPosition = 1 - pathPosition; pathPosition = 1 - pathPosition;
Vector2 position = HitObject.Position + HitObject.Path.PositionAt(pathPosition); Vector2 position = HitObject.Position + HitObject.Path.PositionAt(pathPosition);

View File

@ -60,6 +60,9 @@ namespace osu.Game.Rulesets.Osu.Edit
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
// Give a bit of breathing room around the playfield content.
PlayfieldContentContainer.Padding = new MarginPadding(10);
LayerBelowRuleset.AddRange(new Drawable[] LayerBelowRuleset.AddRange(new Drawable[]
{ {
distanceSnapGridContainer = new Container distanceSnapGridContainer = new Container

View File

@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
followDelay = modFlashlight.FollowDelay.Value; followDelay = modFlashlight.FollowDelay.Value;
FlashlightSize = new Vector2(0, GetSizeFor(0)); FlashlightSize = new Vector2(0, GetSize());
FlashlightSmoothness = 1.4f; FlashlightSmoothness = 1.4f;
} }
@ -83,9 +83,9 @@ namespace osu.Game.Rulesets.Osu.Mods
return base.OnMouseMove(e); return base.OnMouseMove(e);
} }
protected override void OnComboChange(ValueChangedEvent<int> e) protected override void UpdateFlashlightSize(float size)
{ {
this.TransformTo(nameof(FlashlightSize), new Vector2(0, GetSizeFor(e.NewValue)), FLASHLIGHT_FADE_DURATION); this.TransformTo(nameof(FlashlightSize), new Vector2(0, size), FLASHLIGHT_FADE_DURATION);
} }
protected override string FragmentShader => "CircularFlashlight"; protected override string FragmentShader => "CircularFlashlight";

View File

@ -3,6 +3,7 @@
using System; using System;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Timing; using osu.Framework.Timing;
@ -46,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public void Update(Playfield playfield) public void Update(Playfield playfield)
{ {
var cursorPos = playfield.Cursor.ActiveCursor.DrawPosition; var cursorPos = playfield.Cursor.AsNonNull().ActiveCursor.DrawPosition;
foreach (var drawable in playfield.HitObjectContainer.AliveObjects) foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
{ {

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Localisation; using osu.Framework.Localisation;
@ -9,6 +10,7 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Utils; using osu.Game.Utils;
@ -33,9 +35,15 @@ namespace osu.Game.Rulesets.Osu.Mods
public void Update(Playfield playfield) public void Update(Playfield playfield)
{ {
bool shouldAlwaysShowCursor = IsBreakTime.Value || spinnerPeriods.IsInAny(playfield.Clock.CurrentTime); var osuPlayfield = (OsuPlayfield)playfield;
Debug.Assert(osuPlayfield.Cursor != null);
bool shouldAlwaysShowCursor = IsBreakTime.Value || spinnerPeriods.IsInAny(osuPlayfield.Clock.CurrentTime);
float targetAlpha = shouldAlwaysShowCursor ? 1 : ComboBasedAlpha; float targetAlpha = shouldAlwaysShowCursor ? 1 : ComboBasedAlpha;
playfield.Cursor.Alpha = (float)Interpolation.Lerp(playfield.Cursor.Alpha, targetAlpha, Math.Clamp(playfield.Time.Elapsed / TRANSITION_DURATION, 0, 1)); float currentAlpha = (float)Interpolation.Lerp(osuPlayfield.Cursor.Alpha, targetAlpha, Math.Clamp(osuPlayfield.Time.Elapsed / TRANSITION_DURATION, 0, 1));
osuPlayfield.Cursor.Alpha = currentAlpha;
osuPlayfield.Smoke.Alpha = currentAlpha;
} }
} }
} }

View File

@ -20,7 +20,9 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
{ {
public override LocalisableString Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things."; public override LocalisableString Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
public override Type[] IncompatibleMods =>
base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
/// <summary> /// <summary>
/// How early before a hitobject's start time to trigger a hit. /// How early before a hitobject's start time to trigger a hit.
@ -51,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Mods
return; return;
} }
osuInputManager.AllowUserPresses = false; osuInputManager.AllowGameplayInputs = false;
} }
public void Update(Playfield playfield) public void Update(Playfield playfield)

View File

@ -3,6 +3,7 @@
using System; using System;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Framework.Utils; using osu.Framework.Utils;
@ -45,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public void Update(Playfield playfield) public void Update(Playfield playfield)
{ {
var cursorPos = playfield.Cursor.ActiveCursor.DrawPosition; var cursorPos = playfield.Cursor.AsNonNull().ActiveCursor.DrawPosition;
foreach (var drawable in playfield.HitObjectContainer.AliveObjects) foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
{ {

View File

@ -14,12 +14,10 @@ using osu.Game.Audio;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
@ -106,7 +104,6 @@ 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);
@ -257,22 +254,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
SliderBody?.RecyclePath(); SliderBody?.RecyclePath();
} }
protected override void ApplySkin(ISkinSource skin, bool allowFallback)
{
base.ApplySkin(skin, allowFallback);
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;
}
protected override void CheckForResult(bool userTriggered, double timeOffset) protected override void CheckForResult(bool userTriggered, double timeOffset)
{ {
if (userTriggered || Time.Current < HitObject.EndTime) if (userTriggered || Time.Current < HitObject.EndTime)
@ -331,7 +312,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
base.UpdateHitStateTransforms(state); base.UpdateHitStateTransforms(state);
const float fade_out_time = 450; const float fade_out_time = 240;
switch (state) switch (state)
{ {
@ -341,7 +322,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
break; break;
} }
this.FadeOut(fade_out_time, Easing.OutQuint).Expire(); this.FadeOut(fade_out_time).Expire();
} }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => SliderBody?.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => SliderBody?.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos);

View File

@ -11,28 +11,20 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public class DrawableSliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour public class DrawableSliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition
{ {
public const float FOLLOW_AREA = 2.4f; public const float FOLLOW_AREA = 2.4f;
public Func<OsuAction?> GetInitialHitAction; public Func<OsuAction?> GetInitialHitAction;
public Color4 AccentColour
{
get => ball.Colour;
set => ball.Colour = value;
}
private Drawable followCircleReceptor; private Drawable followCircleReceptor;
private DrawableSlider drawableSlider; private DrawableSlider drawableSlider;
private Drawable ball; private Drawable ball;
@ -186,17 +178,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private Vector2? lastPosition; private Vector2? lastPosition;
private bool rewinding;
public void UpdateProgress(double completionProgress) public void UpdateProgress(double completionProgress)
{ {
Position = drawableSlider.HitObject.CurvePositionAt(completionProgress); Position = drawableSlider.HitObject.CurvePositionAt(completionProgress);
var diff = lastPosition.HasValue ? lastPosition.Value - Position : Position - drawableSlider.HitObject.CurvePositionAt(completionProgress + 0.01f); var diff = lastPosition.HasValue ? lastPosition.Value - Position : Position - drawableSlider.HitObject.CurvePositionAt(completionProgress + 0.01f);
if (Clock.ElapsedFrameTime != 0)
rewinding = Clock.ElapsedFrameTime < 0;
// Ensure the value is substantially high enough to allow for Atan2 to get a valid angle. // Ensure the value is substantially high enough to allow for Atan2 to get a valid angle.
if (diff.LengthFast < 0.01f) if (diff.LengthFast < 0.01f)
return; return;
ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI); ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI) + (rewinding ? 180 : 0);
lastPosition = Position; lastPosition = Position;
} }
} }

View File

@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// </summary> /// </summary>
public readonly IBindable<double> SpinsPerMinute = new BindableDouble(); public readonly IBindable<double> SpinsPerMinute = new BindableDouble();
private const double fade_out_duration = 160; private const double fade_out_duration = 240;
public DrawableSpinner() public DrawableSpinner()
: this(null) : this(null)

View File

@ -34,21 +34,6 @@ namespace osu.Game.Rulesets.Osu.Objects
public override IList<HitSampleInfo> AuxiliarySamples => CreateSlidingSamples().Concat(TailSamples).ToArray(); public override IList<HitSampleInfo> AuxiliarySamples => CreateSlidingSamples().Concat(TailSamples).ToArray();
public IList<HitSampleInfo> CreateSlidingSamples()
{
var slidingSamples = new List<HitSampleInfo>();
var normalSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL);
if (normalSample != null)
slidingSamples.Add(normalSample.With("sliderslide"));
var whistleSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_WHISTLE);
if (whistleSample != null)
slidingSamples.Add(whistleSample.With("sliderwhistle"));
return slidingSamples;
}
private readonly Cached<Vector2> endPositionCache = new Cached<Vector2>(); private readonly Cached<Vector2> endPositionCache = new Cached<Vector2>();
public override Vector2 EndPosition => endPositionCache.IsValid ? endPositionCache.Value : endPositionCache.Value = Position + this.CurvePositionAt(1); public override Vector2 EndPosition => endPositionCache.IsValid ? endPositionCache.Value : endPositionCache.Value = Position + this.CurvePositionAt(1);

View File

@ -5,10 +5,12 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges.Events; using osu.Framework.Input.StateChanges.Events;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu namespace osu.Game.Rulesets.Osu
@ -17,9 +19,16 @@ namespace osu.Game.Rulesets.Osu
{ {
public IEnumerable<OsuAction> PressedActions => KeyBindingContainer.PressedActions; public IEnumerable<OsuAction> PressedActions => KeyBindingContainer.PressedActions;
public bool AllowUserPresses /// <summary>
/// Whether gameplay input buttons should be allowed.
/// Defaults to <c>true</c>, generally used for mods like Relax which turn off main inputs.
/// </summary>
/// <remarks>
/// Of note, auxiliary inputs like the "smoke" key are left usable.
/// </remarks>
public bool AllowGameplayInputs
{ {
set => ((OsuKeyBindingContainer)KeyBindingContainer).AllowUserPresses = value; set => ((OsuKeyBindingContainer)KeyBindingContainer).AllowGameplayInputs = value;
} }
/// <summary> /// <summary>
@ -58,18 +67,36 @@ namespace osu.Game.Rulesets.Osu
private class OsuKeyBindingContainer : RulesetKeyBindingContainer private class OsuKeyBindingContainer : RulesetKeyBindingContainer
{ {
public bool AllowUserPresses = true; private bool allowGameplayInputs = true;
/// <summary>
/// Whether gameplay input buttons should be allowed.
/// Defaults to <c>true</c>, generally used for mods like Relax which turn off main inputs.
/// </summary>
/// <remarks>
/// Of note, auxiliary inputs like the "smoke" key are left usable.
/// </remarks>
public bool AllowGameplayInputs
{
get => allowGameplayInputs;
set
{
allowGameplayInputs = value;
ReloadMappings();
}
}
public OsuKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) public OsuKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
: base(ruleset, variant, unique) : base(ruleset, variant, unique)
{ {
} }
protected override bool Handle(UIEvent e) protected override void ReloadMappings(IQueryable<RealmKeyBinding> realmKeyBindings)
{ {
if (!AllowUserPresses) return false; base.ReloadMappings(realmKeyBindings);
return base.Handle(e); if (!AllowGameplayInputs)
KeyBindings = KeyBindings.Where(b => b.GetAction<OsuAction>() == OsuAction.Smoke).ToList();
} }
} }
} }

View File

@ -75,6 +75,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{ {
default: default:
JudgementText JudgementText
.FadeInFromZero(300, Easing.OutQuint)
.ScaleTo(Vector2.One) .ScaleTo(Vector2.One)
.ScaleTo(new Vector2(1.2f), 1800, Easing.OutQuint); .ScaleTo(new Vector2(1.2f), 1800, Easing.OutQuint);
break; break;
@ -96,7 +97,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
ringExplosion?.PlayAnimation(); ringExplosion?.PlayAnimation();
} }
public Drawable? GetAboveHitObjectsProxiedContent() => null; public Drawable? GetAboveHitObjectsProxiedContent() => JudgementText.CreateProxy();
private class RingExplosion : CompositeDrawable private class RingExplosion : CompositeDrawable
{ {

View File

@ -108,18 +108,23 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{ {
base.LoadComplete(); base.LoadComplete();
accentColour.BindValueChanged(colour =>
{
outerFill.Colour = innerFill.Colour = colour.NewValue.Darken(4);
outerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.1f));
innerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue.Darken(0.5f), colour.NewValue.Darken(0.6f));
flash.Colour = colour.NewValue;
}, true);
indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true); indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true);
accentColour.BindValueChanged(colour =>
{
// A colour transform is applied.
// Without removing transforms first, when it is rewound it may apply an old colour.
outerGradient.ClearTransforms(targetMember: nameof(Colour));
outerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.1f));
outerFill.Colour = innerFill.Colour = colour.NewValue.Darken(4);
innerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue.Darken(0.5f), colour.NewValue.Darken(0.6f));
flash.Colour = colour.NewValue;
updateStateTransforms(drawableObject, drawableObject.State.Value);
}, true);
drawableObject.ApplyCustomUpdateState += updateStateTransforms; drawableObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableObject, drawableObject.State.Value);
} }
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
@ -173,11 +178,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
.FadeOut(flash_in_duration); .FadeOut(flash_in_duration);
} }
// The flash layer starts white to give the wanted brightness, but is almost immediately
// recoloured to the accent colour. This would more correctly be done with two layers (one for the initial flash)
// but works well enough with the colour fade.
flash.FadeTo(1, flash_in_duration, Easing.OutQuint); flash.FadeTo(1, flash_in_duration, Easing.OutQuint);
flash.FlashColour(accentColour.Value, fade_out_time, Easing.OutQuint);
this.FadeOut(fade_out_time, Easing.OutQuad); this.FadeOut(fade_out_time, Easing.OutQuad);
break; break;

View File

@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
InternalChildren = new[] InternalChildren = new[]
{ {
CircleSprite = new KiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) }) CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) })
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Child = OverlaySprite = new KiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d)) Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d))
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -134,10 +134,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
switch (state) switch (state)
{ {
case ArmedState.Hit: case ArmedState.Hit:
CircleSprite.FadeOut(legacy_fade_duration, Easing.Out); CircleSprite.FadeOut(legacy_fade_duration);
CircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); CircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
OverlaySprite.FadeOut(legacy_fade_duration, Easing.Out); OverlaySprite.FadeOut(legacy_fade_duration);
OverlaySprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); OverlaySprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
if (hasNumber) if (hasNumber)
@ -146,11 +146,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
if (legacyVersion >= 2.0m) if (legacyVersion >= 2.0m)
// legacy skins of version 2.0 and newer only apply very short fade out to the number piece. // legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out); hitCircleText.FadeOut(legacy_fade_duration / 4);
else else
{ {
// old skins scale and fade it normally along other pieces. // old skins scale and fade it normally along other pieces.
hitCircleText.FadeOut(legacy_fade_duration, Easing.Out); hitCircleText.FadeOut(legacy_fade_duration);
hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
} }
} }

View File

@ -107,8 +107,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt)) using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
this.FadeOut(); this.FadeOut();
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2)) using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn))
this.FadeInFromZero(spinner.TimeFadeIn / 2); this.FadeInFromZero(spinner.TimeFadeIn);
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt)) using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
{ {

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
@ -21,6 +22,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private DrawableHitObject? parentObject { get; set; } private DrawableHitObject? parentObject { get; set; }
public Color4 BallColour => animationContent.Colour;
private Sprite layerNd = null!; private Sprite layerNd = null!;
private Sprite layerSpec = null!; private Sprite layerSpec = null!;
@ -61,6 +64,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
}; };
} }
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
@ -69,6 +74,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
parentObject.ApplyCustomUpdateState += updateStateTransforms; parentObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(parentObject, parentObject.State.Value); updateStateTransforms(parentObject, parentObject.State.Value);
if (skin.GetConfig<SkinConfiguration.LegacySetting, bool>(SkinConfiguration.LegacySetting.AllowSliderBallTint)?.Value == true)
{
accentColour.BindTo(parentObject.AccentColour);
accentColour.BindValueChanged(a => animationContent.Colour = a.NewValue, true);
}
} }
} }

View File

@ -65,6 +65,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
spin = new Sprite spin = new Sprite
{ {
Alpha = 0,
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-spin"), Texture = source.GetTexture("spinner-spin"),
@ -82,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
}, },
bonusCounter = new LegacySpriteText(LegacyFont.Score) bonusCounter = new LegacySpriteText(LegacyFont.Score)
{ {
Alpha = 0f, Alpha = 0,
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Scale = new Vector2(SPRITE_SCALE), Scale = new Vector2(SPRITE_SCALE),
@ -179,6 +180,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out); spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out);
} }
using (BeginAbsoluteSequence(d.HitObject.StartTime - d.HitObject.TimeFadeIn / 2))
spin.FadeInFromZero(d.HitObject.TimeFadeIn / 2);
using (BeginAbsoluteSequence(d.HitObject.StartTime)) using (BeginAbsoluteSequence(d.HitObject.StartTime))
ApproachCircle?.ScaleTo(SPRITE_SCALE * 0.1f, d.HitObject.Duration); ApproachCircle?.ScaleTo(SPRITE_SCALE * 0.1f, d.HitObject.Duration);

View File

@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
public class OsuLegacySkinTransformer : LegacySkinTransformer public class OsuLegacySkinTransformer : LegacySkinTransformer
{ {
public override bool IsProvidingLegacyResources => base.IsProvidingLegacyResources || hasHitCircle.Value;
private readonly Lazy<bool> hasHitCircle; private readonly Lazy<bool> hasHitCircle;
/// <summary> /// <summary>

View File

@ -9,7 +9,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
SliderBorderSize, SliderBorderSize,
SliderPathRadius, SliderPathRadius,
AllowSliderBallTint,
CursorCentre, CursorCentre,
CursorExpand, CursorExpand,
CursorRotate, CursorRotate,

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -14,6 +15,7 @@ using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Utils;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -21,8 +23,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
public abstract class SmokeSegment : Drawable, ITexturedShaderDrawable public abstract class SmokeSegment : Drawable, ITexturedShaderDrawable
{ {
private const int max_point_count = 18_000;
// fade anim values // fade anim values
private const double initial_fade_out_duration = 4000; private const double initial_fade_out_duration = 4000;
@ -84,12 +84,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
totalDistance = pointInterval; totalDistance = pointInterval;
} }
private Vector2 nextPointDirection()
{
float angle = RNG.NextSingle(0, 2 * MathF.PI);
return new Vector2(MathF.Sin(angle), -MathF.Cos(angle));
}
public void AddPosition(Vector2 position, double time) public void AddPosition(Vector2 position, double time)
{ {
lastPosition ??= position; lastPosition ??= position;
@ -106,33 +100,27 @@ namespace osu.Game.Rulesets.Osu.Skinning
Vector2 pointPos = (pointInterval - (totalDistance - delta)) * increment + (Vector2)lastPosition; Vector2 pointPos = (pointInterval - (totalDistance - delta)) * increment + (Vector2)lastPosition;
increment *= pointInterval; increment *= pointInterval;
if (SmokePoints.Count > 0 && SmokePoints[^1].Time > time)
{
int index = ~SmokePoints.BinarySearch(new SmokePoint { Time = time }, new SmokePoint.UpperBoundComparer());
SmokePoints.RemoveRange(index, SmokePoints.Count - index);
}
totalDistance %= pointInterval; totalDistance %= pointInterval;
for (int i = 0; i < count; i++) if (SmokePoints.Count == 0 || SmokePoints[^1].Time <= time)
{ {
SmokePoints.Add(new SmokePoint for (int i = 0; i < count; i++)
{ {
Position = pointPos, SmokePoints.Add(new SmokePoint
Time = time, {
Direction = nextPointDirection(), Position = pointPos,
}); Time = time,
Angle = RNG.NextSingle(0, 2 * MathF.PI),
});
pointPos += increment; pointPos += increment;
}
} }
Invalidate(Invalidation.DrawNode); Invalidate(Invalidation.DrawNode);
} }
lastPosition = position; lastPosition = position;
if (SmokePoints.Count >= max_point_count)
FinishDrawing(time);
} }
public void FinishDrawing(double time) public void FinishDrawing(double time)
@ -156,7 +144,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
public Vector2 Position; public Vector2 Position;
public double Time; public double Time;
public Vector2 Direction; public float Angle;
public struct UpperBoundComparer : IComparer<SmokePoint> public struct UpperBoundComparer : IComparer<SmokePoint>
{ {
@ -170,6 +158,17 @@ namespace osu.Game.Rulesets.Osu.Skinning
return x.Time > target.Time ? 1 : -1; return x.Time > target.Time ? 1 : -1;
} }
} }
public struct LowerBoundComparer : IComparer<SmokePoint>
{
public int Compare(SmokePoint x, SmokePoint target)
{
// Similar logic as UpperBoundComparer, except returned index will always be
// the first element larger or equal
return x.Time < target.Time ? -1 : 1;
}
}
} }
protected class SmokeDrawNode : TexturedShaderDrawNode protected class SmokeDrawNode : TexturedShaderDrawNode
@ -185,17 +184,17 @@ namespace osu.Game.Rulesets.Osu.Skinning
private float radius; private float radius;
private Vector2 drawSize; private Vector2 drawSize;
private Texture? texture; private Texture? texture;
private int rotationSeed;
private int firstVisiblePointIndex;
// anim calculation vars (color, scale, direction) // anim calculation vars (color, scale, direction)
private double initialFadeOutDurationTrunc; private double initialFadeOutDurationTrunc;
private double firstVisiblePointTime; private double firstVisiblePointTimeAfterSmokeEnded;
private double initialFadeOutTime; private double initialFadeOutTime;
private double reFadeInTime; private double reFadeInTime;
private double finalFadeOutTime; private double finalFadeOutTime;
private Random rotationRNG = new Random();
public SmokeDrawNode(ITexturedShaderDrawable source) public SmokeDrawNode(ITexturedShaderDrawable source)
: base(source) : base(source)
{ {
@ -205,9 +204,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
base.ApplyState(); base.ApplyState();
points.Clear();
points.AddRange(Source.SmokePoints);
radius = Source.radius; radius = Source.radius;
drawSize = Source.DrawSize; drawSize = Source.DrawSize;
texture = Source.Texture; texture = Source.Texture;
@ -216,14 +212,21 @@ namespace osu.Game.Rulesets.Osu.Skinning
SmokeEndTime = Source.smokeEndTime; SmokeEndTime = Source.smokeEndTime;
CurrentTime = Source.Clock.CurrentTime; CurrentTime = Source.Clock.CurrentTime;
rotationRNG = new Random(Source.rotationSeed); rotationSeed = Source.rotationSeed;
initialFadeOutDurationTrunc = Math.Min(initial_fade_out_duration, SmokeEndTime - SmokeStartTime); initialFadeOutDurationTrunc = Math.Min(initial_fade_out_duration, SmokeEndTime - SmokeStartTime);
firstVisiblePointTime = SmokeEndTime - initialFadeOutDurationTrunc; firstVisiblePointTimeAfterSmokeEnded = SmokeEndTime - initialFadeOutDurationTrunc;
initialFadeOutTime = CurrentTime; initialFadeOutTime = Math.Min(CurrentTime, SmokeEndTime);
reFadeInTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / re_fade_in_speed); reFadeInTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTimeAfterSmokeEnded * (1 - 1 / re_fade_in_speed);
finalFadeOutTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / final_fade_out_speed); finalFadeOutTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTimeAfterSmokeEnded * (1 - 1 / final_fade_out_speed);
double firstVisiblePointTime = Math.Min(SmokeEndTime, CurrentTime) - initialFadeOutDurationTrunc;
firstVisiblePointIndex = ~Source.SmokePoints.BinarySearch(new SmokePoint { Time = firstVisiblePointTime }, new SmokePoint.LowerBoundComparer());
int futurePointIndex = ~Source.SmokePoints.BinarySearch(new SmokePoint { Time = CurrentTime }, new SmokePoint.UpperBoundComparer());
points.Clear();
points.AddRange(Source.SmokePoints.Skip(firstVisiblePointIndex).Take(futurePointIndex - firstVisiblePointIndex));
} }
public sealed override void Draw(IRenderer renderer) public sealed override void Draw(IRenderer renderer)
@ -233,7 +236,14 @@ namespace osu.Game.Rulesets.Osu.Skinning
if (points.Count == 0) if (points.Count == 0)
return; return;
quadBatch ??= renderer.CreateQuadBatch<TexturedVertex2D>(max_point_count / 10, 10); quadBatch ??= renderer.CreateQuadBatch<TexturedVertex2D>(200, 4);
if (points.Count > quadBatch.Size && quadBatch.Size != IRenderer.MAX_QUADS)
{
int batchSize = Math.Min(quadBatch.Size * 2, IRenderer.MAX_QUADS);
quadBatch = renderer.CreateQuadBatch<TexturedVertex2D>(batchSize, 4);
}
texture ??= renderer.WhitePixel; texture ??= renderer.WhitePixel;
RectangleF textureRect = texture.GetTextureRect(); RectangleF textureRect = texture.GetTextureRect();
@ -245,8 +255,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
shader.Bind(); shader.Bind();
texture.Bind(); texture.Bind();
foreach (var point in points) for (int i = 0; i < points.Count; i++)
drawPointQuad(point, textureRect); drawPointQuad(points[i], textureRect, i + firstVisiblePointIndex);
shader.Unbind(); shader.Unbind();
renderer.PopLocalMatrix(); renderer.PopLocalMatrix();
@ -260,30 +270,34 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
var color = Color4.White; var color = Color4.White;
double timeDoingInitialFadeOut = Math.Min(initialFadeOutTime, SmokeEndTime) - point.Time; double timeDoingFinalFadeOut = finalFadeOutTime - point.Time / final_fade_out_speed;
if (timeDoingInitialFadeOut > 0) if (timeDoingFinalFadeOut > 0 && point.Time >= firstVisiblePointTimeAfterSmokeEnded)
{ {
float fraction = Math.Clamp((float)(timeDoingInitialFadeOut / initial_fade_out_duration), 0, 1); float fraction = Math.Clamp((float)(timeDoingFinalFadeOut / final_fade_out_duration), 0, 1);
color.A = (1 - fraction) * initial_alpha; fraction = MathF.Pow(fraction, 5);
color.A = (1 - fraction) * re_fade_in_alpha;
} }
else
if (color.A > 0)
{ {
double timeDoingReFadeIn = reFadeInTime - point.Time / re_fade_in_speed; double timeDoingInitialFadeOut = initialFadeOutTime - point.Time;
double timeDoingFinalFadeOut = finalFadeOutTime - point.Time / final_fade_out_speed;
if (timeDoingFinalFadeOut > 0) if (timeDoingInitialFadeOut > 0)
{ {
float fraction = Math.Clamp((float)(timeDoingFinalFadeOut / final_fade_out_duration), 0, 1); float fraction = Math.Clamp((float)(timeDoingInitialFadeOut / initial_fade_out_duration), 0, 1);
fraction = MathF.Pow(fraction, 5); color.A = (1 - fraction) * initial_alpha;
color.A = (1 - fraction) * re_fade_in_alpha;
} }
else if (timeDoingReFadeIn > 0)
if (point.Time > firstVisiblePointTimeAfterSmokeEnded)
{ {
float fraction = Math.Clamp((float)(timeDoingReFadeIn / re_fade_in_duration), 0, 1); double timeDoingReFadeIn = reFadeInTime - point.Time / re_fade_in_speed;
fraction = 1 - MathF.Pow(1 - fraction, 5);
color.A = fraction * (re_fade_in_alpha - color.A) + color.A; if (timeDoingReFadeIn > 0)
{
float fraction = Math.Clamp((float)(timeDoingReFadeIn / re_fade_in_duration), 0, 1);
fraction = 1 - MathF.Pow(1 - fraction, 5);
color.A = fraction * (re_fade_in_alpha - color.A) + color.A;
}
} }
} }
@ -298,33 +312,33 @@ namespace osu.Game.Rulesets.Osu.Skinning
return fraction * (final_scale - initial_scale) + initial_scale; return fraction * (final_scale - initial_scale) + initial_scale;
} }
protected virtual Vector2 PointDirection(SmokePoint point) protected virtual Vector2 PointDirection(SmokePoint point, int index)
{ {
float initialAngle = MathF.Atan2(point.Direction.Y, point.Direction.X);
float finalAngle = initialAngle + nextRotation();
double timeDoingRotation = CurrentTime - point.Time; double timeDoingRotation = CurrentTime - point.Time;
float fraction = Math.Clamp((float)(timeDoingRotation / rotation_duration), 0, 1); float fraction = Math.Clamp((float)(timeDoingRotation / rotation_duration), 0, 1);
fraction = 1 - MathF.Pow(1 - fraction, 5); fraction = 1 - MathF.Pow(1 - fraction, 5);
float angle = fraction * (finalAngle - initialAngle) + initialAngle; float angle = fraction * getRotation(index) + point.Angle;
return new Vector2(MathF.Sin(angle), -MathF.Cos(angle)); return new Vector2(MathF.Sin(angle), -MathF.Cos(angle));
} }
private float nextRotation() => max_rotation * ((float)rotationRNG.NextDouble() * 2 - 1); private float getRotation(int index) => max_rotation * (StatelessRNG.NextSingle(rotationSeed, index) * 2 - 1);
private void drawPointQuad(SmokePoint point, RectangleF textureRect) private void drawPointQuad(SmokePoint point, RectangleF textureRect, int index)
{ {
Debug.Assert(quadBatch != null); Debug.Assert(quadBatch != null);
var colour = PointColour(point); var colour = PointColour(point);
float scale = PointScale(point); if (colour.A == 0)
var dir = PointDirection(point);
var ortho = dir.PerpendicularLeft;
if (colour.A == 0 || scale == 0)
return; return;
float scale = PointScale(point);
if (scale == 0)
return;
var dir = PointDirection(point, index);
var ortho = dir.PerpendicularLeft;
var localTopLeft = point.Position + (radius * scale * (-ortho - dir)); var localTopLeft = point.Position + (radius * scale * (-ortho - dir));
var localTopRight = point.Position + (radius * scale * (-ortho + dir)); var localTopRight = point.Position + (radius * scale * (-ortho + dir));
var localBotLeft = point.Position + (radius * scale * (ortho - dir)); var localBotLeft = point.Position + (radius * scale * (ortho - dir));

View File

@ -36,6 +36,7 @@ namespace osu.Game.Rulesets.Osu.UI
private readonly ProxyContainer spinnerProxies; private readonly ProxyContainer spinnerProxies;
private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer; private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer;
public SmokeContainer Smoke { get; }
public FollowPointRenderer FollowPoints { get; } public FollowPointRenderer FollowPoints { get; }
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
@ -54,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.UI
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both }, playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
new SmokeContainer { RelativeSizeAxes = Axes.Both }, Smoke = new SmokeContainer { RelativeSizeAxes = Axes.Both },
spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both }, spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both },
FollowPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both }, FollowPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both },
judgementLayer = new JudgementContainer<DrawableOsuJudgement> { RelativeSizeAxes = Axes.Both }, judgementLayer = new JudgementContainer<DrawableOsuJudgement> { RelativeSizeAxes = Axes.Both },

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Taiko.Mods;
namespace osu.Game.Rulesets.Taiko.Tests.Mods
{
public class TestSceneTaikoModFlashlight : TaikoModTestScene
{
[TestCase(1f)]
[TestCase(0.5f)]
[TestCase(1.25f)]
[TestCase(1.5f)]
public void TestSizeMultiplier(float sizeMultiplier) => CreateModTest(new ModTestData { Mod = new TaikoModFlashlight { SizeMultiplier = { Value = sizeMultiplier } }, PassCondition = () => true });
[Test]
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new TaikoModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
}
}

View File

@ -25,8 +25,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
TimeRange = { Value = 5000 }, TimeRange = { Value = 5000 },
}; };
[BackgroundDependencyLoader] [Test]
private void load() public void DrumrollTest()
{ {
AddStep("Drum roll", () => SetContents(_ => AddStep("Drum roll", () => SetContents(_ =>
{ {

View File

@ -0,0 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps;
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
[TestFixture]
public class TestSceneDrawableDrumRollKiai : TestSceneDrawableDrumRoll
{
[SetUp]
public void SetUp() => Schedule(() =>
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
Beatmap.Value = CreateWorkingBeatmap(new Beatmap
{
ControlPointInfo = controlPointInfo
});
// track needs to be playing for BeatSyncedContainer to work.
Beatmap.Value.Track.Start();
});
}
}

View File

@ -4,7 +4,6 @@
#nullable disable #nullable disable
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -16,8 +15,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[TestFixture] [TestFixture]
public class TestSceneDrawableHit : TaikoSkinnableTestScene public class TestSceneDrawableHit : TaikoSkinnableTestScene
{ {
[BackgroundDependencyLoader] [Test]
private void load() public void TestHits()
{ {
AddStep("Centre hit", () => SetContents(_ => new DrawableHit(createHitAtCurrentTime()) AddStep("Centre hit", () => SetContents(_ => new DrawableHit(createHitAtCurrentTime())
{ {

View File

@ -0,0 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
[TestFixture]
public class TestSceneDrawableHitKiai : TestSceneDrawableHit
{
[SetUp]
public void SetUp() => Schedule(() =>
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
Beatmap.Value = CreateWorkingBeatmap(new Beatmap
{
ControlPointInfo = controlPointInfo
});
// track needs to be playing for BeatSyncedContainer to work.
Beatmap.Value.Track.Start();
});
}
}

View File

@ -0,0 +1,87 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Taiko.Tests
{
public class TestSceneBarLineGeneration : OsuTestScene
{
[Test]
public void TestCloseBarLineGeneration()
{
const double start_time = 1000;
var beatmap = new Beatmap<TaikoHitObject>
{
HitObjects =
{
new Hit
{
Type = HitType.Centre,
StartTime = start_time
}
},
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
Ruleset = new TaikoRuleset().RulesetInfo
},
};
beatmap.ControlPointInfo.Add(start_time, new TimingControlPoint());
beatmap.ControlPointInfo.Add(start_time + 1, new TimingControlPoint());
var barlines = new BarLineGenerator<BarLine>(beatmap).BarLines;
AddAssert("first barline generated", () => barlines.Any(b => b.StartTime == start_time));
AddAssert("second barline generated", () => barlines.Any(b => b.StartTime == start_time + 1));
}
[Test]
public void TestOmitBarLineEffectPoint()
{
const double start_time = 1000;
const double beat_length = 500;
const int time_signature_numerator = 4;
var beatmap = new Beatmap<TaikoHitObject>
{
HitObjects =
{
new Hit
{
Type = HitType.Centre,
StartTime = start_time
}
},
BeatmapInfo =
{
Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
Ruleset = new TaikoRuleset().RulesetInfo
},
};
beatmap.ControlPointInfo.Add(start_time, new TimingControlPoint
{
BeatLength = beat_length,
TimeSignature = new TimeSignature(time_signature_numerator)
});
beatmap.ControlPointInfo.Add(start_time, new EffectControlPoint { OmitFirstBarLine = true });
var barlines = new BarLineGenerator<BarLine>(beatmap).BarLines;
AddAssert("first barline ommited", () => barlines.All(b => b.StartTime != start_time));
AddAssert("second barline generated", () => barlines.Any(b => b.StartTime == start_time + (beat_length * time_signature_numerator)));
}
}
}

View File

@ -27,6 +27,12 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
}; };
} }
protected override void LoadComplete()
{
base.LoadComplete();
BeginPlacement();
}
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)
{ {
switch (e.Button) switch (e.Button)

View File

@ -52,6 +52,12 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
private double originalStartTime; private double originalStartTime;
private Vector2 originalPosition; private Vector2 originalPosition;
protected override void LoadComplete()
{
base.LoadComplete();
BeginPlacement();
}
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)
{ {
if (e.Button != MouseButton.Left) if (e.Button != MouseButton.Left)

View File

@ -1,8 +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.
#nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;

View File

@ -47,21 +47,21 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
this.taikoPlayfield = taikoPlayfield; this.taikoPlayfield = taikoPlayfield;
FlashlightSize = getSizeFor(0); FlashlightSize = adjustSize(GetSize());
FlashlightSmoothness = 1.4f; FlashlightSmoothness = 1.4f;
AddLayout(flashlightProperties); AddLayout(flashlightProperties);
} }
private Vector2 getSizeFor(int combo) private Vector2 adjustSize(float size)
{ {
// Preserve flashlight size through the playfield's aspect adjustment. // Preserve flashlight size through the playfield's aspect adjustment.
return new Vector2(0, GetSizeFor(combo) * taikoPlayfield.DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT); return new Vector2(0, size * taikoPlayfield.DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT);
} }
protected override void OnComboChange(ValueChangedEvent<int> e) protected override void UpdateFlashlightSize(float size)
{ {
this.TransformTo(nameof(FlashlightSize), getSizeFor(e.NewValue), FLASHLIGHT_FADE_DURATION); this.TransformTo(nameof(FlashlightSize), adjustSize(size), FLASHLIGHT_FADE_DURATION);
} }
protected override string FragmentShader => "CircularFlashlight"; protected override string FragmentShader => "CircularFlashlight";
@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
FlashlightPosition = ToLocalSpace(taikoPlayfield.HitTarget.ScreenSpaceDrawQuad.Centre); FlashlightPosition = ToLocalSpace(taikoPlayfield.HitTarget.ScreenSpaceDrawQuad.Centre);
ClearTransforms(targetMember: nameof(FlashlightSize)); ClearTransforms(targetMember: nameof(FlashlightSize));
FlashlightSize = getSizeFor(Combo.Value); FlashlightSize = adjustSize(Combo.Value);
flashlightProperties.Validate(); flashlightProperties.Validate();
} }

View File

@ -3,6 +3,7 @@
#nullable disable #nullable disable
using osu.Framework.Allocation;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -13,6 +14,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
using osuTK.Graphics; using osuTK.Graphics;
@ -32,6 +34,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
private const double pre_beat_transition_time = 80; private const double pre_beat_transition_time = 80;
private const float flash_opacity = 0.3f;
private Color4 accentColour; private Color4 accentColour;
/// <summary> /// <summary>
@ -152,11 +156,22 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
}; };
} }
[Resolved]
private DrawableHitObject drawableHitObject { get; set; }
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{ {
if (!effectPoint.KiaiMode) if (!effectPoint.KiaiMode)
return; return;
if (drawableHitObject.State.Value == ArmedState.Idle)
{
FlashBox
.FadeTo(flash_opacity)
.Then()
.FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine);
}
if (beatIndex % timingPoint.TimeSignature.Numerator != 0) if (beatIndex % timingPoint.TimeSignature.Numerator != 0)
return; return;

View File

@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
} }
// backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer. // backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer.
AddInternal(backgroundLayer = getDrawableFor("circle")); AddInternal(backgroundLayer = new LegacyKiaiFlashingDrawable(() => getDrawableFor("circle")));
var foregroundLayer = getDrawableFor("circleoverlay"); var foregroundLayer = getDrawableFor("circleoverlay");
if (foregroundLayer != null) if (foregroundLayer != null)

View File

@ -14,8 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{ {
public class TaikoLegacySkinTransformer : LegacySkinTransformer public class TaikoLegacySkinTransformer : LegacySkinTransformer
{ {
public override bool IsProvidingLegacyResources => base.IsProvidingLegacyResources || hasHitCircle || hasBarLeft;
private readonly Lazy<bool> hasExplosion; private readonly Lazy<bool> hasExplosion;
private bool hasHitCircle => GetTexture("taikohitcircle") != null;
private bool hasBarLeft => GetTexture("taiko-bar-left") != null;
public TaikoLegacySkinTransformer(ISkin skin) public TaikoLegacySkinTransformer(ISkin skin)
: base(skin) : base(skin)
{ {
@ -42,14 +47,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
return null; return null;
case TaikoSkinComponents.InputDrum: case TaikoSkinComponents.InputDrum:
if (GetTexture("taiko-bar-left") != null) if (hasBarLeft)
return new LegacyInputDrum(); return new LegacyInputDrum();
return null; return null;
case TaikoSkinComponents.CentreHit: case TaikoSkinComponents.CentreHit:
case TaikoSkinComponents.RimHit: case TaikoSkinComponents.RimHit:
if (GetTexture("taikohitcircle") != null) if (hasHitCircle)
return new LegacyHit(taikoComponent.Component); return new LegacyHit(taikoComponent.Component);
return null; return null;

View File

@ -9,8 +9,10 @@ using osu.Framework.Extensions;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
@ -96,5 +98,41 @@ namespace osu.Game.Tests.Beatmaps
var second = beatmaps.GetWorkingBeatmap(beatmap, true); var second = beatmaps.GetWorkingBeatmap(beatmap, true);
Assert.That(first, Is.Not.SameAs(second)); Assert.That(first, Is.Not.SameAs(second));
}); });
[Test]
public void TestSavePreservesCollections() => AddStep("run test", () =>
{
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID).Detach());
var working = beatmaps.GetWorkingBeatmap(beatmap);
Assert.That(working.BeatmapInfo.BeatmapSet?.Files, Has.Count.GreaterThan(0));
string initialHash = working.BeatmapInfo.MD5Hash;
var preserveCollection = new BeatmapCollection("test contained");
preserveCollection.BeatmapMD5Hashes.Add(initialHash);
var noNewCollection = new BeatmapCollection("test not contained");
Realm.Write(r =>
{
r.Add(preserveCollection);
r.Add(noNewCollection);
});
Assert.That(preserveCollection.BeatmapMD5Hashes, Does.Contain(initialHash));
Assert.That(noNewCollection.BeatmapMD5Hashes, Does.Not.Contain(initialHash));
beatmaps.Save(working.BeatmapInfo, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo));
string finalHash = working.BeatmapInfo.MD5Hash;
Assert.That(finalHash, Is.Not.SameAs(initialHash));
Assert.That(preserveCollection.BeatmapMD5Hashes, Does.Not.Contain(initialHash));
Assert.That(preserveCollection.BeatmapMD5Hashes, Does.Contain(finalHash));
Assert.That(noNewCollection.BeatmapMD5Hashes, Does.Not.Contain(finalHash));
});
} }
} }

View File

@ -71,9 +71,9 @@ namespace osu.Game.Tests.Editing
[TestCase(1)] [TestCase(1)]
[TestCase(2)] [TestCase(2)]
public void TestSpeedMultiplier(float multiplier) public void TestSpeedMultiplierDoesNotChangeDistanceSnap(float multiplier)
{ {
assertSnapDistance(100 * multiplier, new HitObject assertSnapDistance(100, new HitObject
{ {
DifficultyControlPoint = new DifficultyControlPoint DifficultyControlPoint = new DifficultyControlPoint
{ {

View File

@ -38,7 +38,9 @@ namespace osu.Game.Tests.Skins
// Covers legacy song progress, UR counter, colour hit error metre. // Covers legacy song progress, UR counter, colour hit error metre.
"Archives/modified-classic-20220801.osk", "Archives/modified-classic-20220801.osk",
// Covers clicks/s counter // Covers clicks/s counter
"Archives/modified-default-20220818.osk" "Archives/modified-default-20220818.osk",
// Covers longest combo counter
"Archives/modified-default-20221012.osk"
}; };
/// <summary> /// <summary>

View File

@ -244,7 +244,10 @@ namespace osu.Game.Tests.Visual.Background
public void TestResumeFromPlayer() public void TestResumeFromPlayer()
{ {
performFullSetup(); performFullSetup();
AddStep("Move mouse to Visual Settings", () => InputManager.MoveMouseTo(playerLoader.VisualSettingsPos)); AddStep("Move mouse to Visual Settings location", () => InputManager.MoveMouseTo(playerLoader.ScreenSpaceDrawQuad.TopRight
+ new Vector2(-playerLoader.VisualSettingsPos.ScreenSpaceDrawQuad.Width,
playerLoader.VisualSettingsPos.ScreenSpaceDrawQuad.Height / 2
)));
AddStep("Resume PlayerLoader", () => player.Restart()); AddStep("Resume PlayerLoader", () => player.Restart());
AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddStep("Move mouse to center of screen", () => InputManager.MoveMouseTo(playerLoader.ScreenPos)); AddStep("Move mouse to center of screen", () => InputManager.MoveMouseTo(playerLoader.ScreenPos));

View File

@ -4,8 +4,10 @@
#nullable disable #nullable disable
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Screens.Edit.Components.RadioButtons; using osu.Game.Screens.Edit.Components.RadioButtons;
namespace osu.Game.Tests.Visual.Editing namespace osu.Game.Tests.Visual.Editing
@ -13,6 +15,9 @@ namespace osu.Game.Tests.Visual.Editing
[TestFixture] [TestFixture]
public class TestSceneEditorComposeRadioButtons : OsuTestScene public class TestSceneEditorComposeRadioButtons : OsuTestScene
{ {
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
public TestSceneEditorComposeRadioButtons() public TestSceneEditorComposeRadioButtons()
{ {
EditorRadioButtonCollection collection; EditorRadioButtonCollection collection;

View File

@ -148,10 +148,6 @@ namespace osu.Game.Tests.Visual.Editing
}); });
AddAssert("no circles placed", () => editorBeatmap.HitObjects.Count == 0); AddAssert("no circles placed", () => editorBeatmap.HitObjects.Count == 0);
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
AddAssert("circle placed", () => editorBeatmap.HitObjects.Count == 1);
} }
[Test] [Test]

View File

@ -29,16 +29,18 @@ namespace osu.Game.Tests.Visual.Editing
private TimelineBlueprintContainer blueprintContainer private TimelineBlueprintContainer blueprintContainer
=> Editor.ChildrenOfType<TimelineBlueprintContainer>().First(); => Editor.ChildrenOfType<TimelineBlueprintContainer>().First();
private Vector2 getPosition(HitObject hitObject) =>
blueprintContainer.SelectionBlueprints.First(s => s.Item == hitObject).ScreenSpaceDrawQuad.Centre;
private Vector2 getMiddlePosition(HitObject hitObject1, HitObject hitObject2) =>
(getPosition(hitObject1) + getPosition(hitObject2)) / 2;
private void moveMouseToObject(Func<HitObject> targetFunc) private void moveMouseToObject(Func<HitObject> targetFunc)
{ {
AddStep("move mouse to object", () => AddStep("move mouse to object", () =>
{ {
var pos = blueprintContainer.SelectionBlueprints var hitObject = targetFunc();
.First(s => s.Item == targetFunc()) InputManager.MoveMouseTo(getPosition(hitObject));
.ChildrenOfType<TimelineHitObjectBlueprint>()
.First().ScreenSpaceDrawQuad.Centre;
InputManager.MoveMouseTo(pos);
}); });
} }
@ -262,6 +264,56 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
} }
[Test]
public void TestBasicDragSelection()
{
var addedObjects = new[]
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 500, Position = new Vector2(100) },
new HitCircle { StartTime = 1000, Position = new Vector2(200) },
new HitCircle { StartTime = 1500, Position = new Vector2(300) },
};
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
AddStep("move mouse", () => InputManager.MoveMouseTo(getMiddlePosition(addedObjects[0], addedObjects[1])));
AddStep("mouse down", () => InputManager.PressButton(MouseButton.Left));
AddStep("drag to select", () => InputManager.MoveMouseTo(getMiddlePosition(addedObjects[2], addedObjects[3])));
assertSelectionIs(new[] { addedObjects[1], addedObjects[2] });
AddStep("drag to deselect", () => InputManager.MoveMouseTo(getMiddlePosition(addedObjects[1], addedObjects[2])));
assertSelectionIs(new[] { addedObjects[1] });
AddStep("mouse up", () => InputManager.ReleaseButton(MouseButton.Left));
assertSelectionIs(new[] { addedObjects[1] });
}
[Test]
public void TestFastDragSelection()
{
var addedObjects = new[]
{
new HitCircle { StartTime = 0 },
new HitCircle { StartTime = 500 },
new HitCircle { StartTime = 20000, Position = new Vector2(100) },
new HitCircle { StartTime = 31000, Position = new Vector2(200) },
new HitCircle { StartTime = 60000, Position = new Vector2(300) },
};
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
AddStep("move mouse", () => InputManager.MoveMouseTo(getMiddlePosition(addedObjects[0], addedObjects[1])));
AddStep("mouse down", () => InputManager.PressButton(MouseButton.Left));
AddStep("start drag", () => InputManager.MoveMouseTo(getPosition(addedObjects[1])));
AddStep("jump editor clock", () => EditorClock.Seek(30000));
AddStep("jump editor clock", () => EditorClock.Seek(60000));
AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left));
assertSelectionIs(addedObjects.Skip(1));
AddAssert("all blueprints are present", () => blueprintContainer.SelectionBlueprints.Count == EditorBeatmap.SelectedHitObjects.Count);
}
private void assertSelectionIs(IEnumerable<HitObject> hitObjects) private void assertSelectionIs(IEnumerable<HitObject> hitObjects)
=> AddAssert("correct hitobjects selected", () => EditorBeatmap.SelectedHitObjects.OrderBy(h => h.StartTime).SequenceEqual(hitObjects)); => AddAssert("correct hitobjects selected", () => EditorBeatmap.SelectedHitObjects.OrderBy(h => h.StartTime).SequenceEqual(hitObjects));
} }

View File

@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.Editing
} }
} }
}, },
new MenuCursor() new MenuCursorContainer()
}; };
scrollContainer.Add(innerBox = new Box scrollContainer.Add(innerBox = new Box

View File

@ -16,6 +16,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.HitErrorMeters; using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Tests.Gameplay; using osu.Game.Tests.Gameplay;
@ -148,6 +149,42 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent); AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent);
} }
[Test]
public void TestInputDoesntWorkWhenHUDHidden()
{
SongProgressBar getSongProgress() => hudOverlay.ChildrenOfType<SongProgressBar>().Single();
bool seeked = false;
createNew();
AddStep("bind seek", () =>
{
seeked = false;
var progress = getSongProgress();
progress.ShowHandle = true;
progress.OnSeek += _ => seeked = true;
});
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
AddStep("attempt seek", () =>
{
InputManager.MoveMouseTo(getSongProgress());
InputManager.Click(MouseButton.Left);
});
AddAssert("seek not performed", () => !seeked);
AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true);
AddStep("attempt seek", () => InputManager.Click(MouseButton.Left));
AddAssert("seek performed", () => seeked);
}
[Test] [Test]
public void TestHiddenHUDDoesntBlockComponentUpdates() public void TestHiddenHUDDoesntBlockComponentUpdates()
{ {

View File

@ -0,0 +1,498 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneScoring : OsuTestScene
{
private GraphContainer graphs = null!;
private SettingsSlider<int> sliderMaxCombo = null!;
private FillFlowContainer legend = null!;
[Test]
public void TestBasic()
{
AddStep("setup tests", () =>
{
Children = new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{
graphs = new GraphContainer
{
RelativeSizeAxes = Axes.Both,
},
},
new Drawable[]
{
legend = new FillFlowContainer
{
Padding = new MarginPadding(20),
Direction = FillDirection.Full,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
},
new Drawable[]
{
new FillFlowContainer
{
Padding = new MarginPadding(20),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Full,
Children = new Drawable[]
{
sliderMaxCombo = new SettingsSlider<int>
{
Width = 0.5f,
TransferValueOnCommit = true,
Current = new BindableInt(1024)
{
MinValue = 96,
MaxValue = 8192,
},
LabelText = "max combo",
},
new OsuTextFlowContainer
{
RelativeSizeAxes = Axes.X,
Width = 0.5f,
AutoSizeAxes = Axes.Y,
Text = $"Left click to add miss\nRight click to add OK/{base_ok}"
}
}
},
},
}
}
};
sliderMaxCombo.Current.BindValueChanged(_ => rerun());
graphs.MissLocations.BindCollectionChanged((_, __) => rerun());
graphs.NonPerfectLocations.BindCollectionChanged((_, __) => rerun());
graphs.MaxCombo.BindTo(sliderMaxCombo.Current);
rerun();
});
}
private const int base_great = 300;
private const int base_ok = 100;
private void rerun()
{
graphs.Clear();
legend.Clear();
runForProcessor("lazer-standardised", Color4.YellowGreen, new ScoreProcessor(new OsuRuleset()) { Mode = { Value = ScoringMode.Standardised } });
runForProcessor("lazer-classic", Color4.MediumPurple, new ScoreProcessor(new OsuRuleset()) { Mode = { Value = ScoringMode.Classic } });
runScoreV1();
runScoreV2();
}
private void runScoreV1()
{
int totalScore = 0;
int currentCombo = 0;
void applyHitV1(int baseScore)
{
if (baseScore == 0)
{
currentCombo = 0;
return;
}
const float score_multiplier = 1;
totalScore += baseScore;
// combo multiplier
// ReSharper disable once PossibleLossOfFraction
totalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * score_multiplier));
currentCombo++;
}
runForAlgorithm("ScoreV1 (classic)", Color4.Purple,
() => applyHitV1(base_great),
() => applyHitV1(base_ok),
() => applyHitV1(0),
() =>
{
// Arbitrary value chosen towards the upper range.
const double score_multiplier = 4;
return (int)(totalScore * score_multiplier);
});
}
private void runScoreV2()
{
int maxCombo = sliderMaxCombo.Current.Value;
int currentCombo = 0;
double comboPortion = 0;
double currentBaseScore = 0;
double maxBaseScore = 0;
int currentHits = 0;
for (int i = 0; i < maxCombo; i++)
applyHitV2(base_great);
double comboPortionMax = comboPortion;
currentCombo = 0;
comboPortion = 0;
currentBaseScore = 0;
maxBaseScore = 0;
currentHits = 0;
void applyHitV2(int baseScore)
{
maxBaseScore += base_great;
currentBaseScore += baseScore;
comboPortion += baseScore * (1 + ++currentCombo / 10.0);
currentHits++;
}
runForAlgorithm("ScoreV2", Color4.OrangeRed,
() => applyHitV2(base_great),
() => applyHitV2(base_ok),
() =>
{
currentHits++;
maxBaseScore += base_great;
currentCombo = 0;
}, () =>
{
double accuracy = currentBaseScore / maxBaseScore;
return (int)Math.Round
(
700000 * comboPortion / comboPortionMax +
300000 * Math.Pow(accuracy, 10) * ((double)currentHits / maxCombo)
);
});
}
private void runForProcessor(string name, Color4 colour, ScoreProcessor processor)
{
int maxCombo = sliderMaxCombo.Current.Value;
var beatmap = new OsuBeatmap();
for (int i = 0; i < maxCombo; i++)
beatmap.HitObjects.Add(new HitCircle());
processor.ApplyBeatmap(beatmap);
runForAlgorithm(name, colour,
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }),
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }),
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }),
() => (int)processor.TotalScore.Value);
}
private void runForAlgorithm(string name, Color4 colour, Action applyHit, Action applyNonPerfect, Action applyMiss, Func<int> getTotalScore)
{
int maxCombo = sliderMaxCombo.Current.Value;
List<float> results = new List<float>();
for (int i = 0; i < maxCombo; i++)
{
if (graphs.MissLocations.Contains(i))
applyMiss();
else if (graphs.NonPerfectLocations.Contains(i))
applyNonPerfect();
else
applyHit();
results.Add(getTotalScore());
}
graphs.Add(new LineGraph
{
Name = name,
RelativeSizeAxes = Axes.Both,
LineColour = colour,
Values = results
});
legend.Add(new OsuSpriteText
{
Colour = colour,
RelativeSizeAxes = Axes.X,
Width = 0.5f,
Text = $"{FontAwesome.Solid.Circle.Icon} {name}"
});
legend.Add(new OsuSpriteText
{
Colour = colour,
RelativeSizeAxes = Axes.X,
Width = 0.5f,
Text = $"final score {getTotalScore():#,0}"
});
}
}
public class GraphContainer : Container, IHasCustomTooltip<IEnumerable<LineGraph>>
{
public readonly BindableList<double> MissLocations = new BindableList<double>();
public readonly BindableList<double> NonPerfectLocations = new BindableList<double>();
public Bindable<int> MaxCombo = new Bindable<int>();
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
private readonly Box hoverLine;
private readonly Container missLines;
private readonly Container verticalGridLines;
public int CurrentHoverCombo { get; private set; }
public GraphContainer()
{
InternalChild = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = OsuColour.Gray(0.1f),
RelativeSizeAxes = Axes.Both,
},
verticalGridLines = new Container
{
RelativeSizeAxes = Axes.Both,
},
hoverLine = new Box
{
Colour = Color4.Yellow,
RelativeSizeAxes = Axes.Y,
Origin = Anchor.TopCentre,
Alpha = 0,
Width = 1,
},
missLines = new Container
{
Alpha = 0.6f,
RelativeSizeAxes = Axes.Both,
},
Content,
}
};
MissLocations.BindCollectionChanged((_, _) => updateMissLocations());
NonPerfectLocations.BindCollectionChanged((_, _) => updateMissLocations());
MaxCombo.BindValueChanged(_ =>
{
updateMissLocations();
updateVerticalGridLines();
}, true);
}
private void updateVerticalGridLines()
{
verticalGridLines.Clear();
for (int i = 0; i < MaxCombo.Value; i++)
{
if (i % 100 == 0)
{
verticalGridLines.AddRange(new Drawable[]
{
new Box
{
Colour = OsuColour.Gray(0.2f),
Origin = Anchor.TopCentre,
Width = 1,
RelativeSizeAxes = Axes.Y,
RelativePositionAxes = Axes.X,
X = (float)i / MaxCombo.Value,
},
new OsuSpriteText
{
RelativePositionAxes = Axes.X,
X = (float)i / MaxCombo.Value,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Text = $"{i:#,0}",
Rotation = -30,
Y = -20,
}
});
}
}
}
private void updateMissLocations()
{
missLines.Clear();
foreach (int miss in MissLocations)
{
missLines.Add(new Box
{
Colour = Color4.Red,
Origin = Anchor.TopCentre,
Width = 1,
RelativeSizeAxes = Axes.Y,
RelativePositionAxes = Axes.X,
X = (float)miss / MaxCombo.Value,
});
}
foreach (int miss in NonPerfectLocations)
{
missLines.Add(new Box
{
Colour = Color4.Orange,
Origin = Anchor.TopCentre,
Width = 1,
RelativeSizeAxes = Axes.Y,
RelativePositionAxes = Axes.X,
X = (float)miss / MaxCombo.Value,
});
}
}
protected override bool OnHover(HoverEvent e)
{
hoverLine.Show();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
hoverLine.Hide();
base.OnHoverLost(e);
}
protected override bool OnMouseMove(MouseMoveEvent e)
{
CurrentHoverCombo = (int)(e.MousePosition.X / DrawWidth * MaxCombo.Value);
hoverLine.X = e.MousePosition.X;
return base.OnMouseMove(e);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button == MouseButton.Left)
MissLocations.Add(CurrentHoverCombo);
else
NonPerfectLocations.Add(CurrentHoverCombo);
return true;
}
private GraphTooltip? tooltip;
public ITooltip<IEnumerable<LineGraph>> GetCustomTooltip() => tooltip ??= new GraphTooltip(this);
public IEnumerable<LineGraph> TooltipContent => Content.OfType<LineGraph>();
public class GraphTooltip : CompositeDrawable, ITooltip<IEnumerable<LineGraph>>
{
private readonly GraphContainer graphContainer;
private readonly OsuTextFlowContainer textFlow;
public GraphTooltip(GraphContainer graphContainer)
{
this.graphContainer = graphContainer;
AutoSizeAxes = Axes.Both;
Masking = true;
CornerRadius = 10;
InternalChildren = new Drawable[]
{
new Box
{
Colour = OsuColour.Gray(0.15f),
RelativeSizeAxes = Axes.Both,
},
textFlow = new OsuTextFlowContainer
{
Colour = Color4.White,
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding(10),
}
};
}
private int? lastContentCombo;
public void SetContent(IEnumerable<LineGraph> content)
{
int relevantCombo = graphContainer.CurrentHoverCombo;
if (lastContentCombo == relevantCombo)
return;
lastContentCombo = relevantCombo;
textFlow.Clear();
textFlow.AddParagraph($"At combo {relevantCombo}:");
foreach (var graph in content)
{
float valueAtHover = graph.Values.ElementAt(relevantCombo);
float ofTotal = valueAtHover / graph.Values.Last();
textFlow.AddParagraph($"{graph.Name}: {valueAtHover:#,0} ({ofTotal * 100:N0}% of final)\n", st => st.Colour = graph.LineColour);
}
}
public void Move(Vector2 pos) => this.MoveTo(pos);
}
}
}

View File

@ -214,7 +214,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void addControlPoints(IList<MultiplierControlPoint> controlPoints, double sequenceStartTime) private void addControlPoints(IList<MultiplierControlPoint> controlPoints, double sequenceStartTime)
{ {
controlPoints.ForEach(point => point.StartTime += sequenceStartTime); controlPoints.ForEach(point => point.Time += sequenceStartTime);
scrollContainers.ForEach(container => scrollContainers.ForEach(container =>
{ {
@ -224,7 +224,7 @@ namespace osu.Game.Tests.Visual.Gameplay
foreach (var playfield in playfields) foreach (var playfield in playfields)
{ {
foreach (var controlPoint in controlPoints) foreach (var controlPoint in controlPoints)
playfield.Add(createDrawablePoint(playfield, controlPoint.StartTime)); playfield.Add(createDrawablePoint(playfield, controlPoint.Time));
} }
} }

View File

@ -97,14 +97,23 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
[Test] [Test]
public void TestCurrentItemDoesNotHaveDeleteButton() public void TestSingleItemDoesNotHaveDeleteButton()
{
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode.Value == QueueMode.AllPlayers);
assertDeleteButtonVisibility(0, false);
}
[Test]
public void TestCurrentItemHasDeleteButtonIfNotSingle()
{ {
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely()); AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode.Value == QueueMode.AllPlayers); AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode.Value == QueueMode.AllPlayers);
addPlaylistItem(() => API.LocalUser.Value.OnlineID); addPlaylistItem(() => API.LocalUser.Value.OnlineID);
assertDeleteButtonVisibility(0, false); assertDeleteButtonVisibility(0, true);
assertDeleteButtonVisibility(1, true); assertDeleteButtonVisibility(1, true);
AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely()); AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely());

View File

@ -25,6 +25,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Navigation namespace osu.Game.Tests.Visual.Navigation
{ {
@ -79,6 +80,16 @@ namespace osu.Game.Tests.Visual.Navigation
[Resolved] [Resolved]
private OsuGameBase gameBase { get; set; } private OsuGameBase gameBase { get; set; }
[Test]
public void TestCursorHidesWhenIdle()
{
AddStep("click mouse", () => InputManager.Click(MouseButton.Left));
AddUntilStep("wait until idle", () => Game.IsIdle.Value);
AddUntilStep("menu cursor hidden", () => Game.GlobalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 0);
AddStep("click mouse", () => InputManager.Click(MouseButton.Left));
AddUntilStep("menu cursor shown", () => Game.GlobalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 1);
}
[Test] [Test]
public void TestNullRulesetHandled() public void TestNullRulesetHandled()
{ {

View File

@ -0,0 +1,257 @@
// 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;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Comments;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneCommentActions : OsuManualInputManagerTestScene
{
private Container<Drawable> content = null!;
protected override Container<Drawable> Content => content;
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
[Cached(typeof(IDialogOverlay))]
private readonly DialogOverlay dialogOverlay = new DialogOverlay();
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
private CommentsContainer commentsContainer = null!;
[BackgroundDependencyLoader]
private void load()
{
base.Content.AddRange(new Drawable[]
{
content = new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both
},
dialogOverlay
});
}
[SetUpSteps]
public void SetUp()
{
Schedule(() =>
{
API.Login("test", "test");
Child = commentsContainer = new CommentsContainer();
});
}
[Test]
public void TestNonOwnCommentCantBeDeleted()
{
addTestComments();
AddUntilStep("First comment has button", () =>
{
var comments = this.ChildrenOfType<DrawableComment>();
var ourComment = comments.SingleOrDefault(x => x.Comment.Id == 1);
return ourComment != null && ourComment.ChildrenOfType<OsuSpriteText>().Any(x => x.Text == "Delete");
});
AddAssert("Second doesn't", () =>
{
var comments = this.ChildrenOfType<DrawableComment>();
var ourComment = comments.Single(x => x.Comment.Id == 2);
return ourComment.ChildrenOfType<OsuSpriteText>().All(x => x.Text != "Delete");
});
}
private readonly ManualResetEventSlim deletionPerformed = new ManualResetEventSlim();
[Test]
public void TestDeletion()
{
DrawableComment? ourComment = null;
addTestComments();
AddUntilStep("Comment exists", () =>
{
var comments = this.ChildrenOfType<DrawableComment>();
ourComment = comments.SingleOrDefault(x => x.Comment.Id == 1);
return ourComment != null;
});
AddStep("It has delete button", () =>
{
var btn = ourComment.ChildrenOfType<OsuSpriteText>().Single(x => x.Text == "Delete");
InputManager.MoveMouseTo(btn);
});
AddStep("Click delete button", () =>
{
InputManager.Click(MouseButton.Left);
});
AddStep("Setup request handling", () =>
{
deletionPerformed.Reset();
dummyAPI.HandleRequest = request =>
{
if (!(request is CommentDeleteRequest req))
return false;
if (req.CommentId != 1)
return false;
CommentBundle cb = new CommentBundle
{
Comments = new List<Comment>
{
new Comment
{
Id = 2,
Message = "This is a comment by another user",
UserId = API.LocalUser.Value.Id + 1,
CreatedAt = DateTimeOffset.Now,
User = new APIUser
{
Id = API.LocalUser.Value.Id + 1,
Username = "Another user"
}
},
},
IncludedComments = new List<Comment>(),
PinnedComments = new List<Comment>(),
};
Task.Run(() =>
{
deletionPerformed.Wait(10000);
req.TriggerSuccess(cb);
});
return true;
};
});
AddStep("Confirm dialog", () => InputManager.Key(Key.Number1));
AddAssert("Loading spinner shown", () => commentsContainer.ChildrenOfType<LoadingSpinner>().Any(d => d.IsPresent));
AddStep("Complete request", () => deletionPerformed.Set());
AddUntilStep("Comment is deleted locally", () => this.ChildrenOfType<DrawableComment>().Single(x => x.Comment.Id == 1).WasDeleted);
}
[Test]
public void TestDeletionFail()
{
DrawableComment? ourComment = null;
bool delete = false;
addTestComments();
AddUntilStep("Comment exists", () =>
{
var comments = this.ChildrenOfType<DrawableComment>();
ourComment = comments.SingleOrDefault(x => x.Comment.Id == 1);
return ourComment != null;
});
AddStep("It has delete button", () =>
{
var btn = ourComment.ChildrenOfType<OsuSpriteText>().Single(x => x.Text == "Delete");
InputManager.MoveMouseTo(btn);
});
AddStep("Click delete button", () =>
{
InputManager.Click(MouseButton.Left);
});
AddStep("Setup request handling", () =>
{
dummyAPI.HandleRequest = request =>
{
if (request is not CommentDeleteRequest req)
return false;
req.TriggerFailure(new Exception());
delete = true;
return false;
};
});
AddStep("Confirm dialog", () => InputManager.Key(Key.Number1));
AddUntilStep("Deletion requested", () => delete);
AddUntilStep("Comment is available", () =>
{
return !this.ChildrenOfType<DrawableComment>().Single(x => x.Comment.Id == 1).WasDeleted;
});
AddAssert("Loading spinner hidden", () =>
{
return ourComment.ChildrenOfType<LoadingSpinner>().All(d => !d.IsPresent);
});
AddAssert("Actions available", () =>
{
return ourComment.ChildrenOfType<LinkFlowContainer>().Single(x => x.Name == @"Actions buttons").IsPresent;
});
}
private void addTestComments()
{
AddStep("set up response", () =>
{
CommentBundle cb = new CommentBundle
{
Comments = new List<Comment>
{
new Comment
{
Id = 1,
Message = "This is our comment",
UserId = API.LocalUser.Value.Id,
CreatedAt = DateTimeOffset.Now,
User = API.LocalUser.Value,
},
new Comment
{
Id = 2,
Message = "This is a comment by another user",
UserId = API.LocalUser.Value.Id + 1,
CreatedAt = DateTimeOffset.Now,
User = new APIUser
{
Id = API.LocalUser.Value.Id + 1,
Username = "Another user"
}
},
},
IncludedComments = new List<Comment>(),
PinnedComments = new List<Comment>(),
};
setUpCommentsResponse(cb);
});
AddStep("show comments", () => commentsContainer.ShowComments(CommentableType.Beatmapset, 123));
}
private void setUpCommentsResponse(CommentBundle commentBundle)
{
dummyAPI.HandleRequest = request =>
{
if (!(request is GetCommentsRequest getCommentsRequest))
return false;
getCommentsRequest.TriggerSuccess(commentBundle);
return true;
};
}
}
}

View File

@ -189,6 +189,16 @@ Line after image";
}); });
} }
[Test]
public void TestFlag()
{
AddStep("Add flag", () =>
{
markdownContainer.CurrentPath = @"https://dev.ppy.sh";
markdownContainer.Text = "::{flag=\"AU\"}:: ::{flag=\"ZZ\"}::";
});
}
private class TestMarkdownContainer : WikiMarkdownContainer private class TestMarkdownContainer : WikiMarkdownContainer
{ {
public LinkInline Link; public LinkInline Link;

File diff suppressed because one or more lines are too long

View File

@ -3,18 +3,17 @@
#nullable disable #nullable disable
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Tests.Visual.UserInterface;
namespace osu.Game.Tests.Visual.Settings namespace osu.Game.Tests.Visual.Settings
{ {
public class TestSceneDirectorySelector : OsuTestScene public class TestSceneDirectorySelector : ThemeComparisonTestScene
{ {
[BackgroundDependencyLoader] protected override Drawable CreateContent() => new OsuDirectorySelector
private void load()
{ {
Add(new OsuDirectorySelector { RelativeSizeAxes = Axes.Both }); RelativeSizeAxes = Axes.Both
} };
} }
} }

View File

@ -4,23 +4,43 @@
#nullable disable #nullable disable
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Tests.Visual.UserInterface;
namespace osu.Game.Tests.Visual.Settings namespace osu.Game.Tests.Visual.Settings
{ {
public class TestSceneFileSelector : OsuTestScene public class TestSceneFileSelector : ThemeComparisonTestScene
{ {
[Test] [Resolved]
public void TestAllFiles() private OsuColour colours { get; set; }
{
AddStep("create", () => Child = new OsuFileSelector { RelativeSizeAxes = Axes.Both });
}
[Test] [Test]
public void TestJpgFilesOnly() public void TestJpgFilesOnly()
{ {
AddStep("create", () => Child = new OsuFileSelector(validFileExtensions: new[] { ".jpg" }) { RelativeSizeAxes = Axes.Both }); AddStep("create", () =>
{
Cell(0, 0).Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.GreySeaFoam
},
new OsuFileSelector(validFileExtensions: new[] { ".jpg" })
{
RelativeSizeAxes = Axes.Both,
},
};
});
} }
protected override Drawable CreateContent() => new OsuFileSelector
{
RelativeSizeAxes = Axes.Both,
};
} }
} }

View File

@ -6,14 +6,18 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Carousel;
using osu.Game.Tests.Online; using osu.Game.Tests.Online;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect namespace osu.Game.Tests.Visual.SongSelect
{ {
@ -41,17 +45,7 @@ namespace osu.Game.Tests.Visual.SongSelect
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {
AddStep("create carousel", () => AddStep("create carousel", () => Child = createCarousel());
{
Child = carousel = new BeatmapCarousel
{
RelativeSizeAxes = Axes.Both,
BeatmapSets = new List<BeatmapSetInfo>
{
(testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo()),
}
};
});
AddUntilStep("wait for load", () => carousel.BeatmapSetsLoaded); AddUntilStep("wait for load", () => carousel.BeatmapSetsLoaded);
@ -152,5 +146,62 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("wait for button enabled", () => getUpdateButton()?.Enabled.Value == true); AddUntilStep("wait for button enabled", () => getUpdateButton()?.Enabled.Value == true);
} }
[Test]
public void TestUpdateLocalBeatmap()
{
DialogOverlay dialogOverlay = null!;
UpdateBeatmapSetButton? updateButton = null;
AddStep("create carousel with dialog overlay", () =>
{
dialogOverlay = new DialogOverlay();
Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[] { (typeof(IDialogOverlay), dialogOverlay), },
Children = new Drawable[]
{
createCarousel(),
dialogOverlay,
},
};
});
AddStep("setup beatmap state", () =>
{
testBeatmapSetInfo.Beatmaps.First().OnlineMD5Hash = "different hash";
testBeatmapSetInfo.Beatmaps.First().LastOnlineUpdate = DateTimeOffset.Now;
testBeatmapSetInfo.Status = BeatmapOnlineStatus.LocallyModified;
carousel.UpdateBeatmapSet(testBeatmapSetInfo);
});
AddUntilStep("wait for update button", () => (updateButton = getUpdateButton()) != null);
AddStep("click button", () => updateButton.AsNonNull().TriggerClick());
AddAssert("dialog displayed", () => dialogOverlay.CurrentDialog is UpdateLocalConfirmationDialog);
AddStep("click confirmation", () =>
{
InputManager.MoveMouseTo(dialogOverlay.CurrentDialog.ChildrenOfType<PopupDialogButton>().First());
InputManager.PressButton(MouseButton.Left);
});
AddUntilStep("update started", () => beatmapDownloader.GetExistingDownload(testBeatmapSetInfo) != null);
AddStep("release mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
}
private BeatmapCarousel createCarousel()
{
return carousel = new BeatmapCarousel
{
RelativeSizeAxes = Axes.Both,
BeatmapSets = new List<BeatmapSetInfo>
{
(testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo()),
}
};
}
} }
} }

View File

@ -15,6 +15,7 @@ using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
{ {
@ -81,25 +82,24 @@ namespace osu.Game.Tests.Visual.UserInterface
}; };
AddToggleStep("Smooth transitions", b => cursorBoxes.ForEach(box => box.SmoothTransition = b)); AddToggleStep("Smooth transitions", b => cursorBoxes.ForEach(box => box.SmoothTransition = b));
testUserCursor();
testLocalCursor();
testUserCursorOverride();
testMultipleLocalCursors();
} }
[SetUp]
public void SetUp() => Schedule(moveOut);
/// <summary> /// <summary>
/// -- Green Box -- /// -- Green Box --
/// Tests whether hovering in and out of a drawable that provides the user cursor (green) /// Tests whether hovering in and out of a drawable that provides the user cursor (green)
/// results in the correct visibility state for that cursor. /// results in the correct visibility state for that cursor.
/// </summary> /// </summary>
private void testUserCursor() [Test]
public void TestUserCursor()
{ {
AddStep("Move to green area", () => InputManager.MoveMouseTo(cursorBoxes[0])); AddStep("Move to green area", () => InputManager.MoveMouseTo(cursorBoxes[0]));
AddAssert("Check green cursor visible", () => checkVisible(cursorBoxes[0].MenuCursor)); AddAssert("Check green cursor visible", () => checkVisible(cursorBoxes[0].Cursor));
AddAssert("Check green cursor at mouse", () => checkAtMouse(cursorBoxes[0].MenuCursor)); AddAssert("Check green cursor at mouse", () => checkAtMouse(cursorBoxes[0].Cursor));
AddStep("Move out", moveOut); AddStep("Move out", moveOut);
AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].MenuCursor)); AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].Cursor));
AddAssert("Check global cursor visible", () => checkVisible(globalCursorDisplay.MenuCursor)); AddAssert("Check global cursor visible", () => checkVisible(globalCursorDisplay.MenuCursor));
} }
@ -108,15 +108,16 @@ namespace osu.Game.Tests.Visual.UserInterface
/// Tests whether hovering in and out of a drawable that provides a local cursor (purple) /// Tests whether hovering in and out of a drawable that provides a local cursor (purple)
/// results in the correct visibility and state for that cursor. /// results in the correct visibility and state for that cursor.
/// </summary> /// </summary>
private void testLocalCursor() [Test]
public void TestLocalCursor()
{ {
AddStep("Move to purple area", () => InputManager.MoveMouseTo(cursorBoxes[3])); AddStep("Move to purple area", () => InputManager.MoveMouseTo(cursorBoxes[3]));
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor)); AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].MenuCursor)); AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].Cursor));
AddAssert("Check global cursor visible", () => checkVisible(globalCursorDisplay.MenuCursor)); AddAssert("Check global cursor visible", () => checkVisible(globalCursorDisplay.MenuCursor));
AddAssert("Check global cursor at mouse", () => checkAtMouse(globalCursorDisplay.MenuCursor)); AddAssert("Check global cursor at mouse", () => checkAtMouse(globalCursorDisplay.MenuCursor));
AddStep("Move out", moveOut); AddStep("Move out", moveOut);
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor)); AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
AddAssert("Check global cursor visible", () => checkVisible(globalCursorDisplay.MenuCursor)); AddAssert("Check global cursor visible", () => checkVisible(globalCursorDisplay.MenuCursor));
} }
@ -125,47 +126,98 @@ namespace osu.Game.Tests.Visual.UserInterface
/// Tests whether overriding a user cursor (green) with another user cursor (blue) /// Tests whether overriding a user cursor (green) with another user cursor (blue)
/// results in the correct visibility and states for the cursors. /// results in the correct visibility and states for the cursors.
/// </summary> /// </summary>
private void testUserCursorOverride() [Test]
public void TestUserCursorOverride()
{ {
AddStep("Move to blue-green boundary", () => InputManager.MoveMouseTo(cursorBoxes[1].ScreenSpaceDrawQuad.BottomRight - new Vector2(10))); AddStep("Move to blue-green boundary", () => InputManager.MoveMouseTo(cursorBoxes[1].ScreenSpaceDrawQuad.BottomRight - new Vector2(10)));
AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].MenuCursor)); AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].Cursor));
AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].MenuCursor)); AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].Cursor));
AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].MenuCursor)); AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].Cursor));
AddStep("Move out", moveOut); AddStep("Move out", moveOut);
AddAssert("Check blue cursor not visible", () => !checkVisible(cursorBoxes[1].MenuCursor)); AddAssert("Check blue cursor not visible", () => !checkVisible(cursorBoxes[1].Cursor));
AddAssert("Check green cursor not visible", () => !checkVisible(cursorBoxes[0].MenuCursor)); AddAssert("Check green cursor not visible", () => !checkVisible(cursorBoxes[0].Cursor));
} }
/// <summary> /// <summary>
/// -- Yellow-Purple Box Boundary -- /// -- Yellow-Purple Box Boundary --
/// Tests whether multiple local cursors (purple + yellow) may be visible and at the mouse position at the same time. /// Tests whether multiple local cursors (purple + yellow) may be visible and at the mouse position at the same time.
/// </summary> /// </summary>
private void testMultipleLocalCursors() [Test]
public void TestMultipleLocalCursors()
{ {
AddStep("Move to yellow-purple boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.BottomRight - new Vector2(10))); AddStep("Move to yellow-purple boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.BottomRight - new Vector2(10)));
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor)); AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].MenuCursor)); AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].Cursor));
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor)); AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].MenuCursor)); AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].Cursor));
AddStep("Move out", moveOut); AddStep("Move out", moveOut);
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor)); AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor)); AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
} }
/// <summary> /// <summary>
/// -- Yellow-Blue Box Boundary -- /// -- Yellow-Blue Box Boundary --
/// Tests whether a local cursor (yellow) may be displayed along with a user cursor override (blue). /// Tests whether a local cursor (yellow) may be displayed along with a user cursor override (blue).
/// </summary> /// </summary>
private void testUserOverrideWithLocal() [Test]
public void TestUserOverrideWithLocal()
{ {
AddStep("Move to yellow-blue boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.TopRight - new Vector2(10))); AddStep("Move to yellow-blue boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.TopRight - new Vector2(10, 0)));
AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].MenuCursor)); AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].Cursor));
AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].MenuCursor)); AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].Cursor));
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor)); AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].MenuCursor)); AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].Cursor));
AddStep("Move out", moveOut); AddStep("Move out", moveOut);
AddAssert("Check blue cursor invisible", () => !checkVisible(cursorBoxes[1].MenuCursor)); AddAssert("Check blue cursor invisible", () => !checkVisible(cursorBoxes[1].Cursor));
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor)); AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
}
/// <summary>
/// Ensures non-mouse input hides global cursor on a "local cursor" area (which doesn't hide global cursor).
/// </summary>
[Test]
public void TestKeyboardLocalCursor([Values] bool clickToShow)
{
AddStep("Enable cursor hiding", () => globalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = true);
AddStep("Move to purple area", () => InputManager.MoveMouseTo(cursorBoxes[3].ScreenSpaceDrawQuad.Centre + new Vector2(10, 0)));
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
AddAssert("Check global cursor alpha is 1", () => globalCursorDisplay.MenuCursor.Alpha == 1);
AddStep("Press key", () => InputManager.Key(Key.A));
AddAssert("Check purple cursor still visible", () => checkVisible(cursorBoxes[3].Cursor));
AddUntilStep("Check global cursor alpha is 0", () => globalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 0);
if (clickToShow)
AddStep("Click mouse", () => InputManager.Click(MouseButton.Left));
else
AddStep("Move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + Vector2.One));
AddAssert("Check purple cursor still visible", () => checkVisible(cursorBoxes[3].Cursor));
AddUntilStep("Check global cursor alpha is 1", () => globalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 1);
}
/// <summary>
/// Ensures mouse input after non-mouse input doesn't show global cursor on a "user cursor" area (which hides global cursor).
/// </summary>
[Test]
public void TestKeyboardUserCursor([Values] bool clickToShow)
{
AddStep("Enable cursor hiding", () => globalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = true);
AddStep("Move to green area", () => InputManager.MoveMouseTo(cursorBoxes[0]));
AddAssert("Check green cursor visible", () => checkVisible(cursorBoxes[0].Cursor));
AddAssert("Check global cursor alpha is 0", () => !checkVisible(globalCursorDisplay.MenuCursor) && globalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 0);
AddStep("Press key", () => InputManager.Key(Key.A));
AddAssert("Check green cursor still visible", () => checkVisible(cursorBoxes[0].Cursor));
AddAssert("Check global cursor alpha is still 0", () => !checkVisible(globalCursorDisplay.MenuCursor) && globalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 0);
if (clickToShow)
AddStep("Click mouse", () => InputManager.Click(MouseButton.Left));
else
AddStep("Move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + Vector2.One));
AddAssert("Check green cursor still visible", () => checkVisible(cursorBoxes[0].Cursor));
AddAssert("Check global cursor alpha is still 0", () => !checkVisible(globalCursorDisplay.MenuCursor) && globalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 0);
} }
/// <summary> /// <summary>
@ -191,7 +243,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
public bool SmoothTransition; public bool SmoothTransition;
public CursorContainer MenuCursor { get; } public CursorContainer Cursor { get; }
public bool ProvidingUserCursor { get; } public bool ProvidingUserCursor { get; }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || (SmoothTransition && !ProvidingUserCursor); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || (SmoothTransition && !ProvidingUserCursor);
@ -218,7 +270,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Origin = Anchor.Centre, Origin = Anchor.Centre,
Text = providesUserCursor ? "User cursor" : "Local cursor" Text = providesUserCursor ? "User cursor" : "Local cursor"
}, },
MenuCursor = new TestCursorContainer Cursor = new TestCursorContainer
{ {
State = { Value = providesUserCursor ? Visibility.Hidden : Visibility.Visible }, State = { Value = providesUserCursor ? Visibility.Hidden : Visibility.Visible },
} }

View File

@ -5,12 +5,14 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
@ -20,11 +22,17 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
private SettingsToolboxGroup group; private SettingsToolboxGroup group;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
[SetUp] [SetUp]
public void SetUp() => Schedule(() => public void SetUp() => Schedule(() =>
{ {
Child = group = new SettingsToolboxGroup("example") Child = group = new SettingsToolboxGroup("example")
{ {
Scale = new Vector2(3),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[] Children = new Drawable[]
{ {
new RoundedButton new RoundedButton

View File

@ -6,6 +6,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Drawing; using System.Drawing;
using System.Globalization;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace osu.Game.Tournament namespace osu.Game.Tournament
@ -31,7 +32,9 @@ namespace osu.Game.Tournament
Debug.Assert(str != null); Debug.Assert(str != null);
return new PointConverter().ConvertFromString(str) as Point? ?? new Point(); // Null check suppression is required due to .NET standard expecting a non-null context.
// Seems to work fine at a runtime level (and the parameter is nullable in .NET 6+).
return new PointConverter().ConvertFromString(null!, CultureInfo.InvariantCulture, str) as Point? ?? new Point();
} }
var point = new Point(); var point = new Point();

View File

@ -239,17 +239,17 @@ namespace osu.Game.Tournament.Screens.Editors
var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = Model.ID }); var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = Model.ID });
req.Success += res => req.Success += res => Schedule(() =>
{ {
Model.Beatmap = new TournamentBeatmap(res); Model.Beatmap = new TournamentBeatmap(res);
updatePanel(); updatePanel();
}; });
req.Failure += _ => req.Failure += _ => Schedule(() =>
{ {
Model.Beatmap = null; Model.Beatmap = null;
updatePanel(); updatePanel();
}; });
API.Queue(req); API.Queue(req);
}, true); }, true);

View File

@ -141,18 +141,9 @@ namespace osu.Game.Beatmaps
// Handle collections using permissive difficulty name to track difficulties. // Handle collections using permissive difficulty name to track difficulties.
foreach (var originalBeatmap in original.Beatmaps) foreach (var originalBeatmap in original.Beatmaps)
{ {
var updatedBeatmap = updated.Beatmaps.FirstOrDefault(b => b.DifficultyName == originalBeatmap.DifficultyName); updated.Beatmaps
.FirstOrDefault(b => b.DifficultyName == originalBeatmap.DifficultyName)?
if (updatedBeatmap == null) .TransferCollectionReferences(realm, originalBeatmap.MD5Hash);
continue;
var collections = realm.All<BeatmapCollection>().AsEnumerable().Where(c => c.BeatmapMD5Hashes.Contains(originalBeatmap.MD5Hash));
foreach (var c in collections)
{
c.BeatmapMD5Hashes.Remove(originalBeatmap.MD5Hash);
c.BeatmapMD5Hashes.Add(updatedBeatmap.MD5Hash);
}
} }
} }

View File

@ -8,6 +8,7 @@ using JetBrains.Annotations;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Collections;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Models; using osu.Game.Models;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
@ -213,6 +214,23 @@ namespace osu.Game.Beatmaps
return fileHashX == fileHashY; return fileHashX == fileHashY;
} }
/// <summary>
/// When updating a beatmap, its hashes will change. Collections currently track beatmaps by hash, so they need to be updated.
/// This method will handle updating
/// </summary>
/// <param name="realm">A realm instance in an active write transaction.</param>
/// <param name="previousMD5Hash">The previous MD5 hash of the beatmap before update.</param>
public void TransferCollectionReferences(Realm realm, string previousMD5Hash)
{
var collections = realm.All<BeatmapCollection>().AsEnumerable().Where(c => c.BeatmapMD5Hashes.Contains(previousMD5Hash));
foreach (var c in collections)
{
c.BeatmapMD5Hashes.Remove(previousMD5Hash);
c.BeatmapMD5Hashes.Add(MD5Hash);
}
}
IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata; IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet; IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
IRulesetInfo IBeatmapInfo.Ruleset => Ruleset; IRulesetInfo IBeatmapInfo.Ruleset => Ruleset;

View File

@ -311,6 +311,8 @@ namespace osu.Game.Beatmaps
if (existingFileInfo != null) if (existingFileInfo != null)
DeleteFile(setInfo, existingFileInfo); DeleteFile(setInfo, existingFileInfo);
string oldMd5Hash = beatmapInfo.MD5Hash;
beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
beatmapInfo.Hash = stream.ComputeSHA2Hash(); beatmapInfo.Hash = stream.ComputeSHA2Hash();
@ -327,6 +329,8 @@ namespace osu.Game.Beatmaps
setInfo.CopyChangesToRealm(liveBeatmapSet); setInfo.CopyChangesToRealm(liveBeatmapSet);
beatmapInfo.TransferCollectionReferences(r, oldMd5Hash);
ProcessBeatmap?.Invoke((liveBeatmapSet, false)); ProcessBeatmap?.Invoke((liveBeatmapSet, false));
}); });
} }

View File

@ -9,11 +9,8 @@ using osuTK.Graphics;
namespace osu.Game.Beatmaps.ControlPoints namespace osu.Game.Beatmaps.ControlPoints
{ {
public abstract class ControlPoint : IComparable<ControlPoint>, IDeepCloneable<ControlPoint>, IEquatable<ControlPoint> public abstract class ControlPoint : IComparable<ControlPoint>, IDeepCloneable<ControlPoint>, IEquatable<ControlPoint>, IControlPoint
{ {
/// <summary>
/// The time at which the control point takes effect.
/// </summary>
[JsonIgnore] [JsonIgnore]
public double Time { get; set; } public double Time { get; set; }

View File

@ -196,8 +196,8 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <param name="time">The time to find the control point at.</param> /// <param name="time">The time to find the control point at.</param>
/// <param name="fallback">The control point to use when <paramref name="time"/> is before any control points.</param> /// <param name="fallback">The control point to use when <paramref name="time"/> is before any control points.</param>
/// <returns>The active control point at <paramref name="time"/>, or a fallback <see cref="ControlPoint"/> if none found.</returns> /// <returns>The active control point at <paramref name="time"/>, or a fallback <see cref="ControlPoint"/> if none found.</returns>
protected T BinarySearchWithFallback<T>(IReadOnlyList<T> list, double time, T fallback) public static T BinarySearchWithFallback<T>(IReadOnlyList<T> list, double time, T fallback)
where T : ControlPoint where T : class, IControlPoint
{ {
return BinarySearch(list, time) ?? fallback; return BinarySearch(list, time) ?? fallback;
} }
@ -207,9 +207,9 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary> /// </summary>
/// <param name="list">The list to search.</param> /// <param name="list">The list to search.</param>
/// <param name="time">The time to find the control point at.</param> /// <param name="time">The time to find the control point at.</param>
/// <returns>The active control point at <paramref name="time"/>.</returns> /// <returns>The active control point at <paramref name="time"/>. Will return <c>null</c> if there are no control points, or if the time is before the first control point.</returns>
protected virtual T BinarySearch<T>(IReadOnlyList<T> list, double time) public static T BinarySearch<T>(IReadOnlyList<T> list, double time)
where T : ControlPoint where T : class, IControlPoint
{ {
if (list == null) if (list == null)
throw new ArgumentNullException(nameof(list)); throw new ArgumentNullException(nameof(list));

View File

@ -0,0 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Beatmaps.ControlPoints
{
public interface IControlPoint
{
/// <summary>
/// The time at which the control point takes effect.
/// </summary>
double Time { get; }
}
}

View File

@ -355,6 +355,14 @@ namespace osu.Game.Beatmaps.Formats
switch (type) switch (type)
{ {
case LegacyEventType.Sprite:
// Generally, the background is the first thing defined in a beatmap file.
// In some older beatmaps, it is not present and replaced by a storyboard-level background instead.
// Allow the first sprite (by file order) to act as the background in such cases.
if (string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.BackgroundFile))
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[3]);
break;
case LegacyEventType.Background: case LegacyEventType.Background:
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]); beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]);
break; break;

View File

@ -134,6 +134,6 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// Reads the correct track restart point from beatmap metadata and sets looping to enabled. /// Reads the correct track restart point from beatmap metadata and sets looping to enabled.
/// </summary> /// </summary>
void PrepareTrackForPreview(bool looping); void PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint = 0);
} }
} }

View File

@ -110,7 +110,7 @@ namespace osu.Game.Beatmaps
public Track LoadTrack() => track = GetBeatmapTrack() ?? GetVirtualTrack(1000); public Track LoadTrack() => track = GetBeatmapTrack() ?? GetVirtualTrack(1000);
public void PrepareTrackForPreview(bool looping) public void PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint = 0)
{ {
Track.Looping = looping; Track.Looping = looping;
Track.RestartPoint = Metadata.PreviewTime; Track.RestartPoint = Metadata.PreviewTime;
@ -125,6 +125,8 @@ namespace osu.Game.Beatmaps
Track.RestartPoint = 0.4f * Track.Length; Track.RestartPoint = 0.4f * Track.Length;
} }
Track.RestartPoint += offsetFromPreviewPoint;
} }
/// <summary> /// <summary>

View File

@ -45,7 +45,7 @@ namespace osu.Game.Database
public bool Download(T model, bool minimiseDownloadSize = false) => Download(model, minimiseDownloadSize, null); public bool Download(T model, bool minimiseDownloadSize = false) => Download(model, minimiseDownloadSize, null);
public void DownloadAsUpdate(TModel originalModel) => Download(originalModel, false, originalModel); public void DownloadAsUpdate(TModel originalModel, bool minimiseDownloadSize) => Download(originalModel, minimiseDownloadSize, originalModel);
protected bool Download(T model, bool minimiseDownloadSize, TModel? originalModel) protected bool Download(T model, bool minimiseDownloadSize, TModel? originalModel)
{ {

View File

@ -294,15 +294,38 @@ namespace osu.Game.Database
// Log output here will be missing a valid hash in non-batch imports. // Log output here will be missing a valid hash in non-batch imports.
LogForModel(item, $@"Beginning import from {archive?.Name ?? "unknown"}..."); LogForModel(item, $@"Beginning import from {archive?.Name ?? "unknown"}...");
List<RealmNamedFileUsage> files = new List<RealmNamedFileUsage>();
if (archive != null)
{
// Import files to the disk store.
// We intentionally delay adding to realm to avoid blocking on a write during disk operations.
foreach (var filenames in getShortenedFilenames(archive))
{
using (Stream s = archive.GetStream(filenames.original))
files.Add(new RealmNamedFileUsage(Files.Add(s, realm, false), filenames.shortened));
}
}
using (var transaction = realm.BeginWrite())
{
// Add all files to realm in one go.
// This is done ahead of the main transaction to ensure we can correctly cleanup the files, even if the import fails.
foreach (var file in files)
{
if (!file.File.IsManaged)
realm.Add(file.File, true);
}
transaction.Commit();
}
item.Files.AddRange(files);
item.Hash = ComputeHash(item);
// TODO: do we want to make the transaction this local? not 100% sure, will need further investigation. // TODO: do we want to make the transaction this local? not 100% sure, will need further investigation.
using (var transaction = realm.BeginWrite()) using (var transaction = realm.BeginWrite())
{ {
if (archive != null)
// TODO: look into rollback of file additions (or delayed commit).
item.Files.AddRange(createFileInfos(archive, Files, realm));
item.Hash = ComputeHash(item);
// TODO: we may want to run this outside of the transaction. // TODO: we may want to run this outside of the transaction.
Populate(item, archive, realm, cancellationToken); Populate(item, archive, realm, cancellationToken);
@ -425,16 +448,6 @@ namespace osu.Game.Database
{ {
var fileInfos = new List<RealmNamedFileUsage>(); var fileInfos = new List<RealmNamedFileUsage>();
// import files to manager
foreach (var filenames in getShortenedFilenames(reader))
{
using (Stream s = reader.GetStream(filenames.original))
{
var item = new RealmNamedFileUsage(files.Add(s, realm), filenames.shortened);
fileInfos.Add(item);
}
}
return fileInfos; return fileInfos;
} }

View File

@ -40,8 +40,8 @@ namespace osu.Game.Database
/// </summary> /// </summary>
/// <param name="data">The file data stream.</param> /// <param name="data">The file data stream.</param>
/// <param name="realm">The realm instance to add to. Should already be in a transaction.</param> /// <param name="realm">The realm instance to add to. Should already be in a transaction.</param>
/// <returns></returns> /// <param name="addToRealm">Whether the <see cref="RealmFile"/> should immediately be added to the underlying realm. If <c>false</c> is provided here, the instance must be manually added.</param>
public RealmFile Add(Stream data, Realm realm) public RealmFile Add(Stream data, Realm realm, bool addToRealm = true)
{ {
string hash = data.ComputeSHA2Hash(); string hash = data.ComputeSHA2Hash();
@ -52,7 +52,7 @@ namespace osu.Game.Database
if (!checkFileExistsAndMatchesHash(file)) if (!checkFileExistsAndMatchesHash(file))
copyToStore(file, data); copyToStore(file, data);
if (!file.IsManaged) if (addToRealm && !file.IsManaged)
realm.Add(file); realm.Add(file);
return file; return file;

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