mirror of
https://github.com/ppy/osu.git
synced 2024-11-06 06:57:39 +08:00
Merge branch 'master' into sv_to_scrollspeed
This commit is contained in:
commit
73a9fac6d2
@ -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.
|
||||
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.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.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.
|
||||
|
@ -51,8 +51,8 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1008.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.1011.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1021.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.1022.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Dependencies">
|
||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||
|
@ -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 });
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
// 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 System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Utils;
|
||||
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.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -19,15 +21,28 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
public class TestSceneComboCounter : CatchSkinnableTestScene
|
||||
{
|
||||
private ScoreProcessor scoreProcessor;
|
||||
private ScoreProcessor scoreProcessor = null!;
|
||||
|
||||
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]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
scoreProcessor = new ScoreProcessor(new CatchRuleset());
|
||||
|
||||
showHud.Value = true;
|
||||
|
||||
SetContents(_ => new CatchComboDisplay
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
@ -51,9 +66,15 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
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 } };
|
||||
|
||||
|
@ -36,5 +36,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
|
||||
return base.CreateHitObjectBlueprintFor(hitObject);
|
||||
}
|
||||
|
||||
protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
@ -10,10 +11,11 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
@ -21,7 +23,6 @@ using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
@ -33,10 +34,14 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
|
||||
private CatchDistanceSnapGrid distanceSnapGrid;
|
||||
|
||||
private readonly Bindable<TernaryState> distanceSnapToggle = new Bindable<TernaryState>();
|
||||
|
||||
private InputManager inputManager;
|
||||
|
||||
private readonly BindableDouble timeRangeMultiplier = new BindableDouble(1)
|
||||
{
|
||||
MinValue = 1,
|
||||
MaxValue = 10,
|
||||
};
|
||||
|
||||
public CatchHitObjectComposer(CatchRuleset ruleset)
|
||||
: base(ruleset)
|
||||
{
|
||||
@ -51,7 +56,10 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
|
||||
LayerBelowRuleset.Add(new PlayfieldBorder
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = CatchPlayfield.HEIGHT,
|
||||
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
|
||||
});
|
||||
|
||||
@ -70,6 +78,19 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
inputManager = GetContainingInputManager();
|
||||
}
|
||||
|
||||
protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
|
||||
{
|
||||
// osu!catch's distance snap implementation is limited, in that a custom spacing cannot be specified.
|
||||
// Therefore this functionality is not currently used.
|
||||
//
|
||||
// The implementation below is probably correct but should be checked if/when exposed via controls.
|
||||
|
||||
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
|
||||
float actualDistance = Math.Abs(((CatchHitObject)before).EffectiveX - ((CatchHitObject)after).EffectiveX);
|
||||
|
||||
return actualDistance / expectedDistance;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
@ -77,8 +98,30 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
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) =>
|
||||
new DrawableCatchEditorRuleset(ruleset, beatmap, mods);
|
||||
new DrawableCatchEditorRuleset(ruleset, beatmap, mods)
|
||||
{
|
||||
TimeRangeMultiplier = { BindTarget = timeRangeMultiplier, }
|
||||
};
|
||||
|
||||
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => new HitObjectCompositionTool[]
|
||||
{
|
||||
@ -87,11 +130,6 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
new BananaShowerCompositionTool()
|
||||
};
|
||||
|
||||
protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
|
||||
{
|
||||
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
|
||||
});
|
||||
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
{
|
||||
var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
||||
@ -163,7 +201,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
|
||||
private void updateDistanceSnapGrid()
|
||||
{
|
||||
if (distanceSnapToggle.Value != TernaryState.True)
|
||||
if (DistanceSnapToggle.Value != TernaryState.True)
|
||||
{
|
||||
distanceSnapGrid.Hide();
|
||||
return;
|
||||
|
@ -4,6 +4,7 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@ -13,11 +14,24 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
public class DrawableCatchEditorRuleset : DrawableCatchRuleset
|
||||
{
|
||||
public readonly BindableDouble TimeRangeMultiplier = new BindableDouble(1);
|
||||
|
||||
public DrawableCatchEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||
: 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);
|
||||
|
||||
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchEditorPlayfieldAdjustmentContainer();
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
this.playfield = playfield;
|
||||
|
||||
FlashlightSize = new Vector2(0, GetSizeFor(0));
|
||||
FlashlightSize = new Vector2(0, GetSize());
|
||||
FlashlightSmoothness = 1.4f;
|
||||
}
|
||||
|
||||
@ -66,9 +66,9 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
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";
|
||||
|
@ -19,17 +19,20 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
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)
|
||||
{
|
||||
this.drawableRuleset = drawableRuleset;
|
||||
this.drawableRuleset = (DrawableCatchRuleset)drawableRuleset;
|
||||
}
|
||||
|
||||
public void ApplyToPlayer(Player player)
|
||||
{
|
||||
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
|
||||
@ -38,9 +41,10 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||
|
||||
public MouseInputHelper(CatchPlayfield playfield)
|
||||
public MouseInputHelper(CatcherArea catcherArea)
|
||||
{
|
||||
catcherArea = playfield.CatcherArea;
|
||||
this.catcherArea = catcherArea;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,10 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
{
|
||||
public class CatchLegacySkinTransformer : LegacySkinTransformer
|
||||
{
|
||||
public override bool IsProvidingLegacyResources => base.IsProvidingLegacyResources || hasPear;
|
||||
|
||||
private bool hasPear => GetTexture("fruit-pear") != null;
|
||||
|
||||
/// <summary>
|
||||
/// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default.
|
||||
/// </summary>
|
||||
@ -49,7 +53,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
switch (catchSkinComponent.Component)
|
||||
{
|
||||
case CatchSkinComponents.Fruit:
|
||||
if (GetTexture("fruit-pear") != null)
|
||||
if (hasPear)
|
||||
return new LegacyFruitPiece();
|
||||
|
||||
return null;
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
|
@ -1,12 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -19,14 +20,29 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
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()
|
||||
: 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)
|
||||
{
|
||||
base.SkinChanged(skin);
|
||||
|
@ -23,6 +23,12 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// </summary>
|
||||
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>
|
||||
/// The center position of the playfield.
|
||||
/// </summary>
|
||||
|
@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
: base(ruleset, beatmap, mods)
|
||||
{
|
||||
Direction.Value = ScrollingDirection.Down;
|
||||
TimeRange.Value = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450);
|
||||
TimeRange.Value = GetTimeRange(beatmap.Difficulty.ApproachRate);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -42,6 +42,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
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 ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield);
|
||||
|
@ -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 });
|
||||
}
|
||||
}
|
@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
assertHeadJudgement(HitResult.Miss);
|
||||
assertTickJudgement(HitResult.LargeTickMiss);
|
||||
assertTailJudgement(HitResult.Miss);
|
||||
assertNoteJudgement(HitResult.IgnoreHit);
|
||||
assertNoteJudgement(HitResult.IgnoreMiss);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -76,8 +76,8 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
|
||||
performTest(objects, new List<ReplayFrame>());
|
||||
|
||||
addJudgementAssert(objects[0], HitResult.IgnoreHit);
|
||||
addJudgementAssert(objects[1], HitResult.IgnoreHit);
|
||||
addJudgementAssert(objects[0], HitResult.IgnoreMiss);
|
||||
addJudgementAssert(objects[1], HitResult.IgnoreMiss);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -33,5 +33,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
}
|
||||
|
||||
protected override SelectionHandler<HitObject> CreateSelectionHandler() => new ManiaSelectionHandler();
|
||||
|
||||
protected sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
public ManiaFlashlight(ManiaModFlashlight modFlashlight)
|
||||
: base(modFlashlight)
|
||||
{
|
||||
FlashlightSize = new Vector2(DrawWidth, GetSizeFor(0));
|
||||
FlashlightSize = new Vector2(DrawWidth, GetSize());
|
||||
|
||||
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";
|
||||
|
@ -69,6 +69,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
/// </summary>
|
||||
private double? releaseTime;
|
||||
|
||||
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
|
||||
|
||||
public DrawableHoldNote()
|
||||
: this(null)
|
||||
{
|
||||
@ -260,7 +262,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
tick.MissForcefully();
|
||||
}
|
||||
|
||||
ApplyResult(r => r.Type = r.Judgement.MaxResult);
|
||||
ApplyResult(r => r.Type = Tail.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||
endHold();
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
{
|
||||
public class ManiaLegacySkinTransformer : LegacySkinTransformer
|
||||
{
|
||||
public override bool IsProvidingLegacyResources => base.IsProvidingLegacyResources || hasKeyTexture.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Mapping of <see cref="HitResult"/> to their corresponding
|
||||
/// <see cref="LegacyManiaSkinConfigurationLookups"/> value.
|
||||
|
@ -4,14 +4,18 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Edit;
|
||||
@ -33,6 +37,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
[Cached(typeof(IBeatSnapProvider))]
|
||||
private readonly EditorBeatmap editorBeatmap;
|
||||
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
[Cached]
|
||||
private readonly EditorClock editorClock;
|
||||
|
||||
@ -48,6 +55,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
};
|
||||
|
||||
private OsuDistanceSnapGrid grid;
|
||||
private SnappingCursorContainer cursor;
|
||||
|
||||
public TestSceneOsuDistanceSnapGrid()
|
||||
{
|
||||
@ -84,8 +92,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.SlateGray
|
||||
},
|
||||
cursor = new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position },
|
||||
grid = new OsuDistanceSnapGrid(new HitCircle { Position = grid_position }),
|
||||
new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position }
|
||||
};
|
||||
});
|
||||
|
||||
@ -150,6 +158,37 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
assertSnappedDistance(expectedDistance);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestReferenceObjectNotOnSnapGrid()
|
||||
{
|
||||
AddStep("create grid", () =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.SlateGray
|
||||
},
|
||||
cursor = new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position },
|
||||
grid = new OsuDistanceSnapGrid(new HitCircle
|
||||
{
|
||||
Position = grid_position,
|
||||
// This is important. It sets the reference object to a point in time that isn't on the current snap divisor's grid.
|
||||
// We are testing that the grid's display is offset correctly.
|
||||
StartTime = 40,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
AddStep("move mouse to point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2(beat_length, 0) * 2)));
|
||||
|
||||
AddAssert("Ensure cursor is on a grid line", () =>
|
||||
{
|
||||
return grid.ChildrenOfType<CircularProgress>().Any(p => Precision.AlmostEquals(p.ScreenSpaceDrawQuad.TopRight.X, grid.ToScreenSpace(cursor.LastSnappedPosition).X));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLimitedDistance()
|
||||
{
|
||||
@ -162,8 +201,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.SlateGray
|
||||
},
|
||||
cursor = new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position },
|
||||
grid = new OsuDistanceSnapGrid(new HitCircle { Position = grid_position }, new HitCircle { StartTime = 200 }),
|
||||
new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position }
|
||||
};
|
||||
});
|
||||
|
||||
@ -182,6 +221,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public Func<Vector2, Vector2> GetSnapPosition;
|
||||
|
||||
public Vector2 LastSnappedPosition { get; private set; }
|
||||
|
||||
private readonly Drawable cursor;
|
||||
|
||||
private InputManager inputManager;
|
||||
@ -210,7 +251,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
cursor.Position = GetSnapPosition.Invoke(inputManager.CurrentState.Mouse.Position);
|
||||
cursor.Position = LastSnappedPosition = GetSnapPosition.Invoke(inputManager.CurrentState.Mouse.Position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,20 +20,49 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
||||
|
||||
[Test]
|
||||
public void TestGridExclusivity()
|
||||
public void TestGridToggles()
|
||||
{
|
||||
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
|
||||
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||
|
||||
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||
rectangularGridActive(false);
|
||||
|
||||
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
|
||||
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||
|
||||
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||
AddUntilStep("distance snap grid still visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||
rectangularGridActive(true);
|
||||
|
||||
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
|
||||
AddStep("disable distance snap grid", () => InputManager.Key(Key.T));
|
||||
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||
rectangularGridActive(true);
|
||||
|
||||
AddStep("disable rectangular grid", () => InputManager.Key(Key.Y));
|
||||
AddUntilStep("distance snap grid still hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||
rectangularGridActive(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDistanceSnapMomentaryToggle()
|
||||
{
|
||||
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
|
||||
|
||||
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||
AddStep("hold alt", () => InputManager.PressKey(Key.AltLeft));
|
||||
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
|
||||
AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType<OsuDistanceSnapGrid>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGridSnapMomentaryToggle()
|
||||
{
|
||||
rectangularGridActive(false);
|
||||
AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
|
||||
rectangularGridActive(true);
|
||||
AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
|
||||
rectangularGridActive(false);
|
||||
}
|
||||
|
||||
@ -50,8 +79,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
AddAssert("placement blueprint at (0, 0)", () => Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position, new Vector2(0, 0)));
|
||||
else
|
||||
AddAssert("placement blueprint at (1, 1)", () => Precision.AlmostEquals(Editor.ChildrenOfType<HitCirclePlacementBlueprint>().Single().HitObject.Position, new Vector2(1, 1)));
|
||||
|
||||
AddStep("choose selection tool", () => InputManager.Key(Key.Number1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -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 });
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
// 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 TestSceneOsuModFreezeFrame : OsuModTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestFreezeFrame()
|
||||
{
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModFreezeFrame(),
|
||||
PassCondition = () => true,
|
||||
Autoplay = false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -12,6 +13,7 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osuTK;
|
||||
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Legacy;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
@ -68,10 +69,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
AddStep("create slider", () =>
|
||||
{
|
||||
var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo());
|
||||
tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1";
|
||||
|
||||
var provider = Ruleset.Value.CreateInstance().CreateSkinTransformer(tintingSkin, Beatmap.Value.Beatmap);
|
||||
var skin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo());
|
||||
var provider = Ruleset.Value.CreateInstance().CreateSkinTransformer(skin, Beatmap.Value.Beatmap);
|
||||
|
||||
Child = new SkinProvidingContainer(provider)
|
||||
{
|
||||
@ -92,10 +91,10 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
});
|
||||
|
||||
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);
|
||||
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)
|
||||
|
@ -13,8 +13,11 @@ using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@ -44,12 +47,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
new SpinnerCompositionTool()
|
||||
};
|
||||
|
||||
private readonly Bindable<TernaryState> distanceSnapToggle = new Bindable<TernaryState>();
|
||||
private readonly Bindable<TernaryState> rectangularGridSnapToggle = new Bindable<TernaryState>();
|
||||
|
||||
protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
|
||||
{
|
||||
new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }),
|
||||
new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Th })
|
||||
});
|
||||
|
||||
@ -60,6 +61,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
// Give a bit of breathing room around the playfield content.
|
||||
PlayfieldContentContainer.Padding = new MarginPadding(10);
|
||||
|
||||
LayerBelowRuleset.AddRange(new Drawable[]
|
||||
{
|
||||
distanceSnapGridContainer = new Container
|
||||
@ -77,19 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
placementObject = EditorBeatmap.PlacementObject.GetBoundCopy();
|
||||
placementObject.ValueChanged += _ => updateDistanceSnapGrid();
|
||||
distanceSnapToggle.ValueChanged += _ =>
|
||||
{
|
||||
updateDistanceSnapGrid();
|
||||
|
||||
if (distanceSnapToggle.Value == TernaryState.True)
|
||||
rectangularGridSnapToggle.Value = TernaryState.False;
|
||||
};
|
||||
|
||||
rectangularGridSnapToggle.ValueChanged += _ =>
|
||||
{
|
||||
if (rectangularGridSnapToggle.Value == TernaryState.True)
|
||||
distanceSnapToggle.Value = TernaryState.False;
|
||||
};
|
||||
DistanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid();
|
||||
|
||||
// we may be entering the screen with a selection already active
|
||||
updateDistanceSnapGrid();
|
||||
@ -109,6 +101,14 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
private RectangularPositionSnapGrid rectangularPositionSnapGrid;
|
||||
|
||||
protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
|
||||
{
|
||||
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
|
||||
float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position);
|
||||
|
||||
return actualDistance / expectedDistance;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
@ -129,24 +129,46 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
|
||||
{
|
||||
if (snapType.HasFlagFast(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
|
||||
{
|
||||
// In the case of snapping to nearby objects, a time value is not provided.
|
||||
// This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap
|
||||
// this could result in unexpected behaviour when distance snapping is turned on and a user attempts to place an object that is
|
||||
// BOTH on a valid distance snap ring, and also at the same position as a previous object.
|
||||
//
|
||||
// We want to ensure that in this particular case, the time-snapping component of distance snap is still applied.
|
||||
// The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over
|
||||
// the time value if the proposed positions are roughly the same.
|
||||
if (snapType.HasFlagFast(SnapType.Grids) && DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
|
||||
{
|
||||
(Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition));
|
||||
if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1))
|
||||
snapResult.Time = distanceSnappedTime;
|
||||
}
|
||||
|
||||
return snapResult;
|
||||
}
|
||||
|
||||
SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
||||
|
||||
if (snapType.HasFlagFast(SnapType.Grids))
|
||||
{
|
||||
if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
|
||||
if (DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
|
||||
{
|
||||
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
|
||||
return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
||||
|
||||
result.ScreenSpacePosition = distanceSnapGrid.ToScreenSpace(pos);
|
||||
result.Time = time;
|
||||
}
|
||||
|
||||
if (rectangularGridSnapToggle.Value == TernaryState.True)
|
||||
{
|
||||
Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition));
|
||||
return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition));
|
||||
Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(result.ScreenSpacePosition));
|
||||
|
||||
result.ScreenSpacePosition = rectangularPositionSnapGrid.ToScreenSpace(pos);
|
||||
}
|
||||
}
|
||||
|
||||
return base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
|
||||
@ -199,7 +221,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
distanceSnapGridCache.Invalidate();
|
||||
distanceSnapGrid = null;
|
||||
|
||||
if (distanceSnapToggle.Value != TernaryState.True)
|
||||
if (DistanceSnapToggle.Value != TernaryState.True)
|
||||
return;
|
||||
|
||||
switch (BlueprintContainer.CurrentTool)
|
||||
@ -226,6 +248,42 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (e.Repeat)
|
||||
return false;
|
||||
|
||||
handleToggleViaKey(e);
|
||||
return base.OnKeyDown(e);
|
||||
}
|
||||
|
||||
protected override void OnKeyUp(KeyUpEvent e)
|
||||
{
|
||||
handleToggleViaKey(e);
|
||||
base.OnKeyUp(e);
|
||||
}
|
||||
|
||||
protected override bool AdjustDistanceSpacing(GlobalAction action, float amount)
|
||||
{
|
||||
// To allow better visualisation, ensure that the spacing grid is visible before adjusting.
|
||||
DistanceSnapToggle.Value = TernaryState.True;
|
||||
|
||||
return base.AdjustDistanceSpacing(action, amount);
|
||||
}
|
||||
|
||||
private bool gridSnapMomentary;
|
||||
|
||||
private void handleToggleViaKey(KeyboardEvent key)
|
||||
{
|
||||
bool shiftPressed = key.ShiftPressed;
|
||||
|
||||
if (shiftPressed != gridSnapMomentary)
|
||||
{
|
||||
gridSnapMomentary = shiftPressed;
|
||||
rectangularGridSnapToggle.Value = rectangularGridSnapToggle.Value == TernaryState.False ? TernaryState.True : TernaryState.False;
|
||||
}
|
||||
}
|
||||
|
||||
private DistanceSnapGrid createDistanceSnapGrid(IEnumerable<HitObject> selectedHitObjects)
|
||||
{
|
||||
if (BlueprintContainer.CurrentTool is SpinnerCompositionTool)
|
||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) };
|
||||
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModFreezeFrame) };
|
||||
|
||||
[SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 0)]
|
||||
public BindableFloat Scale { get; } = new BindableFloat(4)
|
||||
|
@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
followDelay = modFlashlight.FollowDelay.Value;
|
||||
|
||||
FlashlightSize = new Vector2(0, GetSizeFor(0));
|
||||
FlashlightSize = new Vector2(0, GetSize());
|
||||
FlashlightSmoothness = 1.4f;
|
||||
}
|
||||
|
||||
@ -83,9 +83,9 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
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";
|
||||
|
89
osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs
Normal file
89
osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs
Normal file
@ -0,0 +1,89 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModFreezeFrame : Mod, IApplicableToDrawableHitObject, IApplicableToBeatmap
|
||||
{
|
||||
public override string Name => "Freeze Frame";
|
||||
|
||||
public override string Acronym => "FR";
|
||||
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override LocalisableString Description => "Burn the notes into your memory.";
|
||||
|
||||
//Alters the transforms of the approach circles, breaking the effects of these mods.
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModApproachDifferent) };
|
||||
|
||||
public override ModType Type => ModType.Fun;
|
||||
|
||||
//mod breaks normal approach circle preempt
|
||||
private double originalPreempt;
|
||||
|
||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
var firstHitObject = beatmap.HitObjects.OfType<OsuHitObject>().FirstOrDefault();
|
||||
if (firstHitObject == null)
|
||||
return;
|
||||
|
||||
double lastNewComboTime = 0;
|
||||
|
||||
originalPreempt = firstHitObject.TimePreempt;
|
||||
|
||||
foreach (var obj in beatmap.HitObjects.OfType<OsuHitObject>())
|
||||
{
|
||||
if (obj.NewCombo) { lastNewComboTime = obj.StartTime; }
|
||||
|
||||
applyFadeInAdjustment(obj);
|
||||
}
|
||||
|
||||
void applyFadeInAdjustment(OsuHitObject osuObject)
|
||||
{
|
||||
osuObject.TimePreempt += osuObject.StartTime - lastNewComboTime;
|
||||
|
||||
foreach (var nested in osuObject.NestedHitObjects.OfType<OsuHitObject>())
|
||||
{
|
||||
switch (nested)
|
||||
{
|
||||
//SliderRepeat wont layer correctly if preempt is changed.
|
||||
case SliderRepeat:
|
||||
break;
|
||||
|
||||
default:
|
||||
applyFadeInAdjustment(nested);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyToDrawableHitObject(DrawableHitObject drawableObject)
|
||||
{
|
||||
drawableObject.ApplyCustomUpdateState += (drawableHitObject, _) =>
|
||||
{
|
||||
if (drawableHitObject is not DrawableHitCircle drawableHitCircle) return;
|
||||
|
||||
var hitCircle = drawableHitCircle.HitObject;
|
||||
var approachCircle = drawableHitCircle.ApproachCircle;
|
||||
|
||||
// Reapply scale, ensuring the AR isn't changed due to the new preempt.
|
||||
approachCircle.ClearTransforms(targetMember: nameof(approachCircle.Scale));
|
||||
approachCircle.ScaleTo(4 * (float)(hitCircle.TimePreempt / originalPreempt));
|
||||
|
||||
using (drawableHitCircle.ApproachCircle.BeginAbsoluteSequence(hitCircle.StartTime - hitCircle.TimePreempt))
|
||||
approachCircle.ScaleTo(1, hitCircle.TimePreempt).Then().Expire();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Timing;
|
||||
@ -46,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
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)
|
||||
{
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
@ -9,6 +10,7 @@ using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Utils;
|
||||
|
||||
@ -33,9 +35,15 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Utils;
|
||||
@ -45,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
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)
|
||||
{
|
||||
|
@ -14,12 +14,10 @@ using osu.Game.Audio;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Skinning;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
@ -106,7 +104,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
foreach (var drawableHitObject in NestedHitObjects)
|
||||
drawableHitObject.AccentColour.Value = colour.NewValue;
|
||||
updateBallTint();
|
||||
}, true);
|
||||
|
||||
Tracking.BindValueChanged(updateSlidingSample);
|
||||
@ -257,22 +254,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
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)
|
||||
{
|
||||
if (userTriggered || Time.Current < HitObject.EndTime)
|
||||
@ -331,7 +312,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
base.UpdateHitStateTransforms(state);
|
||||
|
||||
const float fade_out_time = 450;
|
||||
const float fade_out_time = 240;
|
||||
|
||||
switch (state)
|
||||
{
|
||||
@ -341,7 +322,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
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);
|
||||
|
@ -11,28 +11,20 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
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 Func<OsuAction?> GetInitialHitAction;
|
||||
|
||||
public Color4 AccentColour
|
||||
{
|
||||
get => ball.Colour;
|
||||
set => ball.Colour = value;
|
||||
}
|
||||
|
||||
private Drawable followCircleReceptor;
|
||||
private DrawableSlider drawableSlider;
|
||||
private Drawable ball;
|
||||
|
@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
/// </summary>
|
||||
public readonly IBindable<double> SpinsPerMinute = new BindableDouble();
|
||||
|
||||
private const double fade_out_duration = 160;
|
||||
private const double fade_out_duration = 240;
|
||||
|
||||
public DrawableSpinner()
|
||||
: this(null)
|
||||
|
@ -201,7 +201,8 @@ namespace osu.Game.Rulesets.Osu
|
||||
new OsuModMuted(),
|
||||
new OsuModNoScope(),
|
||||
new MultiMod(new OsuModMagnetised(), new OsuModRepel()),
|
||||
new ModAdaptiveSpeed()
|
||||
new ModAdaptiveSpeed(),
|
||||
new OsuModFreezeFrame()
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
|
@ -75,6 +75,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
{
|
||||
default:
|
||||
JudgementText
|
||||
.FadeInFromZero(300, Easing.OutQuint)
|
||||
.ScaleTo(Vector2.One)
|
||||
.ScaleTo(new Vector2(1.2f), 1800, Easing.OutQuint);
|
||||
break;
|
||||
@ -96,7 +97,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
ringExplosion?.PlayAnimation();
|
||||
}
|
||||
|
||||
public Drawable? GetAboveHitObjectsProxiedContent() => null;
|
||||
public Drawable? GetAboveHitObjectsProxiedContent() => JudgementText.CreateProxy();
|
||||
|
||||
private class RingExplosion : CompositeDrawable
|
||||
{
|
||||
|
@ -108,18 +108,23 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
{
|
||||
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);
|
||||
|
||||
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;
|
||||
updateStateTransforms(drawableObject, drawableObject.State.Value);
|
||||
}
|
||||
|
||||
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
|
||||
@ -173,11 +178,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
.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.FlashColour(accentColour.Value, fade_out_time, Easing.OutQuint);
|
||||
|
||||
this.FadeOut(fade_out_time, Easing.OutQuad);
|
||||
break;
|
||||
|
@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
CircleSprite = new KiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) })
|
||||
CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) })
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
Anchor = 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,
|
||||
Origin = Anchor.Centre,
|
||||
@ -134,10 +134,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
switch (state)
|
||||
{
|
||||
case ArmedState.Hit:
|
||||
CircleSprite.FadeOut(legacy_fade_duration, Easing.Out);
|
||||
CircleSprite.FadeOut(legacy_fade_duration);
|
||||
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);
|
||||
|
||||
if (hasNumber)
|
||||
@ -146,11 +146,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
if (legacyVersion >= 2.0m)
|
||||
// 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
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
@ -107,8 +107,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
|
||||
this.FadeOut();
|
||||
|
||||
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2))
|
||||
this.FadeInFromZero(spinner.TimeFadeIn / 2);
|
||||
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn))
|
||||
this.FadeInFromZero(spinner.TimeFadeIn);
|
||||
|
||||
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
|
||||
{
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
@ -21,6 +22,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
[Resolved(canBeNull: true)]
|
||||
private DrawableHitObject? parentObject { get; set; }
|
||||
|
||||
public Color4 BallColour => animationContent.Colour;
|
||||
|
||||
private Sprite layerNd = 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()
|
||||
{
|
||||
base.LoadComplete();
|
||||
@ -69,6 +74,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
parentObject.ApplyCustomUpdateState += updateStateTransforms;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,6 +65,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
spin = new Sprite
|
||||
{
|
||||
Alpha = 0,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = source.GetTexture("spinner-spin"),
|
||||
@ -82,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
},
|
||||
bonusCounter = new LegacySpriteText(LegacyFont.Score)
|
||||
{
|
||||
Alpha = 0f,
|
||||
Alpha = 0,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.Centre,
|
||||
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);
|
||||
}
|
||||
|
||||
using (BeginAbsoluteSequence(d.HitObject.StartTime - d.HitObject.TimeFadeIn / 2))
|
||||
spin.FadeInFromZero(d.HitObject.TimeFadeIn / 2);
|
||||
|
||||
using (BeginAbsoluteSequence(d.HitObject.StartTime))
|
||||
ApproachCircle?.ScaleTo(SPRITE_SCALE * 0.1f, d.HitObject.Duration);
|
||||
|
||||
|
@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
public class OsuLegacySkinTransformer : LegacySkinTransformer
|
||||
{
|
||||
public override bool IsProvidingLegacyResources => base.IsProvidingLegacyResources || hasHitCircle.Value;
|
||||
|
||||
private readonly Lazy<bool> hasHitCircle;
|
||||
|
||||
/// <summary>
|
||||
|
@ -9,7 +9,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
{
|
||||
SliderBorderSize,
|
||||
SliderPathRadius,
|
||||
AllowSliderBallTint,
|
||||
CursorCentre,
|
||||
CursorExpand,
|
||||
CursorRotate,
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
@ -14,6 +15,7 @@ using osu.Framework.Graphics.Rendering.Vertices;
|
||||
using osu.Framework.Graphics.Shaders;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Utils;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -21,8 +23,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
{
|
||||
public abstract class SmokeSegment : Drawable, ITexturedShaderDrawable
|
||||
{
|
||||
private const int max_point_count = 18_000;
|
||||
|
||||
// fade anim values
|
||||
private const double initial_fade_out_duration = 4000;
|
||||
|
||||
@ -84,12 +84,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
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)
|
||||
{
|
||||
lastPosition ??= position;
|
||||
@ -106,33 +100,27 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
Vector2 pointPos = (pointInterval - (totalDistance - delta)) * increment + (Vector2)lastPosition;
|
||||
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;
|
||||
|
||||
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,
|
||||
Time = time,
|
||||
Direction = nextPointDirection(),
|
||||
});
|
||||
SmokePoints.Add(new SmokePoint
|
||||
{
|
||||
Position = pointPos,
|
||||
Time = time,
|
||||
Angle = RNG.NextSingle(0, 2 * MathF.PI),
|
||||
});
|
||||
|
||||
pointPos += increment;
|
||||
pointPos += increment;
|
||||
}
|
||||
}
|
||||
|
||||
Invalidate(Invalidation.DrawNode);
|
||||
}
|
||||
|
||||
lastPosition = position;
|
||||
|
||||
if (SmokePoints.Count >= max_point_count)
|
||||
FinishDrawing(time);
|
||||
}
|
||||
|
||||
public void FinishDrawing(double time)
|
||||
@ -156,7 +144,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
{
|
||||
public Vector2 Position;
|
||||
public double Time;
|
||||
public Vector2 Direction;
|
||||
public float Angle;
|
||||
|
||||
public struct UpperBoundComparer : IComparer<SmokePoint>
|
||||
{
|
||||
@ -170,6 +158,17 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
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
|
||||
@ -185,17 +184,17 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
private float radius;
|
||||
private Vector2 drawSize;
|
||||
private Texture? texture;
|
||||
private int rotationSeed;
|
||||
private int firstVisiblePointIndex;
|
||||
|
||||
// anim calculation vars (color, scale, direction)
|
||||
private double initialFadeOutDurationTrunc;
|
||||
private double firstVisiblePointTime;
|
||||
private double firstVisiblePointTimeAfterSmokeEnded;
|
||||
|
||||
private double initialFadeOutTime;
|
||||
private double reFadeInTime;
|
||||
private double finalFadeOutTime;
|
||||
|
||||
private Random rotationRNG = new Random();
|
||||
|
||||
public SmokeDrawNode(ITexturedShaderDrawable source)
|
||||
: base(source)
|
||||
{
|
||||
@ -205,9 +204,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
{
|
||||
base.ApplyState();
|
||||
|
||||
points.Clear();
|
||||
points.AddRange(Source.SmokePoints);
|
||||
|
||||
radius = Source.radius;
|
||||
drawSize = Source.DrawSize;
|
||||
texture = Source.Texture;
|
||||
@ -216,14 +212,21 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
SmokeEndTime = Source.smokeEndTime;
|
||||
CurrentTime = Source.Clock.CurrentTime;
|
||||
|
||||
rotationRNG = new Random(Source.rotationSeed);
|
||||
rotationSeed = Source.rotationSeed;
|
||||
|
||||
initialFadeOutDurationTrunc = Math.Min(initial_fade_out_duration, SmokeEndTime - SmokeStartTime);
|
||||
firstVisiblePointTime = SmokeEndTime - initialFadeOutDurationTrunc;
|
||||
firstVisiblePointTimeAfterSmokeEnded = SmokeEndTime - initialFadeOutDurationTrunc;
|
||||
|
||||
initialFadeOutTime = CurrentTime;
|
||||
reFadeInTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / re_fade_in_speed);
|
||||
finalFadeOutTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / final_fade_out_speed);
|
||||
initialFadeOutTime = Math.Min(CurrentTime, SmokeEndTime);
|
||||
reFadeInTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTimeAfterSmokeEnded * (1 - 1 / re_fade_in_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)
|
||||
@ -233,7 +236,14 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
if (points.Count == 0)
|
||||
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;
|
||||
RectangleF textureRect = texture.GetTextureRect();
|
||||
|
||||
@ -245,8 +255,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
shader.Bind();
|
||||
texture.Bind();
|
||||
|
||||
foreach (var point in points)
|
||||
drawPointQuad(point, textureRect);
|
||||
for (int i = 0; i < points.Count; i++)
|
||||
drawPointQuad(points[i], textureRect, i + firstVisiblePointIndex);
|
||||
|
||||
shader.Unbind();
|
||||
renderer.PopLocalMatrix();
|
||||
@ -260,30 +270,34 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
{
|
||||
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);
|
||||
color.A = (1 - fraction) * initial_alpha;
|
||||
float fraction = Math.Clamp((float)(timeDoingFinalFadeOut / final_fade_out_duration), 0, 1);
|
||||
fraction = MathF.Pow(fraction, 5);
|
||||
color.A = (1 - fraction) * re_fade_in_alpha;
|
||||
}
|
||||
|
||||
if (color.A > 0)
|
||||
else
|
||||
{
|
||||
double timeDoingReFadeIn = reFadeInTime - point.Time / re_fade_in_speed;
|
||||
double timeDoingFinalFadeOut = finalFadeOutTime - point.Time / final_fade_out_speed;
|
||||
double timeDoingInitialFadeOut = initialFadeOutTime - point.Time;
|
||||
|
||||
if (timeDoingFinalFadeOut > 0)
|
||||
if (timeDoingInitialFadeOut > 0)
|
||||
{
|
||||
float fraction = Math.Clamp((float)(timeDoingFinalFadeOut / final_fade_out_duration), 0, 1);
|
||||
fraction = MathF.Pow(fraction, 5);
|
||||
color.A = (1 - fraction) * re_fade_in_alpha;
|
||||
float fraction = Math.Clamp((float)(timeDoingInitialFadeOut / initial_fade_out_duration), 0, 1);
|
||||
color.A = (1 - fraction) * initial_alpha;
|
||||
}
|
||||
else if (timeDoingReFadeIn > 0)
|
||||
|
||||
if (point.Time > firstVisiblePointTimeAfterSmokeEnded)
|
||||
{
|
||||
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;
|
||||
double timeDoingReFadeIn = reFadeInTime - point.Time / re_fade_in_speed;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
float fraction = Math.Clamp((float)(timeDoingRotation / rotation_duration), 0, 1);
|
||||
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));
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
var colour = PointColour(point);
|
||||
float scale = PointScale(point);
|
||||
var dir = PointDirection(point);
|
||||
var ortho = dir.PerpendicularLeft;
|
||||
|
||||
if (colour.A == 0 || scale == 0)
|
||||
if (colour.A == 0)
|
||||
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 localTopRight = point.Position + (radius * scale * (-ortho + dir));
|
||||
var localBotLeft = point.Position + (radius * scale * (ortho - dir));
|
||||
|
@ -36,6 +36,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
private readonly ProxyContainer spinnerProxies;
|
||||
private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer;
|
||||
|
||||
public SmokeContainer Smoke { get; }
|
||||
public FollowPointRenderer FollowPoints { get; }
|
||||
|
||||
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
|
||||
@ -54,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
|
||||
new SmokeContainer { RelativeSizeAxes = Axes.Both },
|
||||
Smoke = new SmokeContainer { RelativeSizeAxes = Axes.Both },
|
||||
spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both },
|
||||
FollowPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both },
|
||||
judgementLayer = new JudgementContainer<DrawableOsuJudgement> { RelativeSizeAxes = Axes.Both },
|
||||
|
@ -0,0 +1,56 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Rulesets.Taiko.UI;
|
||||
using osuTK;
|
||||
|
||||
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 });
|
||||
|
||||
[Test]
|
||||
public void TestFlashlightAlwaysHasNonZeroSize()
|
||||
{
|
||||
bool failed = false;
|
||||
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new TestTaikoModFlashlight { ComboBasedSize = { Value = true } },
|
||||
Autoplay = false,
|
||||
PassCondition = () =>
|
||||
{
|
||||
failed |= this.ChildrenOfType<TestTaikoModFlashlight.TestTaikoFlashlight>().SingleOrDefault()?.FlashlightSize.Y == 0;
|
||||
return !failed;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private class TestTaikoModFlashlight : TaikoModFlashlight
|
||||
{
|
||||
protected override Flashlight CreateFlashlight() => new TestTaikoFlashlight(this, Playfield);
|
||||
|
||||
public class TestTaikoFlashlight : TaikoFlashlight
|
||||
{
|
||||
public TestTaikoFlashlight(TaikoModFlashlight modFlashlight, TaikoPlayfield taikoPlayfield)
|
||||
: base(modFlashlight, taikoPlayfield)
|
||||
{
|
||||
}
|
||||
|
||||
public new Vector2 FlashlightSize => base.FlashlightSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -25,8 +25,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
|
||||
TimeRange = { Value = 5000 },
|
||||
};
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
[Test]
|
||||
public void DrumrollTest()
|
||||
{
|
||||
AddStep("Drum roll", () => SetContents(_ =>
|
||||
{
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@
|
||||
#nullable disable
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
@ -16,8 +15,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
|
||||
[TestFixture]
|
||||
public class TestSceneDrawableHit : TaikoSkinnableTestScene
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
[Test]
|
||||
public void TestHits()
|
||||
{
|
||||
AddStep("Centre hit", () => SetContents(_ => new DrawableHit(createHitAtCurrentTime())
|
||||
{
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
@ -27,6 +27,12 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
BeginPlacement();
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
switch (e.Button)
|
||||
|
@ -52,6 +52,12 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
|
||||
private double originalStartTime;
|
||||
private Vector2 originalPosition;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
BeginPlacement();
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
if (e.Button != MouseButton.Left)
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
|
@ -27,17 +27,17 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
|
||||
public override float DefaultFlashlightSize => 200;
|
||||
|
||||
protected override Flashlight CreateFlashlight() => new TaikoFlashlight(this, playfield);
|
||||
protected override Flashlight CreateFlashlight() => new TaikoFlashlight(this, Playfield);
|
||||
|
||||
private TaikoPlayfield playfield = null!;
|
||||
protected TaikoPlayfield Playfield { get; private set; } = null!;
|
||||
|
||||
public override void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
|
||||
{
|
||||
playfield = (TaikoPlayfield)drawableRuleset.Playfield;
|
||||
Playfield = (TaikoPlayfield)drawableRuleset.Playfield;
|
||||
base.ApplyToDrawableRuleset(drawableRuleset);
|
||||
}
|
||||
|
||||
private class TaikoFlashlight : Flashlight
|
||||
public class TaikoFlashlight : Flashlight
|
||||
{
|
||||
private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo);
|
||||
private readonly TaikoPlayfield taikoPlayfield;
|
||||
@ -47,21 +47,28 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
this.taikoPlayfield = taikoPlayfield;
|
||||
|
||||
FlashlightSize = getSizeFor(0);
|
||||
FlashlightSize = adjustSizeForPlayfieldAspectRatio(GetSize());
|
||||
FlashlightSmoothness = 1.4f;
|
||||
|
||||
AddLayout(flashlightProperties);
|
||||
}
|
||||
|
||||
private Vector2 getSizeFor(int combo)
|
||||
/// <summary>
|
||||
/// Returns the aspect ratio-adjusted size of the flashlight.
|
||||
/// This ensures that the size of the flashlight remains independent of taiko-specific aspect ratio adjustments.
|
||||
/// </summary>
|
||||
/// <param name="size">
|
||||
/// The size of the flashlight.
|
||||
/// The value provided here should always come from <see cref="ModFlashlight{T}.Flashlight.GetSize"/>.
|
||||
/// </param>
|
||||
private Vector2 adjustSizeForPlayfieldAspectRatio(float size)
|
||||
{
|
||||
// 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), adjustSizeForPlayfieldAspectRatio(size), FLASHLIGHT_FADE_DURATION);
|
||||
}
|
||||
|
||||
protected override string FragmentShader => "CircularFlashlight";
|
||||
@ -75,7 +82,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
FlashlightPosition = ToLocalSpace(taikoPlayfield.HitTarget.ScreenSpaceDrawQuad.Centre);
|
||||
|
||||
ClearTransforms(targetMember: nameof(FlashlightSize));
|
||||
FlashlightSize = getSizeFor(Combo.Value);
|
||||
FlashlightSize = adjustSizeForPlayfieldAspectRatio(GetSize());
|
||||
|
||||
flashlightProperties.Validate();
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
@ -13,6 +14,7 @@ using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Backgrounds;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -32,6 +34,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
|
||||
|
||||
private const double pre_beat_transition_time = 80;
|
||||
|
||||
private const float flash_opacity = 0.3f;
|
||||
|
||||
private Color4 accentColour;
|
||||
|
||||
/// <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)
|
||||
{
|
||||
if (!effectPoint.KiaiMode)
|
||||
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)
|
||||
return;
|
||||
|
||||
|
@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
}
|
||||
|
||||
// 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");
|
||||
if (foregroundLayer != null)
|
||||
|
@ -14,8 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
{
|
||||
public class TaikoLegacySkinTransformer : LegacySkinTransformer
|
||||
{
|
||||
public override bool IsProvidingLegacyResources => base.IsProvidingLegacyResources || hasHitCircle || hasBarLeft;
|
||||
|
||||
private readonly Lazy<bool> hasExplosion;
|
||||
|
||||
private bool hasHitCircle => GetTexture("taikohitcircle") != null;
|
||||
private bool hasBarLeft => GetTexture("taiko-bar-left") != null;
|
||||
|
||||
public TaikoLegacySkinTransformer(ISkin skin)
|
||||
: base(skin)
|
||||
{
|
||||
@ -42,14 +47,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
return null;
|
||||
|
||||
case TaikoSkinComponents.InputDrum:
|
||||
if (GetTexture("taiko-bar-left") != null)
|
||||
if (hasBarLeft)
|
||||
return new LegacyInputDrum();
|
||||
|
||||
return null;
|
||||
|
||||
case TaikoSkinComponents.CentreHit:
|
||||
case TaikoSkinComponents.RimHit:
|
||||
if (GetTexture("taikohitcircle") != null)
|
||||
if (hasHitCircle)
|
||||
return new LegacyHit(taikoComponent.Component);
|
||||
|
||||
return null;
|
||||
|
@ -9,8 +9,10 @@ using osu.Framework.Extensions;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
@ -96,5 +98,41 @@ namespace osu.Game.Tests.Beatmaps
|
||||
var second = beatmaps.GetWorkingBeatmap(beatmap, true);
|
||||
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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -71,9 +71,9 @@ namespace osu.Game.Tests.Editing
|
||||
|
||||
[TestCase(1)]
|
||||
[TestCase(2)]
|
||||
public void TestSpeedMultiplier(float multiplier)
|
||||
public void TestSpeedMultiplierDoesNotChangeDistanceSnap(float multiplier)
|
||||
{
|
||||
assertSnapDistance(100 * multiplier, new HitObject
|
||||
assertSnapDistance(100, new HitObject
|
||||
{
|
||||
DifficultyControlPoint = new DifficultyControlPoint
|
||||
{
|
||||
|
BIN
osu.Game.Tests/Resources/Archives/modified-default-20221012.osk
Normal file
BIN
osu.Game.Tests/Resources/Archives/modified-default-20221012.osk
Normal file
Binary file not shown.
@ -38,7 +38,9 @@ namespace osu.Game.Tests.Skins
|
||||
// Covers legacy song progress, UR counter, colour hit error metre.
|
||||
"Archives/modified-classic-20220801.osk",
|
||||
// Covers clicks/s counter
|
||||
"Archives/modified-default-20220818.osk"
|
||||
"Archives/modified-default-20220818.osk",
|
||||
// Covers longest combo counter
|
||||
"Archives/modified-default-20221012.osk"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
@ -244,7 +244,10 @@ namespace osu.Game.Tests.Visual.Background
|
||||
public void TestResumeFromPlayer()
|
||||
{
|
||||
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());
|
||||
AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
|
||||
AddStep("Move mouse to center of screen", () => InputManager.MoveMouseTo(playerLoader.ScreenPos));
|
||||
|
@ -106,6 +106,49 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
assertBeatSnap(16);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestKeyboardNavigation()
|
||||
{
|
||||
pressKey(1);
|
||||
assertBeatSnap(1);
|
||||
assertPreset(BeatDivisorType.Common);
|
||||
|
||||
pressKey(2);
|
||||
assertBeatSnap(2);
|
||||
assertPreset(BeatDivisorType.Common);
|
||||
|
||||
pressKey(3);
|
||||
assertBeatSnap(3);
|
||||
assertPreset(BeatDivisorType.Triplets);
|
||||
|
||||
pressKey(4);
|
||||
assertBeatSnap(4);
|
||||
assertPreset(BeatDivisorType.Common);
|
||||
|
||||
pressKey(5);
|
||||
assertBeatSnap(5);
|
||||
assertPreset(BeatDivisorType.Custom, 5);
|
||||
|
||||
pressKey(6);
|
||||
assertBeatSnap(6);
|
||||
assertPreset(BeatDivisorType.Triplets);
|
||||
|
||||
pressKey(7);
|
||||
assertBeatSnap(7);
|
||||
assertPreset(BeatDivisorType.Custom, 7);
|
||||
|
||||
pressKey(8);
|
||||
assertBeatSnap(8);
|
||||
assertPreset(BeatDivisorType.Common);
|
||||
|
||||
void pressKey(int key) => AddStep($"press shift+{key}", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ShiftLeft);
|
||||
InputManager.Key(Key.Number0 + key);
|
||||
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBeatPresetNavigation()
|
||||
{
|
||||
|
32
osu.Game.Tests/Visual/Editing/TestSceneEditorBindings.cs
Normal file
32
osu.Game.Tests/Visual/Editing/TestSceneEditorBindings.cs
Normal file
@ -0,0 +1,32 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
/// <summary>
|
||||
/// Test editor hotkeys at a high level to ensure they all work well together.
|
||||
/// </summary>
|
||||
public class TestSceneEditorBindings : EditorTestScene
|
||||
{
|
||||
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
||||
|
||||
[Test]
|
||||
public void TestBeatDivisorChangeHotkeys()
|
||||
{
|
||||
AddStep("hold shift", () => InputManager.PressKey(Key.LShift));
|
||||
|
||||
AddStep("press 4", () => InputManager.Key(Key.Number4));
|
||||
AddAssert("snap updated to 4", () => EditorBeatmap.BeatmapInfo.BeatDivisor, () => Is.EqualTo(4));
|
||||
|
||||
AddStep("press 6", () => InputManager.Key(Key.Number6));
|
||||
AddAssert("snap updated to 6", () => EditorBeatmap.BeatmapInfo.BeatDivisor, () => Is.EqualTo(6));
|
||||
|
||||
AddStep("release shift", () => InputManager.ReleaseKey(Key.LShift));
|
||||
}
|
||||
}
|
||||
}
|
@ -155,6 +155,20 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddUntilStep("composer selection box is visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha > 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClone()
|
||||
{
|
||||
var addedObject = new HitCircle { StartTime = 1000 };
|
||||
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
|
||||
AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
|
||||
|
||||
AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1);
|
||||
AddStep("clone", () => Editor.Clone());
|
||||
AddAssert("is two objects", () => EditorBeatmap.HitObjects.Count == 2);
|
||||
AddStep("clone", () => Editor.Clone());
|
||||
AddAssert("is three objects", () => EditorBeatmap.HitObjects.Count == 3);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCutNothing()
|
||||
{
|
||||
@ -175,5 +189,22 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddStep("paste hitobject", () => Editor.Paste());
|
||||
AddAssert("are no objects", () => EditorBeatmap.HitObjects.Count == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCloneNothing()
|
||||
{
|
||||
// Add arbitrary object and copy to clipboard.
|
||||
// This is tested to ensure that clone doesn't incorrectly read from the clipboard when no selection is made.
|
||||
var addedObject = new HitCircle { StartTime = 1000 };
|
||||
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
|
||||
AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
|
||||
AddStep("copy hitobject", () => Editor.Copy());
|
||||
|
||||
AddStep("deselect all objects", () => EditorBeatmap.SelectedHitObjects.Clear());
|
||||
|
||||
AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1);
|
||||
AddStep("clone", () => Editor.Clone());
|
||||
AddAssert("still one object", () => EditorBeatmap.HitObjects.Count == 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,10 @@
|
||||
#nullable disable
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.Edit.Components.RadioButtons;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
@ -13,6 +15,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[TestFixture]
|
||||
public class TestSceneEditorComposeRadioButtons : OsuTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
public TestSceneEditorComposeRadioButtons()
|
||||
{
|
||||
EditorRadioButtonCollection collection;
|
||||
|
@ -148,10 +148,6 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
});
|
||||
|
||||
AddAssert("no circles placed", () => editorBeatmap.HitObjects.Count == 0);
|
||||
|
||||
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("circle placed", () => editorBeatmap.HitObjects.Count == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -165,10 +161,11 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddStep("hold alt", () => InputManager.PressKey(Key.LAlt));
|
||||
|
||||
AddStep("scroll mouse 5 steps", () => InputManager.ScrollVerticalBy(5));
|
||||
AddAssert("distance spacing increased by 0.5", () => editorBeatmap.BeatmapInfo.DistanceSpacing == originalSpacing + 0.5);
|
||||
|
||||
AddStep("release alt", () => InputManager.ReleaseKey(Key.LAlt));
|
||||
AddStep("release ctrl", () => InputManager.ReleaseKey(Key.LControl));
|
||||
|
||||
AddAssert("distance spacing increased by 0.5", () => editorBeatmap.BeatmapInfo.DistanceSpacing == originalSpacing + 0.5);
|
||||
}
|
||||
|
||||
public class EditorBeatmapContainer : Container
|
||||
|
@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
}
|
||||
}
|
||||
},
|
||||
new MenuCursor()
|
||||
new MenuCursorContainer()
|
||||
};
|
||||
|
||||
scrollContainer.Add(innerBox = new Box
|
||||
|
498
osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs
Normal file
498
osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -214,7 +214,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
private void addControlPoints(IList<MultiplierControlPoint> controlPoints, double sequenceStartTime)
|
||||
{
|
||||
controlPoints.ForEach(point => point.StartTime += sequenceStartTime);
|
||||
controlPoints.ForEach(point => point.Time += sequenceStartTime);
|
||||
|
||||
scrollContainers.ForEach(container =>
|
||||
{
|
||||
@ -224,7 +224,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
foreach (var playfield in playfields)
|
||||
{
|
||||
foreach (var controlPoint in controlPoints)
|
||||
playfield.Add(createDrawablePoint(playfield, controlPoint.StartTime));
|
||||
playfield.Add(createDrawablePoint(playfield, controlPoint.Time));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -97,14 +97,23 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}
|
||||
|
||||
[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());
|
||||
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode.Value == QueueMode.AllPlayers);
|
||||
|
||||
addPlaylistItem(() => API.LocalUser.Value.OnlineID);
|
||||
|
||||
assertDeleteButtonVisibility(0, false);
|
||||
assertDeleteButtonVisibility(0, true);
|
||||
assertDeleteButtonVisibility(1, true);
|
||||
|
||||
AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely());
|
||||
|
@ -25,6 +25,7 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Navigation
|
||||
{
|
||||
@ -79,6 +80,16 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
[Resolved]
|
||||
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]
|
||||
public void TestNullRulesetHandled()
|
||||
{
|
||||
|
257
osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs
Normal file
257
osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs
Normal 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -3,18 +3,17 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Tests.Visual.UserInterface;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Settings
|
||||
{
|
||||
public class TestSceneDirectorySelector : OsuTestScene
|
||||
public class TestSceneDirectorySelector : ThemeComparisonTestScene
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
protected override Drawable CreateContent() => new OsuDirectorySelector
|
||||
{
|
||||
Add(new OsuDirectorySelector { RelativeSizeAxes = Axes.Both });
|
||||
}
|
||||
RelativeSizeAxes = Axes.Both
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -4,23 +4,43 @@
|
||||
#nullable disable
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Tests.Visual.UserInterface;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Settings
|
||||
{
|
||||
public class TestSceneFileSelector : OsuTestScene
|
||||
public class TestSceneFileSelector : ThemeComparisonTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestAllFiles()
|
||||
{
|
||||
AddStep("create", () => Child = new OsuFileSelector { RelativeSizeAxes = Axes.Both });
|
||||
}
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
[Test]
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -862,52 +862,6 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRandomFallbackOnNonMatchingPrevious()
|
||||
{
|
||||
List<BeatmapSetInfo> manySets = new List<BeatmapSetInfo>();
|
||||
|
||||
AddStep("populate maps", () =>
|
||||
{
|
||||
manySets.Clear();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
manySets.Add(TestResources.CreateTestBeatmapSetInfo(3, new[]
|
||||
{
|
||||
// all taiko except for first
|
||||
rulesets.GetRuleset(i > 0 ? 1 : 0)
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
loadBeatmaps(manySets);
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
AddStep("Reset filter", () => carousel.Filter(new FilterCriteria(), false));
|
||||
|
||||
AddStep("select first beatmap", () => carousel.SelectBeatmap(manySets.First().Beatmaps.First()));
|
||||
|
||||
AddStep("Toggle non-matching filter", () =>
|
||||
{
|
||||
carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false);
|
||||
});
|
||||
|
||||
AddAssert("selection lost", () => carousel.SelectedBeatmapInfo == null);
|
||||
|
||||
AddStep("Restore different ruleset filter", () =>
|
||||
{
|
||||
carousel.Filter(new FilterCriteria { Ruleset = rulesets.GetRuleset(1) }, false);
|
||||
eagerSelectedIDs.Add(carousel.SelectedBeatmapSet!.ID);
|
||||
});
|
||||
|
||||
AddAssert("selection changed", () => !carousel.SelectedBeatmapInfo!.Equals(manySets.First().Beatmaps.First()));
|
||||
}
|
||||
|
||||
AddAssert("Selection was random", () => eagerSelectedIDs.Count > 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFilteringByUserStarDifficulty()
|
||||
{
|
||||
@ -955,6 +909,63 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
checkVisibleItemCount(true, 15);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCarouselSelectsNextWhenPreviousIsFiltered()
|
||||
{
|
||||
List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>();
|
||||
|
||||
// 10 sets that go osu! -> taiko -> catch -> osu! -> ...
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var rulesetInfo = rulesets.AvailableRulesets.ElementAt(i % 3);
|
||||
sets.Add(TestResources.CreateTestBeatmapSetInfo(5, new[] { rulesetInfo }));
|
||||
}
|
||||
|
||||
// Sort mode is important to keep the ruleset order
|
||||
loadBeatmaps(sets, () => new FilterCriteria { Sort = SortMode.Title });
|
||||
setSelected(1, 1);
|
||||
|
||||
for (int i = 1; i < 10; i++)
|
||||
{
|
||||
var rulesetInfo = rulesets.AvailableRulesets.ElementAt(i % 3);
|
||||
AddStep($"Set ruleset to {rulesetInfo.ShortName}", () =>
|
||||
{
|
||||
carousel.Filter(new FilterCriteria { Ruleset = rulesetInfo, Sort = SortMode.Title }, false);
|
||||
});
|
||||
waitForSelection(i + 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCarouselSelectsBackwardsWhenDistanceIsShorter()
|
||||
{
|
||||
List<BeatmapSetInfo> sets = new List<BeatmapSetInfo>();
|
||||
|
||||
// 10 sets that go taiko, osu!, osu!, osu!, taiko, osu!, osu!, osu!, ...
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var rulesetInfo = rulesets.AvailableRulesets.ElementAt(i % 4 == 0 ? 1 : 0);
|
||||
sets.Add(TestResources.CreateTestBeatmapSetInfo(5, new[] { rulesetInfo }));
|
||||
}
|
||||
|
||||
// Sort mode is important to keep the ruleset order
|
||||
loadBeatmaps(sets, () => new FilterCriteria { Sort = SortMode.Title });
|
||||
|
||||
for (int i = 2; i < 10; i += 4)
|
||||
{
|
||||
setSelected(i, 1);
|
||||
AddStep("Set ruleset to taiko", () =>
|
||||
{
|
||||
carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1), Sort = SortMode.Title }, false);
|
||||
});
|
||||
waitForSelection(i - 1, 1);
|
||||
AddStep("Remove ruleset filter", () =>
|
||||
{
|
||||
carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null, Func<FilterCriteria> initialCriteria = null, Action<BeatmapCarousel> carouselAdjust = null, int? count = null,
|
||||
bool randomDifficulties = false)
|
||||
{
|
||||
|
@ -6,14 +6,18 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Carousel;
|
||||
using osu.Game.Tests.Online;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
@ -41,17 +45,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("create carousel", () =>
|
||||
{
|
||||
Child = carousel = new BeatmapCarousel
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
BeatmapSets = new List<BeatmapSetInfo>
|
||||
{
|
||||
(testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo()),
|
||||
}
|
||||
};
|
||||
});
|
||||
AddStep("create carousel", () => Child = createCarousel());
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
[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()),
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
|
||||
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));
|
||||
|
||||
testUserCursor();
|
||||
testLocalCursor();
|
||||
testUserCursorOverride();
|
||||
testMultipleLocalCursors();
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(moveOut);
|
||||
|
||||
/// <summary>
|
||||
/// -- Green Box --
|
||||
/// Tests whether hovering in and out of a drawable that provides the user cursor (green)
|
||||
/// results in the correct visibility state for that cursor.
|
||||
/// </summary>
|
||||
private void testUserCursor()
|
||||
[Test]
|
||||
public void TestUserCursor()
|
||||
{
|
||||
AddStep("Move to green area", () => InputManager.MoveMouseTo(cursorBoxes[0]));
|
||||
AddAssert("Check green cursor visible", () => checkVisible(cursorBoxes[0].MenuCursor));
|
||||
AddAssert("Check green cursor at mouse", () => checkAtMouse(cursorBoxes[0].MenuCursor));
|
||||
AddAssert("Check green cursor visible", () => checkVisible(cursorBoxes[0].Cursor));
|
||||
AddAssert("Check green cursor at mouse", () => checkAtMouse(cursorBoxes[0].Cursor));
|
||||
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));
|
||||
}
|
||||
|
||||
@ -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)
|
||||
/// results in the correct visibility and state for that cursor.
|
||||
/// </summary>
|
||||
private void testLocalCursor()
|
||||
[Test]
|
||||
public void TestLocalCursor()
|
||||
{
|
||||
AddStep("Move to purple area", () => InputManager.MoveMouseTo(cursorBoxes[3]));
|
||||
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor));
|
||||
AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].MenuCursor));
|
||||
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
|
||||
AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].Cursor));
|
||||
AddAssert("Check global cursor visible", () => checkVisible(globalCursorDisplay.MenuCursor));
|
||||
AddAssert("Check global cursor at mouse", () => checkAtMouse(globalCursorDisplay.MenuCursor));
|
||||
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));
|
||||
}
|
||||
|
||||
@ -125,47 +126,98 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
/// Tests whether overriding a user cursor (green) with another user cursor (blue)
|
||||
/// results in the correct visibility and states for the cursors.
|
||||
/// </summary>
|
||||
private void testUserCursorOverride()
|
||||
[Test]
|
||||
public void TestUserCursorOverride()
|
||||
{
|
||||
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 green cursor invisible", () => !checkVisible(cursorBoxes[0].MenuCursor));
|
||||
AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].MenuCursor));
|
||||
AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].Cursor));
|
||||
AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].Cursor));
|
||||
AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].Cursor));
|
||||
AddStep("Move out", moveOut);
|
||||
AddAssert("Check blue cursor not visible", () => !checkVisible(cursorBoxes[1].MenuCursor));
|
||||
AddAssert("Check green cursor not visible", () => !checkVisible(cursorBoxes[0].MenuCursor));
|
||||
AddAssert("Check blue cursor not visible", () => !checkVisible(cursorBoxes[1].Cursor));
|
||||
AddAssert("Check green cursor not visible", () => !checkVisible(cursorBoxes[0].Cursor));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -- Yellow-Purple Box Boundary --
|
||||
/// Tests whether multiple local cursors (purple + yellow) may be visible and at the mouse position at the same time.
|
||||
/// </summary>
|
||||
private void testMultipleLocalCursors()
|
||||
[Test]
|
||||
public void TestMultipleLocalCursors()
|
||||
{
|
||||
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 at mouse", () => checkAtMouse(cursorBoxes[3].MenuCursor));
|
||||
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor));
|
||||
AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].MenuCursor));
|
||||
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
|
||||
AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].Cursor));
|
||||
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
|
||||
AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].Cursor));
|
||||
AddStep("Move out", moveOut);
|
||||
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor));
|
||||
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor));
|
||||
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
|
||||
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// -- Yellow-Blue Box Boundary --
|
||||
/// Tests whether a local cursor (yellow) may be displayed along with a user cursor override (blue).
|
||||
/// </summary>
|
||||
private void testUserOverrideWithLocal()
|
||||
[Test]
|
||||
public void TestUserOverrideWithLocal()
|
||||
{
|
||||
AddStep("Move to yellow-blue boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.TopRight - new Vector2(10)));
|
||||
AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].MenuCursor));
|
||||
AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].MenuCursor));
|
||||
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor));
|
||||
AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].MenuCursor));
|
||||
AddStep("Move to yellow-blue boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.TopRight - new Vector2(10, 0)));
|
||||
AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].Cursor));
|
||||
AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].Cursor));
|
||||
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
|
||||
AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].Cursor));
|
||||
AddStep("Move out", moveOut);
|
||||
AddAssert("Check blue cursor invisible", () => !checkVisible(cursorBoxes[1].MenuCursor));
|
||||
AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor));
|
||||
AddAssert("Check blue cursor invisible", () => !checkVisible(cursorBoxes[1].Cursor));
|
||||
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>
|
||||
@ -191,7 +243,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public bool SmoothTransition;
|
||||
|
||||
public CursorContainer MenuCursor { get; }
|
||||
public CursorContainer Cursor { get; }
|
||||
public bool ProvidingUserCursor { get; }
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || (SmoothTransition && !ProvidingUserCursor);
|
||||
@ -218,7 +270,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
Origin = Anchor.Centre,
|
||||
Text = providesUserCursor ? "User cursor" : "Local cursor"
|
||||
},
|
||||
MenuCursor = new TestCursorContainer
|
||||
Cursor = new TestCursorContainer
|
||||
{
|
||||
State = { Value = providesUserCursor ? Visibility.Hidden : Visibility.Visible },
|
||||
}
|
||||
|
@ -5,12 +5,14 @@
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
@ -20,11 +22,17 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
private SettingsToolboxGroup group;
|
||||
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
Child = group = new SettingsToolboxGroup("example")
|
||||
{
|
||||
Scale = new Vector2(3),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new RoundedButton
|
||||
|
@ -6,6 +6,7 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace osu.Game.Tournament
|
||||
@ -31,7 +32,9 @@ namespace osu.Game.Tournament
|
||||
|
||||
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();
|
||||
|
@ -239,17 +239,17 @@ namespace osu.Game.Tournament.Screens.Editors
|
||||
|
||||
var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = Model.ID });
|
||||
|
||||
req.Success += res =>
|
||||
req.Success += res => Schedule(() =>
|
||||
{
|
||||
Model.Beatmap = new TournamentBeatmap(res);
|
||||
updatePanel();
|
||||
};
|
||||
});
|
||||
|
||||
req.Failure += _ =>
|
||||
req.Failure += _ => Schedule(() =>
|
||||
{
|
||||
Model.Beatmap = null;
|
||||
updatePanel();
|
||||
};
|
||||
});
|
||||
|
||||
API.Queue(req);
|
||||
}, true);
|
||||
|
@ -141,18 +141,9 @@ namespace osu.Game.Beatmaps
|
||||
// Handle collections using permissive difficulty name to track difficulties.
|
||||
foreach (var originalBeatmap in original.Beatmaps)
|
||||
{
|
||||
var updatedBeatmap = updated.Beatmaps.FirstOrDefault(b => b.DifficultyName == originalBeatmap.DifficultyName);
|
||||
|
||||
if (updatedBeatmap == null)
|
||||
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);
|
||||
}
|
||||
updated.Beatmaps
|
||||
.FirstOrDefault(b => b.DifficultyName == originalBeatmap.DifficultyName)?
|
||||
.TransferCollectionReferences(realm, originalBeatmap.MD5Hash);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ using JetBrains.Annotations;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
@ -213,6 +214,23 @@ namespace osu.Game.Beatmaps
|
||||
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;
|
||||
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
|
||||
IRulesetInfo IBeatmapInfo.Ruleset => Ruleset;
|
||||
|
@ -311,6 +311,8 @@ namespace osu.Game.Beatmaps
|
||||
if (existingFileInfo != null)
|
||||
DeleteFile(setInfo, existingFileInfo);
|
||||
|
||||
string oldMd5Hash = beatmapInfo.MD5Hash;
|
||||
|
||||
beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
|
||||
beatmapInfo.Hash = stream.ComputeSHA2Hash();
|
||||
|
||||
@ -327,6 +329,8 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
setInfo.CopyChangesToRealm(liveBeatmapSet);
|
||||
|
||||
beatmapInfo.TransferCollectionReferences(r, oldMd5Hash);
|
||||
|
||||
ProcessBeatmap?.Invoke((liveBeatmapSet, false));
|
||||
});
|
||||
}
|
||||
|
@ -9,11 +9,8 @@ using osuTK.Graphics;
|
||||
|
||||
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]
|
||||
public double Time { get; set; }
|
||||
|
||||
|
@ -196,8 +196,8 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// <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>
|
||||
/// <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)
|
||||
where T : ControlPoint
|
||||
public static T BinarySearchWithFallback<T>(IReadOnlyList<T> list, double time, T fallback)
|
||||
where T : class, IControlPoint
|
||||
{
|
||||
return BinarySearch(list, time) ?? fallback;
|
||||
}
|
||||
@ -207,9 +207,9 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// </summary>
|
||||
/// <param name="list">The list to search.</param>
|
||||
/// <param name="time">The time to find the control point at.</param>
|
||||
/// <returns>The active control point at <paramref name="time"/>.</returns>
|
||||
protected virtual T BinarySearch<T>(IReadOnlyList<T> list, double time)
|
||||
where T : ControlPoint
|
||||
/// <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>
|
||||
public static T BinarySearch<T>(IReadOnlyList<T> list, double time)
|
||||
where T : class, IControlPoint
|
||||
{
|
||||
if (list == null)
|
||||
throw new ArgumentNullException(nameof(list));
|
||||
|
13
osu.Game/Beatmaps/ControlPoints/IControlPoint.cs
Normal file
13
osu.Game/Beatmaps/ControlPoints/IControlPoint.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -355,6 +355,14 @@ namespace osu.Game.Beatmaps.Formats
|
||||
|
||||
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:
|
||||
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]);
|
||||
break;
|
||||
@ -427,8 +435,10 @@ namespace osu.Game.Beatmaps.Formats
|
||||
addControlPoint(time, controlPoint, true);
|
||||
}
|
||||
|
||||
int onlineRulesetID = beatmap.BeatmapInfo.Ruleset.OnlineID;
|
||||
|
||||
#pragma warning disable 618
|
||||
addControlPoint(time, new LegacyDifficultyControlPoint(beatLength)
|
||||
addControlPoint(time, new LegacyDifficultyControlPoint(onlineRulesetID, beatLength)
|
||||
#pragma warning restore 618
|
||||
{
|
||||
SliderVelocity = speedMultiplier,
|
||||
|
@ -174,11 +174,15 @@ namespace osu.Game.Beatmaps.Formats
|
||||
/// </summary>
|
||||
public bool GenerateTicks { get; private set; } = true;
|
||||
|
||||
public LegacyDifficultyControlPoint(double beatLength)
|
||||
public LegacyDifficultyControlPoint(int rulesetId, double beatLength)
|
||||
: this()
|
||||
{
|
||||
// Note: In stable, the division occurs on floats, but with compiler optimisations turned on actually seems to occur on doubles via some .NET black magic (possibly inlining?).
|
||||
BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100.0 : 1;
|
||||
if (rulesetId == 1 || rulesetId == 3)
|
||||
BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100.0 : 1;
|
||||
else
|
||||
BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 1000) / 100.0 : 1;
|
||||
|
||||
GenerateTicks = !double.IsNaN(beatLength);
|
||||
}
|
||||
|
||||
|
@ -134,6 +134,6 @@ namespace osu.Game.Beatmaps
|
||||
/// <summary>
|
||||
/// Reads the correct track restart point from beatmap metadata and sets looping to enabled.
|
||||
/// </summary>
|
||||
void PrepareTrackForPreview(bool looping);
|
||||
void PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint = 0);
|
||||
}
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public Track LoadTrack() => track = GetBeatmapTrack() ?? GetVirtualTrack(1000);
|
||||
|
||||
public void PrepareTrackForPreview(bool looping)
|
||||
public void PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint = 0)
|
||||
{
|
||||
Track.Looping = looping;
|
||||
Track.RestartPoint = Metadata.PreviewTime;
|
||||
@ -125,6 +125,8 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
Track.RestartPoint = 0.4f * Track.Length;
|
||||
}
|
||||
|
||||
Track.RestartPoint += offsetFromPreviewPoint;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -294,15 +294,38 @@ namespace osu.Game.Database
|
||||
// Log output here will be missing a valid hash in non-batch imports.
|
||||
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.
|
||||
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.
|
||||
Populate(item, archive, realm, cancellationToken);
|
||||
|
||||
@ -425,16 +448,6 @@ namespace osu.Game.Database
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -40,8 +40,8 @@ namespace osu.Game.Database
|
||||
/// </summary>
|
||||
/// <param name="data">The file data stream.</param>
|
||||
/// <param name="realm">The realm instance to add to. Should already be in a transaction.</param>
|
||||
/// <returns></returns>
|
||||
public RealmFile Add(Stream data, Realm realm)
|
||||
/// <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, bool addToRealm = true)
|
||||
{
|
||||
string hash = data.ComputeSHA2Hash();
|
||||
|
||||
@ -52,7 +52,7 @@ namespace osu.Game.Database
|
||||
if (!checkFileExistsAndMatchesHash(file))
|
||||
copyToStore(file, data);
|
||||
|
||||
if (!file.IsManaged)
|
||||
if (addToRealm && !file.IsManaged)
|
||||
realm.Add(file);
|
||||
|
||||
return file;
|
||||
|
@ -13,7 +13,7 @@ using osu.Game.Configuration;
|
||||
namespace osu.Game.Graphics.Cursor
|
||||
{
|
||||
/// <summary>
|
||||
/// A container which provides the main <see cref="Cursor.MenuCursor"/>.
|
||||
/// A container which provides the main <see cref="MenuCursorContainer"/>.
|
||||
/// Also handles cases where a more localised cursor is provided by another component (via <see cref="IProvideCursor"/>).
|
||||
/// </summary>
|
||||
public class GlobalCursorDisplay : Container, IProvideCursor
|
||||
@ -23,7 +23,9 @@ namespace osu.Game.Graphics.Cursor
|
||||
/// </summary>
|
||||
internal bool ShowCursor = true;
|
||||
|
||||
public CursorContainer MenuCursor { get; }
|
||||
CursorContainer IProvideCursor.Cursor => MenuCursor;
|
||||
|
||||
public MenuCursorContainer MenuCursor { get; }
|
||||
|
||||
public bool ProvidingUserCursor => true;
|
||||
|
||||
@ -42,8 +44,8 @@ namespace osu.Game.Graphics.Cursor
|
||||
{
|
||||
AddRangeInternal(new Drawable[]
|
||||
{
|
||||
MenuCursor = new MenuCursor { State = { Value = Visibility.Hidden } },
|
||||
Content = new Container { RelativeSizeAxes = Axes.Both }
|
||||
Content = new Container { RelativeSizeAxes = Axes.Both },
|
||||
MenuCursor = new MenuCursorContainer { State = { Value = Visibility.Hidden } }
|
||||
});
|
||||
}
|
||||
|
||||
@ -64,7 +66,7 @@ namespace osu.Game.Graphics.Cursor
|
||||
|
||||
if (!hasValidInput || !ShowCursor)
|
||||
{
|
||||
currentOverrideProvider?.MenuCursor?.Hide();
|
||||
currentOverrideProvider?.Cursor?.Hide();
|
||||
currentOverrideProvider = null;
|
||||
return;
|
||||
}
|
||||
@ -83,8 +85,8 @@ namespace osu.Game.Graphics.Cursor
|
||||
if (currentOverrideProvider == newOverrideProvider)
|
||||
return;
|
||||
|
||||
currentOverrideProvider?.MenuCursor?.Hide();
|
||||
newOverrideProvider.MenuCursor?.Show();
|
||||
currentOverrideProvider?.Cursor?.Hide();
|
||||
newOverrideProvider.Cursor?.Show();
|
||||
|
||||
currentOverrideProvider = newOverrideProvider;
|
||||
}
|
||||
|
@ -17,10 +17,10 @@ namespace osu.Game.Graphics.Cursor
|
||||
/// The cursor provided by this <see cref="IDrawable"/>.
|
||||
/// May be null if no cursor should be visible.
|
||||
/// </summary>
|
||||
CursorContainer MenuCursor { get; }
|
||||
CursorContainer Cursor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether <see cref="MenuCursor"/> should be displayed as the singular user cursor. This will temporarily hide any other user cursor.
|
||||
/// Whether <see cref="Cursor"/> should be displayed as the singular user cursor. This will temporarily hide any other user cursor.
|
||||
/// This value is checked every frame and may be used to control whether multiple cursors are displayed (e.g. watching replays).
|
||||
/// </summary>
|
||||
bool ProvidingUserCursor { get; }
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user