Merge branch 'master' into hide-mouse-on-keyboard-input

This commit is contained in:
Salman Ahmed 2022-10-14 22:26:30 +03:00
commit ba72f13f54
68 changed files with 1299 additions and 397 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;

View File

@ -1,12 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable using osu.Framework.Allocation;
using osu.Framework.Bindables;
using JetBrains.Annotations; using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK.Graphics; using osuTK.Graphics;
@ -19,14 +20,29 @@ namespace osu.Game.Rulesets.Catch.UI
{ {
private int currentCombo; private int currentCombo;
[CanBeNull] public ICatchComboCounter? ComboCounter => Drawable as ICatchComboCounter;
public ICatchComboCounter ComboCounter => Drawable as ICatchComboCounter;
private readonly IBindable<bool> showCombo = new BindableBool(true);
public CatchComboDisplay() public CatchComboDisplay()
: base(new CatchSkinComponent(CatchSkinComponents.CatchComboCounter), _ => Empty()) : base(new CatchSkinComponent(CatchSkinComponents.CatchComboCounter), _ => Empty())
{ {
} }
[Resolved(canBeNull: true)]
private Player? player { get; set; }
protected override void LoadComplete()
{
base.LoadComplete();
if (player != null)
{
showCombo.BindTo(player.ShowingOverlayComponents);
showCombo.BindValueChanged(s => this.FadeTo(s.NewValue ? 1 : 0, HUDOverlay.FADE_DURATION, HUDOverlay.FADE_EASING), true);
}
}
protected override void SkinChanged(ISkinSource skin) protected override void SkinChanged(ISkinSource skin)
{ {
base.SkinChanged(skin); base.SkinChanged(skin);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class PopupDialogStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.PopupDialog";
/// <summary>
/// "Are you sure you want to update this beatmap?"
/// </summary>
public static LocalisableString UpdateLocallyModifiedText => new TranslatableString(getKey(@"update_locally_modified_text"), @"Are you sure you want to update this beatmap?");
/// <summary>
/// "This will discard all local changes you have on that beatmap."
/// </summary>
public static LocalisableString UpdateLocallyModifiedDescription => new TranslatableString(getKey(@"update_locally_modified_description"), @"This will discard all local changes you have on that beatmap.");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -0,0 +1,28 @@
// 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.Net.Http;
using osu.Framework.IO.Network;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests
{
public class CommentDeleteRequest : APIRequest<CommentBundle>
{
public readonly long CommentId;
public CommentDeleteRequest(long id)
{
CommentId = id;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.Method = HttpMethod.Delete;
return req;
}
protected override string Target => $@"comments/{CommentId}";
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using Newtonsoft.Json; using Newtonsoft.Json;
using System; using System;
@ -16,18 +14,18 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"parent_id")] [JsonProperty(@"parent_id")]
public long? ParentId { get; set; } public long? ParentId { get; set; }
public Comment ParentComment { get; set; } public Comment? ParentComment { get; set; }
[JsonProperty(@"user_id")] [JsonProperty(@"user_id")]
public long? UserId { get; set; } public long? UserId { get; set; }
public APIUser User { get; set; } public APIUser? User { get; set; }
[JsonProperty(@"message")] [JsonProperty(@"message")]
public string Message { get; set; } public string Message { get; set; } = null!;
[JsonProperty(@"message_html")] [JsonProperty(@"message_html")]
public string MessageHtml { get; set; } public string? MessageHtml { get; set; }
[JsonProperty(@"replies_count")] [JsonProperty(@"replies_count")]
public int RepliesCount { get; set; } public int RepliesCount { get; set; }
@ -36,13 +34,13 @@ namespace osu.Game.Online.API.Requests.Responses
public int VotesCount { get; set; } public int VotesCount { get; set; }
[JsonProperty(@"commenatble_type")] [JsonProperty(@"commenatble_type")]
public string CommentableType { get; set; } public string CommentableType { get; set; } = null!;
[JsonProperty(@"commentable_id")] [JsonProperty(@"commentable_id")]
public int CommentableId { get; set; } public int CommentableId { get; set; }
[JsonProperty(@"legacy_name")] [JsonProperty(@"legacy_name")]
public string LegacyName { get; set; } public string? LegacyName { get; set; }
[JsonProperty(@"created_at")] [JsonProperty(@"created_at")]
public DateTimeOffset CreatedAt { get; set; } public DateTimeOffset CreatedAt { get; set; }
@ -62,7 +60,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"pinned")] [JsonProperty(@"pinned")]
public bool Pinned { get; set; } public bool Pinned { get; set; }
public APIUser EditedUser { get; set; } public APIUser? EditedUser { get; set; }
public bool IsTopLevel => !ParentId.HasValue; public bool IsTopLevel => !ParentId.HasValue;

View File

@ -65,7 +65,7 @@ namespace osu.Game.Online.Rooms
[CanBeNull] [CanBeNull]
public MultiplayerScoresAround ScoresAround { get; set; } public MultiplayerScoresAround ScoresAround { get; set; }
public ScoreInfo CreateScoreInfo(RulesetStore rulesets, PlaylistItem playlistItem, [NotNull] BeatmapInfo beatmap) public ScoreInfo CreateScoreInfo(ScoreManager scoreManager, RulesetStore rulesets, PlaylistItem playlistItem, [NotNull] BeatmapInfo beatmap)
{ {
var ruleset = rulesets.GetRuleset(playlistItem.RulesetID); var ruleset = rulesets.GetRuleset(playlistItem.RulesetID);
if (ruleset == null) if (ruleset == null)
@ -90,6 +90,8 @@ namespace osu.Game.Online.Rooms
Position = Position, Position = Position,
}; };
scoreManager.PopulateMaximumStatistics(scoreInfo);
return scoreInfo; return scoreInfo;
} }
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -22,7 +20,11 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using System.Collections.Specialized; using System.Collections.Specialized;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Comments.Buttons; using osu.Game.Overlays.Comments.Buttons;
using osu.Game.Overlays.Dialog;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Comments namespace osu.Game.Overlays.Comments
@ -31,7 +33,7 @@ namespace osu.Game.Overlays.Comments
{ {
private const int avatar_size = 40; private const int avatar_size = 40;
public Action<DrawableComment, int> RepliesRequested; public Action<DrawableComment, int> RepliesRequested = null!;
public readonly Comment Comment; public readonly Comment Comment;
@ -45,13 +47,29 @@ namespace osu.Game.Overlays.Comments
private int currentPage; private int currentPage;
private FillFlowContainer childCommentsVisibilityContainer; /// <summary>
private FillFlowContainer childCommentsContainer; /// Local field for tracking comment state. Initialized from Comment.IsDeleted, may change when deleting was requested by user.
private LoadRepliesButton loadRepliesButton; /// </summary>
private ShowMoreRepliesButton showMoreButton; public bool WasDeleted { get; protected set; }
private ShowRepliesButton showRepliesButton;
private ChevronButton chevronButton; private FillFlowContainer childCommentsVisibilityContainer = null!;
private DeletedCommentsCounter deletedCommentsCounter; private FillFlowContainer childCommentsContainer = null!;
private LoadRepliesButton loadRepliesButton = null!;
private ShowMoreRepliesButton showMoreButton = null!;
private ShowRepliesButton showRepliesButton = null!;
private ChevronButton chevronButton = null!;
private LinkFlowContainer actionsContainer = null!;
private LoadingSpinner actionsLoading = null!;
private DeletedCommentsCounter deletedCommentsCounter = null!;
private OsuSpriteText deletedLabel = null!;
private GridContainer content = null!;
private VotePill votePill = null!;
[Resolved(canBeNull: true)]
private IDialogOverlay? dialogOverlay { get; set; }
[Resolved]
private IAPIProvider api { get; set; } = null!;
public DrawableComment(Comment comment) public DrawableComment(Comment comment)
{ {
@ -64,8 +82,6 @@ namespace osu.Game.Overlays.Comments
LinkFlowContainer username; LinkFlowContainer username;
FillFlowContainer info; FillFlowContainer info;
CommentMarkdownContainer message; CommentMarkdownContainer message;
GridContainer content;
VotePill votePill;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
@ -148,9 +164,9 @@ namespace osu.Game.Overlays.Comments
}, },
Comment.Pinned ? new PinnedCommentNotice() : Empty(), Comment.Pinned ? new PinnedCommentNotice() : Empty(),
new ParentUsername(Comment), new ParentUsername(Comment),
new OsuSpriteText deletedLabel = new OsuSpriteText
{ {
Alpha = Comment.IsDeleted ? 1 : 0, Alpha = 0f,
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold), Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold),
Text = CommentsStrings.Deleted Text = CommentsStrings.Deleted
} }
@ -163,16 +179,37 @@ namespace osu.Game.Overlays.Comments
DocumentMargin = new MarginPadding(0), DocumentMargin = new MarginPadding(0),
DocumentPadding = new MarginPadding(0), DocumentPadding = new MarginPadding(0),
}, },
info = new FillFlowContainer new FillFlowContainer
{ {
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0), Spacing = new Vector2(10, 0),
Children = new Drawable[] Children = new Drawable[]
{ {
new DrawableDate(Comment.CreatedAt, 12, false) info = new FillFlowContainer
{ {
Colour = colourProvider.Foreground1 AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
new DrawableDate(Comment.CreatedAt, 12, false)
{
Colour = colourProvider.Foreground1
}
}
},
actionsContainer = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold))
{
Name = @"Actions buttons",
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(10, 0)
},
actionsLoading = new LoadingSpinner
{
Size = new Vector2(12f),
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft
} }
} }
}, },
@ -246,9 +283,9 @@ namespace osu.Game.Overlays.Comments
if (Comment.UserId.HasValue) if (Comment.UserId.HasValue)
username.AddUserLink(Comment.User); username.AddUserLink(Comment.User);
else else
username.AddText(Comment.LegacyName); username.AddText(Comment.LegacyName!);
if (Comment.EditedAt.HasValue) if (Comment.EditedAt.HasValue && Comment.EditedUser != null)
{ {
var font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular); var font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular);
var colour = colourProvider.Foreground1; var colour = colourProvider.Foreground1;
@ -282,10 +319,13 @@ namespace osu.Game.Overlays.Comments
if (Comment.HasMessage) if (Comment.HasMessage)
message.Text = Comment.Message; message.Text = Comment.Message;
if (Comment.IsDeleted) WasDeleted = Comment.IsDeleted;
if (WasDeleted)
makeDeleted();
if (Comment.UserId.HasValue && Comment.UserId.Value == api.LocalUser.Value.Id)
{ {
content.FadeColour(OsuColour.Gray(0.5f)); actionsContainer.AddLink("Delete", deleteComment);
votePill.Hide();
} }
if (Comment.IsTopLevel) if (Comment.IsTopLevel)
@ -317,11 +357,57 @@ namespace osu.Game.Overlays.Comments
}; };
} }
/// <summary>
/// Transforms some comment's components to show it as deleted. Invoked both from loading and deleting.
/// </summary>
private void makeDeleted()
{
deletedLabel.Show();
content.FadeColour(OsuColour.Gray(0.5f));
votePill.Hide();
actionsContainer.Expire();
}
/// <summary>
/// Invokes comment deletion with confirmation.
/// </summary>
private void deleteComment()
{
if (dialogOverlay == null)
deleteCommentRequest();
else
dialogOverlay.Push(new ConfirmDialog("Do you really want to delete your comment?", deleteCommentRequest));
}
/// <summary>
/// Invokes comment deletion directly.
/// </summary>
private void deleteCommentRequest()
{
actionsContainer.Hide();
actionsLoading.Show();
var request = new CommentDeleteRequest(Comment.Id);
request.Success += _ => Schedule(() =>
{
actionsLoading.Hide();
makeDeleted();
WasDeleted = true;
if (!ShowDeleted.Value)
Hide();
});
request.Failure += _ => Schedule(() =>
{
actionsLoading.Hide();
actionsContainer.Show();
});
api.Queue(request);
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
ShowDeleted.BindValueChanged(show => ShowDeleted.BindValueChanged(show =>
{ {
if (Comment.IsDeleted) if (WasDeleted)
this.FadeTo(show.NewValue ? 1 : 0); this.FadeTo(show.NewValue ? 1 : 0);
}, true); }, true);
childrenExpanded.BindValueChanged(expanded => childCommentsVisibilityContainer.FadeTo(expanded.NewValue ? 1 : 0), true); childrenExpanded.BindValueChanged(expanded => childCommentsVisibilityContainer.FadeTo(expanded.NewValue ? 1 : 0), true);
@ -425,7 +511,7 @@ namespace osu.Game.Overlays.Comments
{ {
public LocalisableString TooltipText => getParentMessage(); public LocalisableString TooltipText => getParentMessage();
private readonly Comment parentComment; private readonly Comment? parentComment;
public ParentUsername(Comment comment) public ParentUsername(Comment comment)
{ {
@ -445,7 +531,7 @@ namespace osu.Game.Overlays.Comments
new OsuSpriteText new OsuSpriteText
{ {
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, italics: true), Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, italics: true),
Text = parentComment?.User?.Username ?? parentComment?.LegacyName Text = parentComment?.User?.Username ?? parentComment?.LegacyName!
} }
}; };
} }

View File

@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
using osu.Framework; using osu.Framework;
@ -29,37 +28,41 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
{ {
protected override LocalisableString Header => GraphicsSettingsStrings.LayoutHeader; protected override LocalisableString Header => GraphicsSettingsStrings.LayoutHeader;
private FillFlowContainer<SettingsSlider<float>> scalingSettings; private FillFlowContainer<SettingsSlider<float>> scalingSettings = null!;
private readonly Bindable<Display> currentDisplay = new Bindable<Display>(); private readonly Bindable<Display> currentDisplay = new Bindable<Display>();
private readonly IBindableList<WindowMode> windowModes = new BindableList<WindowMode>(); private readonly IBindableList<WindowMode> windowModes = new BindableList<WindowMode>();
private Bindable<ScalingMode> scalingMode; private Bindable<ScalingMode> scalingMode = null!;
private Bindable<Size> sizeFullscreen; private Bindable<Size> sizeFullscreen = null!;
private readonly BindableList<Size> resolutions = new BindableList<Size>(new[] { new Size(9999, 9999) }); private readonly BindableList<Size> resolutions = new BindableList<Size>(new[] { new Size(9999, 9999) });
private readonly IBindable<FullscreenCapability> fullscreenCapability = new Bindable<FullscreenCapability>(FullscreenCapability.Capable); private readonly IBindable<FullscreenCapability> fullscreenCapability = new Bindable<FullscreenCapability>(FullscreenCapability.Capable);
[Resolved] [Resolved]
private OsuGameBase game { get; set; } private OsuGameBase game { get; set; } = null!;
[Resolved] [Resolved]
private GameHost host { get; set; } private GameHost host { get; set; } = null!;
private SettingsDropdown<Size> resolutionDropdown; private IWindow? window;
private SettingsDropdown<Display> displayDropdown;
private SettingsDropdown<WindowMode> windowModeDropdown;
private Bindable<float> scalingPositionX; private SettingsDropdown<Size> resolutionDropdown = null!;
private Bindable<float> scalingPositionY; private SettingsDropdown<Display> displayDropdown = null!;
private Bindable<float> scalingSizeX; private SettingsDropdown<WindowMode> windowModeDropdown = null!;
private Bindable<float> scalingSizeY;
private Bindable<float> scalingPositionX = null!;
private Bindable<float> scalingPositionY = null!;
private Bindable<float> scalingSizeX = null!;
private Bindable<float> scalingSizeY = null!;
private const int transition_duration = 400; private const int transition_duration = 400;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(FrameworkConfigManager config, OsuConfigManager osuConfig, GameHost host) private void load(FrameworkConfigManager config, OsuConfigManager osuConfig, GameHost host)
{ {
window = host.Window;
scalingMode = osuConfig.GetBindable<ScalingMode>(OsuSetting.Scaling); scalingMode = osuConfig.GetBindable<ScalingMode>(OsuSetting.Scaling);
sizeFullscreen = config.GetBindable<Size>(FrameworkSetting.SizeFullscreen); sizeFullscreen = config.GetBindable<Size>(FrameworkSetting.SizeFullscreen);
scalingSizeX = osuConfig.GetBindable<float>(OsuSetting.ScalingSizeX); scalingSizeX = osuConfig.GetBindable<float>(OsuSetting.ScalingSizeX);
@ -67,10 +70,11 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
scalingPositionX = osuConfig.GetBindable<float>(OsuSetting.ScalingPositionX); scalingPositionX = osuConfig.GetBindable<float>(OsuSetting.ScalingPositionX);
scalingPositionY = osuConfig.GetBindable<float>(OsuSetting.ScalingPositionY); scalingPositionY = osuConfig.GetBindable<float>(OsuSetting.ScalingPositionY);
if (host.Window != null) if (window != null)
{ {
currentDisplay.BindTo(host.Window.CurrentDisplayBindable); currentDisplay.BindTo(window.CurrentDisplayBindable);
windowModes.BindTo(host.Window.SupportedWindowModes); windowModes.BindTo(window.SupportedWindowModes);
window.DisplaysChanged += onDisplaysChanged;
} }
if (host.Renderer is IWindowsRenderer windowsRenderer) if (host.Renderer is IWindowsRenderer windowsRenderer)
@ -87,7 +91,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
displayDropdown = new DisplaySettingsDropdown displayDropdown = new DisplaySettingsDropdown
{ {
LabelText = GraphicsSettingsStrings.Display, LabelText = GraphicsSettingsStrings.Display,
Items = host.Window?.Displays, Items = window?.Displays,
Current = currentDisplay, Current = currentDisplay,
}, },
resolutionDropdown = new ResolutionSettingsDropdown resolutionDropdown = new ResolutionSettingsDropdown
@ -202,19 +206,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
// initial update bypasses transforms // initial update bypasses transforms
updateScalingModeVisibility(); updateScalingModeVisibility();
void updateDisplayModeDropdowns()
{
if (resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen)
resolutionDropdown.Show();
else
resolutionDropdown.Hide();
if (displayDropdown.Items.Count() > 1)
displayDropdown.Show();
else
displayDropdown.Hide();
}
void updateScalingModeVisibility() void updateScalingModeVisibility()
{ {
if (scalingMode.Value == ScalingMode.Off) if (scalingMode.Value == ScalingMode.Off)
@ -225,6 +216,28 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
} }
} }
private void onDisplaysChanged(IEnumerable<Display> displays)
{
Scheduler.AddOnce(d =>
{
displayDropdown.Items = d;
updateDisplayModeDropdowns();
}, displays);
}
private void updateDisplayModeDropdowns()
{
if (resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen)
resolutionDropdown.Show();
else
resolutionDropdown.Hide();
if (displayDropdown.Items.Count() > 1)
displayDropdown.Show();
else
displayDropdown.Hide();
}
private void updateScreenModeWarning() private void updateScreenModeWarning()
{ {
if (RuntimeInfo.OS == RuntimeInfo.Platform.macOS) if (RuntimeInfo.OS == RuntimeInfo.Platform.macOS)
@ -280,7 +293,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
}; };
} }
private Drawable preview; private Drawable? preview;
private void showPreview() private void showPreview()
{ {
@ -291,6 +304,14 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
preview.Expire(); preview.Expire();
} }
protected override void Dispose(bool isDisposing)
{
if (window != null)
window.DisplaysChanged -= onDisplaysChanged;
base.Dispose(isDisposing);
}
private class ScalingPreview : ScalingContainer private class ScalingPreview : ScalingContainer
{ {
public ScalingPreview() public ScalingPreview()

View File

@ -1,8 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Caching; using osu.Framework.Caching;
using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.EnumExtensions;
@ -23,25 +22,40 @@ namespace osu.Game.Overlays
{ {
public class SettingsToolboxGroup : Container, IExpandable public class SettingsToolboxGroup : Container, IExpandable
{ {
private readonly string title;
public const int CONTAINER_WIDTH = 270; public const int CONTAINER_WIDTH = 270;
private const float transition_duration = 250; private const float transition_duration = 250;
private const int border_thickness = 2;
private const int header_height = 30; private const int header_height = 30;
private const int corner_radius = 5; private const int corner_radius = 5;
private const float fade_duration = 800;
private const float inactive_alpha = 0.5f;
private readonly Cached headerTextVisibilityCache = new Cached(); private readonly Cached headerTextVisibilityCache = new Cached();
private readonly FillFlowContainer content; protected override Container<Drawable> Content => content;
private readonly FillFlowContainer content = new FillFlowContainer
{
Name = @"Content",
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X,
AutoSizeDuration = transition_duration,
AutoSizeEasing = Easing.OutQuint,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 10, Top = 5, Bottom = 10 },
Spacing = new Vector2(0, 15),
};
public BindableBool Expanded { get; } = new BindableBool(true); public BindableBool Expanded { get; } = new BindableBool(true);
private readonly OsuSpriteText headerText; private OsuSpriteText headerText = null!;
private readonly Container headerContent; private Container headerContent = null!;
private Box background = null!;
private IconButton expandButton = null!;
/// <summary> /// <summary>
/// Create a new instance. /// Create a new instance.
@ -49,20 +63,25 @@ namespace osu.Game.Overlays
/// <param name="title">The title to be displayed in the header of this group.</param> /// <param name="title">The title to be displayed in the header of this group.</param>
public SettingsToolboxGroup(string title) public SettingsToolboxGroup(string title)
{ {
this.title = title;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
Width = CONTAINER_WIDTH; Width = CONTAINER_WIDTH;
Masking = true; Masking = true;
}
[BackgroundDependencyLoader(true)]
private void load(OverlayColourProvider? colourProvider)
{
CornerRadius = corner_radius; CornerRadius = corner_radius;
BorderColour = Color4.Black;
BorderThickness = border_thickness;
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new Box background = new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4.Black, Alpha = 0.1f,
Alpha = 0.5f, Colour = colourProvider?.Background4 ?? Color4.Black,
}, },
new FillFlowContainer new FillFlowContainer
{ {
@ -88,7 +107,7 @@ namespace osu.Game.Overlays
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 17), Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 17),
Padding = new MarginPadding { Left = 10, Right = 30 }, Padding = new MarginPadding { Left = 10, Right = 30 },
}, },
new IconButton expandButton = new IconButton
{ {
Origin = Anchor.Centre, Origin = Anchor.Centre,
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
@ -99,19 +118,7 @@ namespace osu.Game.Overlays
}, },
} }
}, },
content = new FillFlowContainer content
{
Name = @"Content",
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X,
AutoSizeDuration = transition_duration,
AutoSizeEasing = Easing.OutQuint,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(15),
Spacing = new Vector2(0, 15),
}
} }
}, },
}; };
@ -175,9 +182,10 @@ namespace osu.Game.Overlays
private void updateFadeState() private void updateFadeState()
{ {
this.FadeTo(IsHovered ? 1 : inactive_alpha, fade_duration, Easing.OutQuint); const float fade_duration = 500;
}
protected override Container<Drawable> Content => content; background.FadeTo(IsHovered ? 1 : 0.1f, fade_duration, Easing.OutQuint);
expandButton.FadeTo(IsHovered ? 1 : 0, fade_duration, Easing.OutQuint);
}
} }
} }

View File

@ -8,6 +8,8 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
@ -52,20 +54,32 @@ namespace osu.Game.Rulesets.Edit
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(OverlayColourProvider colourProvider)
{ {
AddInternal(RightSideToolboxContainer = new ExpandingToolboxContainer(130, 250) AddInternal(new Container
{ {
Padding = new MarginPadding(10),
Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1,
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
Child = new EditorToolboxGroup("snapping") RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Children = new Drawable[]
{ {
Child = distanceSpacingSlider = new ExpandableSlider<double, SizeSlider<double>> new Box
{ {
Current = { BindTarget = DistanceSpacingMultiplier }, Colour = colourProvider.Background5,
KeyboardStep = adjust_step, RelativeSizeAxes = Axes.Both,
},
RightSideToolboxContainer = new ExpandingToolboxContainer(130, 250)
{
Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1,
Child = new EditorToolboxGroup("snapping")
{
Child = distanceSpacingSlider = new ExpandableSlider<double, SizeSlider<double>>
{
Current = { BindTarget = DistanceSpacingMultiplier },
KeyboardStep = adjust_step,
}
}
} }
} }
}); });

View File

@ -12,10 +12,12 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -80,7 +82,7 @@ namespace osu.Game.Rulesets.Edit
dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(OverlayColourProvider colourProvider)
{ {
Config = Dependencies.Get<IRulesetConfigCache>().GetConfigFor(Ruleset); Config = Dependencies.Get<IRulesetConfigCache>().GetConfigFor(Ruleset);
@ -116,25 +118,37 @@ namespace osu.Game.Rulesets.Edit
.WithChild(BlueprintContainer = CreateBlueprintContainer()) .WithChild(BlueprintContainer = CreateBlueprintContainer())
} }
}, },
new ExpandingToolboxContainer(90, 200) new Container
{ {
Padding = new MarginPadding(10), RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Children = new Drawable[] Children = new Drawable[]
{ {
new EditorToolboxGroup("toolbox (1-9)") new Box
{ {
Child = toolboxCollection = new EditorRadioButtonCollection { RelativeSizeAxes = Axes.X } Colour = colourProvider.Background5,
RelativeSizeAxes = Axes.Both,
}, },
new EditorToolboxGroup("toggles (Q~P)") new ExpandingToolboxContainer(60, 200)
{ {
Child = togglesCollection = new FillFlowContainer Children = new Drawable[]
{ {
RelativeSizeAxes = Axes.X, new EditorToolboxGroup("toolbox (1-9)")
AutoSizeAxes = Axes.Y, {
Direction = FillDirection.Vertical, Child = toolboxCollection = new EditorRadioButtonCollection { RelativeSizeAxes = Axes.X }
Spacing = new Vector2(0, 5), },
}, new EditorToolboxGroup("toggles (Q~P)")
} {
Child = togglesCollection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
},
}
}
},
} }
}, },
}; };

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -12,7 +11,6 @@ using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Beatmaps.Timing;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.OpenGL.Vertices; using osu.Game.Graphics.OpenGL.Vertices;
@ -20,6 +18,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -84,8 +83,6 @@ namespace osu.Game.Rulesets.Mods
flashlight.Combo.BindTo(Combo); flashlight.Combo.BindTo(Combo);
drawableRuleset.KeyBindingInputManager.Add(flashlight); drawableRuleset.KeyBindingInputManager.Add(flashlight);
flashlight.Breaks = drawableRuleset.Beatmap.Breaks;
} }
protected abstract Flashlight CreateFlashlight(); protected abstract Flashlight CreateFlashlight();
@ -100,8 +97,6 @@ namespace osu.Game.Rulesets.Mods
public override bool RemoveCompletedTransforms => false; public override bool RemoveCompletedTransforms => false;
public List<BreakPeriod> Breaks = new List<BreakPeriod>();
private readonly float defaultFlashlightSize; private readonly float defaultFlashlightSize;
private readonly float sizeMultiplier; private readonly float sizeMultiplier;
private readonly bool comboBasedSize; private readonly bool comboBasedSize;
@ -119,37 +114,36 @@ namespace osu.Game.Rulesets.Mods
shader = shaderManager.Load("PositionAndColour", FragmentShader); shader = shaderManager.Load("PositionAndColour", FragmentShader);
} }
[Resolved]
private Player? player { get; set; }
private readonly IBindable<bool> isBreakTime = new BindableBool();
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
Combo.ValueChanged += OnComboChange; Combo.ValueChanged += _ => UpdateFlashlightSize(GetSize());
using (BeginAbsoluteSequence(0)) if (player != null)
{ {
foreach (var breakPeriod in Breaks) isBreakTime.BindTo(player.IsBreakTime);
{ isBreakTime.BindValueChanged(_ => UpdateFlashlightSize(GetSize()), true);
if (!breakPeriod.HasEffect)
continue;
if (breakPeriod.Duration < FLASHLIGHT_FADE_DURATION * 2) continue;
this.Delay(breakPeriod.StartTime + FLASHLIGHT_FADE_DURATION).FadeOutFromOne(FLASHLIGHT_FADE_DURATION);
this.Delay(breakPeriod.EndTime - FLASHLIGHT_FADE_DURATION).FadeInFromZero(FLASHLIGHT_FADE_DURATION);
}
} }
} }
protected abstract void OnComboChange(ValueChangedEvent<int> e); protected abstract void UpdateFlashlightSize(float size);
protected abstract string FragmentShader { get; } protected abstract string FragmentShader { get; }
protected float GetSizeFor(int combo) protected float GetSize()
{ {
float size = defaultFlashlightSize * sizeMultiplier; float size = defaultFlashlightSize * sizeMultiplier;
if (comboBasedSize) if (isBreakTime.Value)
size *= GetComboScaleFor(combo); size *= 2.5f;
else if (comboBasedSize)
size *= GetComboScaleFor(Combo.Value);
return size; return size;
} }

View File

@ -8,13 +8,12 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -30,9 +29,9 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
public readonly RadioButton Button; public readonly RadioButton Button;
private Color4 defaultBackgroundColour; private Color4 defaultBackgroundColour;
private Color4 defaultBubbleColour; private Color4 defaultIconColour;
private Color4 selectedBackgroundColour; private Color4 selectedBackgroundColour;
private Color4 selectedBubbleColour; private Color4 selectedIconColour;
private Drawable icon; private Drawable icon;
@ -50,20 +49,13 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OverlayColourProvider colourProvider)
{ {
defaultBackgroundColour = colours.Gray3; defaultBackgroundColour = colourProvider.Background3;
defaultBubbleColour = defaultBackgroundColour.Darken(0.5f); selectedBackgroundColour = colourProvider.Background1;
selectedBackgroundColour = colours.BlueDark;
selectedBubbleColour = selectedBackgroundColour.Lighten(0.5f);
Content.EdgeEffect = new EdgeEffectParameters defaultIconColour = defaultBackgroundColour.Darken(0.5f);
{ selectedIconColour = selectedBackgroundColour.Lighten(0.5f);
Type = EdgeEffectType.Shadow,
Radius = 2,
Offset = new Vector2(0, 1),
Colour = Color4.Black.Opacity(0.5f)
};
Add(icon = (Button.CreateIcon?.Invoke() ?? new Circle()).With(b => Add(icon = (Button.CreateIcon?.Invoke() ?? new Circle()).With(b =>
{ {
@ -98,7 +90,7 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
return; return;
BackgroundColour = Button.Selected.Value ? selectedBackgroundColour : defaultBackgroundColour; BackgroundColour = Button.Selected.Value ? selectedBackgroundColour : defaultBackgroundColour;
icon.Colour = Button.Selected.Value ? selectedBubbleColour : defaultBubbleColour; icon.Colour = Button.Selected.Value ? selectedIconColour : defaultIconColour;
} }
protected override SpriteText CreateText() => new OsuSpriteText protected override SpriteText CreateText() => new OsuSpriteText

View File

@ -6,12 +6,11 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -20,9 +19,9 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
internal class DrawableTernaryButton : OsuButton internal class DrawableTernaryButton : OsuButton
{ {
private Color4 defaultBackgroundColour; private Color4 defaultBackgroundColour;
private Color4 defaultBubbleColour; private Color4 defaultIconColour;
private Color4 selectedBackgroundColour; private Color4 selectedBackgroundColour;
private Color4 selectedBubbleColour; private Color4 selectedIconColour;
private Drawable icon; private Drawable icon;
@ -38,20 +37,13 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OverlayColourProvider colourProvider)
{ {
defaultBackgroundColour = colours.Gray3; defaultBackgroundColour = colourProvider.Background3;
defaultBubbleColour = defaultBackgroundColour.Darken(0.5f); selectedBackgroundColour = colourProvider.Background1;
selectedBackgroundColour = colours.BlueDark;
selectedBubbleColour = selectedBackgroundColour.Lighten(0.5f);
Content.EdgeEffect = new EdgeEffectParameters defaultIconColour = defaultBackgroundColour.Darken(0.5f);
{ selectedIconColour = selectedBackgroundColour.Lighten(0.5f);
Type = EdgeEffectType.Shadow,
Radius = 2,
Offset = new Vector2(0, 1),
Colour = Color4.Black.Opacity(0.5f)
};
Add(icon = (Button.CreateIcon?.Invoke() ?? new Circle()).With(b => Add(icon = (Button.CreateIcon?.Invoke() ?? new Circle()).With(b =>
{ {
@ -85,17 +77,17 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
switch (Button.Bindable.Value) switch (Button.Bindable.Value)
{ {
case TernaryState.Indeterminate: case TernaryState.Indeterminate:
icon.Colour = selectedBubbleColour.Darken(0.5f); icon.Colour = selectedIconColour.Darken(0.5f);
BackgroundColour = selectedBackgroundColour.Darken(0.5f); BackgroundColour = selectedBackgroundColour.Darken(0.5f);
break; break;
case TernaryState.False: case TernaryState.False:
icon.Colour = defaultBubbleColour; icon.Colour = defaultIconColour;
BackgroundColour = defaultBackgroundColour; BackgroundColour = defaultBackgroundColour;
break; break;
case TernaryState.True: case TernaryState.True:
icon.Colour = selectedBubbleColour; icon.Colour = selectedIconColour;
BackgroundColour = selectedBackgroundColour; BackgroundColour = selectedBackgroundColour;
break; break;
} }

View File

@ -123,16 +123,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
}, },
new Drawable[] new Drawable[]
{
new TextFlowContainer(s => s.Font = s.Font.With(size: 14))
{
Padding = new MarginPadding { Horizontal = 15 },
Text = "beat snap",
RelativeSizeAxes = Axes.X,
TextAnchor = Anchor.TopCentre
},
},
new Drawable[]
{ {
new Container new Container
{ {
@ -173,6 +163,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
} }
}, },
new Drawable[]
{
new TextFlowContainer(s => s.Font = s.Font.With(size: 14))
{
Padding = new MarginPadding { Horizontal = 15, Vertical = 8 },
Text = "beat snap",
RelativeSizeAxes = Axes.X,
TextAnchor = Anchor.TopCentre,
},
},
}, },
RowDimensions = new[] RowDimensions = new[]
{ {

View File

@ -15,6 +15,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -106,11 +107,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected virtual DragBox CreateDragBox() => new DragBox(); protected virtual DragBox CreateDragBox() => new DragBox();
/// <summary>
/// Whether this component is in a state where items outside a drag selection should be deselected. If false, selection will only be added to.
/// </summary>
protected virtual bool AllowDeselectionDuringDrag => true;
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)
{ {
bool selectionPerformed = performMouseDownActions(e); bool selectionPerformed = performMouseDownActions(e);
@ -174,11 +170,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
finishSelectionMovement(); finishSelectionMovement();
} }
private MouseButtonEvent lastDragEvent;
protected override bool OnDragStart(DragStartEvent e) protected override bool OnDragStart(DragStartEvent e)
{ {
if (e.Button == MouseButton.Right) if (e.Button == MouseButton.Right)
return false; return false;
lastDragEvent = e;
if (movementBlueprints != null) if (movementBlueprints != null)
{ {
isDraggingBlueprint = true; isDraggingBlueprint = true;
@ -193,22 +193,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override void OnDrag(DragEvent e) protected override void OnDrag(DragEvent e)
{ {
if (e.Button == MouseButton.Right) lastDragEvent = e;
return;
if (DragBox.State == Visibility.Visible)
{
DragBox.HandleDrag(e);
UpdateSelectionFromDragBox();
}
moveCurrentSelection(e); moveCurrentSelection(e);
} }
protected override void OnDragEnd(DragEndEvent e) protected override void OnDragEnd(DragEndEvent e)
{ {
if (e.Button == MouseButton.Right) lastDragEvent = null;
return;
if (isDraggingBlueprint) if (isDraggingBlueprint)
{ {
@ -219,6 +211,18 @@ namespace osu.Game.Screens.Edit.Compose.Components
DragBox.Hide(); DragBox.Hide();
} }
protected override void Update()
{
base.Update();
if (lastDragEvent != null && DragBox.State == Visibility.Visible)
{
lastDragEvent.Target = this;
DragBox.HandleDrag(lastDragEvent);
UpdateSelectionFromDragBox();
}
}
/// <summary> /// <summary>
/// Called whenever a drag operation completes, before any change transaction is committed. /// Called whenever a drag operation completes, before any change transaction is committed.
/// </summary> /// </summary>
@ -389,12 +393,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
foreach (var blueprint in SelectionBlueprints) foreach (var blueprint in SelectionBlueprints)
{ {
if (blueprint.IsSelected && !AllowDeselectionDuringDrag) switch (blueprint.State)
continue; {
case SelectionState.Selected:
// Selection is preserved even after blueprint becomes dead.
if (!quad.Contains(blueprint.ScreenSpaceSelectionPoint))
blueprint.Deselect();
break;
bool shouldBeSelected = blueprint.IsAlive && blueprint.IsPresent && quad.Contains(blueprint.ScreenSpaceSelectionPoint); case SelectionState.NotSelected:
if (blueprint.IsSelected != shouldBeSelected) if (blueprint.IsAlive && blueprint.IsPresent && quad.Contains(blueprint.ScreenSpaceSelectionPoint))
blueprint.ToggleSelection(); blueprint.Select();
break;
}
} }
} }

View File

@ -12,7 +12,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -37,7 +36,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected new EditorSelectionHandler SelectionHandler => (EditorSelectionHandler)base.SelectionHandler; protected new EditorSelectionHandler SelectionHandler => (EditorSelectionHandler)base.SelectionHandler;
private PlacementBlueprint currentPlacement; private PlacementBlueprint currentPlacement;
private InputManager inputManager;
/// <remarks> /// <remarks>
/// Positional input must be received outside the container's bounds, /// Positional input must be received outside the container's bounds,
@ -66,8 +64,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
base.LoadComplete(); base.LoadComplete();
inputManager = GetContainingInputManager();
Beatmap.HitObjectAdded += hitObjectAdded; Beatmap.HitObjectAdded += hitObjectAdded;
// updates to selected are handled for us by SelectionHandler. // updates to selected are handled for us by SelectionHandler.
@ -83,8 +79,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
} }
protected override bool AllowDeselectionDuringDrag => !EditorClock.IsRunning;
protected override void TransferBlueprintFor(HitObject hitObject, DrawableHitObject drawableObject) protected override void TransferBlueprintFor(HitObject hitObject, DrawableHitObject drawableObject)
{ {
base.TransferBlueprintFor(hitObject, drawableObject); base.TransferBlueprintFor(hitObject, drawableObject);
@ -222,7 +216,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void updatePlacementPosition() private void updatePlacementPosition()
{ {
var snapResult = Composer.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position); var snapResult = Composer.FindSnappedPositionAndTime(InputManager.CurrentState.Mouse.Position);
// if no time was found from positional snapping, we should still quantize to the beat. // if no time was found from positional snapping, we should still quantize to the beat.
snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null); snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null);

View File

@ -8,6 +8,7 @@ using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -27,6 +28,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
private HitObjectUsageEventBuffer usageEventBuffer; private HitObjectUsageEventBuffer usageEventBuffer;
protected InputManager InputManager { get; private set; }
protected EditorBlueprintContainer(HitObjectComposer composer) protected EditorBlueprintContainer(HitObjectComposer composer)
{ {
Composer = composer; Composer = composer;
@ -42,6 +45,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
base.LoadComplete(); base.LoadComplete();
InputManager = GetContainingInputManager();
Beatmap.HitObjectAdded += AddBlueprintFor; Beatmap.HitObjectAdded += AddBlueprintFor;
Beatmap.HitObjectRemoved += RemoveBlueprintFor; Beatmap.HitObjectRemoved += RemoveBlueprintFor;

View File

@ -0,0 +1,64 @@
// 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.Input.Events;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Screens.Edit.Compose.Components
{
/// <summary>
/// A <see cref="DragBox"/> that scrolls along with the scrolling playfield.
/// </summary>
public class ScrollingDragBox : DragBox
{
public double MinTime { get; private set; }
public double MaxTime { get; private set; }
private double? startTime;
private readonly ScrollingPlayfield playfield;
public ScrollingDragBox(Playfield playfield)
{
this.playfield = playfield as ScrollingPlayfield ?? throw new ArgumentException("Playfield must be of type {nameof(ScrollingPlayfield)} to use this class.", nameof(playfield));
}
public override void HandleDrag(MouseButtonEvent e)
{
base.HandleDrag(e);
startTime ??= playfield.TimeAtScreenSpacePosition(e.ScreenSpaceMouseDownPosition);
double endTime = playfield.TimeAtScreenSpacePosition(e.ScreenSpaceMousePosition);
MinTime = Math.Min(startTime.Value, endTime);
MaxTime = Math.Max(startTime.Value, endTime);
var startPos = ToLocalSpace(playfield.ScreenSpacePositionAtTime(startTime.Value));
var endPos = ToLocalSpace(playfield.ScreenSpacePositionAtTime(endTime));
switch (playfield.ScrollingInfo.Direction.Value)
{
case ScrollingDirection.Up:
case ScrollingDirection.Down:
Box.Y = Math.Min(startPos.Y, endPos.Y);
Box.Height = Math.Max(startPos.Y, endPos.Y) - Box.Y;
break;
case ScrollingDirection.Left:
case ScrollingDirection.Right:
Box.X = Math.Min(startPos.X, endPos.X);
Box.Width = Math.Max(startPos.X, endPos.X) - Box.X;
break;
}
}
public override void Hide()
{
base.Hide();
startTime = null;
}
}
}

View File

@ -78,16 +78,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
LabelText = "Waveform", LabelText = "Waveform",
Current = { Value = true }, Current = { Value = true },
}, },
controlPointsCheckbox = new OsuCheckbox
{
LabelText = "Control Points",
Current = { Value = true },
},
ticksCheckbox = new OsuCheckbox ticksCheckbox = new OsuCheckbox
{ {
LabelText = "Ticks", LabelText = "Ticks",
Current = { Value = true }, Current = { Value = true },
} },
controlPointsCheckbox = new OsuCheckbox
{
LabelText = "BPM",
Current = { Value = true },
},
} }
} }
} }

View File

@ -29,10 +29,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private Timeline timeline { get; set; } private Timeline timeline { get; set; }
private DragEvent lastDragEvent;
private Bindable<HitObject> placement; private Bindable<HitObject> placement;
private SelectionBlueprint<HitObject> placementBlueprint; private SelectionBlueprint<HitObject> placementBlueprint;
private bool hitObjectDragged;
/// <remarks> /// <remarks>
/// Positional input must be received outside the container's bounds, /// Positional input must be received outside the container's bounds,
/// in order to handle timeline blueprints which are stacked offscreen. /// in order to handle timeline blueprints which are stacked offscreen.
@ -98,24 +99,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
return base.OnDragStart(e); return base.OnDragStart(e);
} }
protected override void OnDrag(DragEvent e)
{
handleScrollViaDrag(e);
base.OnDrag(e);
}
protected override void OnDragEnd(DragEndEvent e)
{
base.OnDragEnd(e);
lastDragEvent = null;
}
protected override void Update() protected override void Update()
{ {
// trigger every frame so drags continue to update selection while playback is scrolling the timeline. if (IsDragged || hitObjectDragged)
if (lastDragEvent != null) handleScrollViaDrag();
OnDrag(lastDragEvent);
if (Composer != null && timeline != null) if (Composer != null && timeline != null)
{ {
@ -170,7 +157,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
return new TimelineHitObjectBlueprint(item) return new TimelineHitObjectBlueprint(item)
{ {
OnDragHandled = handleScrollViaDrag, OnDragHandled = e => hitObjectDragged = e != null,
}; };
} }
@ -197,24 +184,18 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
} }
} }
private void handleScrollViaDrag(DragEvent e) private void handleScrollViaDrag()
{ {
lastDragEvent = e; if (timeline == null) return;
if (lastDragEvent == null) var timelineQuad = timeline.ScreenSpaceDrawQuad;
return; float mouseX = InputManager.CurrentState.Mouse.Position.X;
if (timeline != null) // scroll if in a drag and dragging outside visible extents
{ if (mouseX > timelineQuad.TopRight.X)
var timelineQuad = timeline.ScreenSpaceDrawQuad; timeline.ScrollBy((float)((mouseX - timelineQuad.TopRight.X) / 10 * Clock.ElapsedFrameTime));
float mouseX = e.ScreenSpaceMousePosition.X; else if (mouseX < timelineQuad.TopLeft.X)
timeline.ScrollBy((float)((mouseX - timelineQuad.TopLeft.X) / 10 * Clock.ElapsedFrameTime));
// scroll if in a drag and dragging outside visible extents
if (mouseX > timelineQuad.TopRight.X)
timeline.ScrollBy((float)((mouseX - timelineQuad.TopRight.X) / 10 * Clock.ElapsedFrameTime));
else if (mouseX < timelineQuad.TopLeft.X)
timeline.ScrollBy((float)((mouseX - timelineQuad.TopLeft.X) / 10 * Clock.ElapsedFrameTime));
}
} }
private class SelectableAreaBackground : CompositeDrawable private class SelectableAreaBackground : CompositeDrawable

View File

@ -106,6 +106,7 @@ namespace osu.Game.Screens.Edit
Name = "Main content", Name = "Main content",
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Depth = float.MaxValue, Depth = float.MaxValue,
Padding = new MarginPadding(10),
Child = spinner = new LoadingSpinner(true) Child = spinner = new LoadingSpinner(true)
{ {
State = { Value = Visibility.Visible }, State = { Value = Visibility.Visible },

View File

@ -278,11 +278,11 @@ namespace osu.Game.Screens.Menu
if (!UsingThemedIntro) if (!UsingThemedIntro)
{ {
initialBeatmap?.PrepareTrackForPreview(false); initialBeatmap?.PrepareTrackForPreview(false, -2600);
drawableTrack.VolumeTo(0); drawableTrack.VolumeTo(0);
drawableTrack.Restart(); drawableTrack.Restart();
drawableTrack.VolumeTo(1, 2200, Easing.InCubic); drawableTrack.VolumeTo(1, 2600, Easing.InCubic);
} }
else else
{ {

View File

@ -78,13 +78,17 @@ namespace osu.Game.Screens.Menu
if (reverbChannel != null) if (reverbChannel != null)
intro.LogoVisualisation.AddAmplitudeSource(reverbChannel); intro.LogoVisualisation.AddAmplitudeSource(reverbChannel);
Scheduler.AddDelayed(() => if (!UsingThemedIntro)
{
StartTrack(); StartTrack();
// this classic intro loops forever. Scheduler.AddDelayed(() =>
{
if (UsingThemedIntro) if (UsingThemedIntro)
{
StartTrack();
// this classic intro loops forever.
Track.Looping = true; Track.Looping = true;
}
const float fade_in_time = 200; const float fade_in_time = 200;

View File

@ -182,7 +182,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
/// <param name="pivot">An optional pivot around which the scores were retrieved.</param> /// <param name="pivot">An optional pivot around which the scores were retrieved.</param>
private void performSuccessCallback([NotNull] Action<IEnumerable<ScoreInfo>> callback, [NotNull] List<MultiplayerScore> scores, [CanBeNull] MultiplayerScores pivot = null) => Schedule(() => private void performSuccessCallback([NotNull] Action<IEnumerable<ScoreInfo>> callback, [NotNull] List<MultiplayerScore> scores, [CanBeNull] MultiplayerScores pivot = null) => Schedule(() =>
{ {
var scoreInfos = scoreManager.OrderByTotalScore(scores.Select(s => s.CreateScoreInfo(rulesets, playlistItem, Beatmap.Value.BeatmapInfo))).ToArray(); var scoreInfos = scoreManager.OrderByTotalScore(scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, Beatmap.Value.BeatmapInfo))).ToArray();
// Select a score if we don't already have one selected. // Select a score if we don't already have one selected.
// Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll). // Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll).

View File

@ -0,0 +1,24 @@
// 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.Game.Graphics.UserInterface;
using osu.Game.Skinning;
namespace osu.Game.Screens.Play.HUD
{
public abstract class ComboCounter : RollingCounter<int>, ISkinnableDrawable
{
public bool UsesFixedAnchor { get; set; }
protected ComboCounter()
{
Current.Value = DisplayedCount = 0;
}
protected override double GetProportionalDuration(int currentValue, int newValue)
{
return Math.Abs(currentValue - newValue) * RollingDuration * 100.0f;
}
}
}

View File

@ -1,29 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public class DefaultComboCounter : RollingCounter<int>, ISkinnableDrawable public class DefaultComboCounter : ComboCounter
{ {
public bool UsesFixedAnchor { get; set; }
public DefaultComboCounter()
{
Current.Value = DisplayedCount = 0;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours, ScoreProcessor scoreProcessor) private void load(OsuColour colours, ScoreProcessor scoreProcessor)
{ {
@ -31,17 +19,12 @@ namespace osu.Game.Screens.Play.HUD
Current.BindTo(scoreProcessor.Combo); Current.BindTo(scoreProcessor.Combo);
} }
protected override OsuSpriteText CreateSpriteText()
=> base.CreateSpriteText().With(s => s.Font = s.Font.With(size: 20f));
protected override LocalisableString FormatCount(int count) protected override LocalisableString FormatCount(int count)
{ {
return $@"{count}x"; return $@"{count}x";
} }
protected override double GetProportionalDuration(int currentValue, int newValue)
{
return Math.Abs(currentValue - newValue) * RollingDuration * 100.0f;
}
protected override OsuSpriteText CreateSpriteText()
=> base.CreateSpriteText().With(s => s.Font = s.Font.With(size: 20f));
} }
} }

View File

@ -0,0 +1,83 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Screens.Play.HUD
{
public class LongestComboCounter : ComboCounter
{
[BackgroundDependencyLoader]
private void load(OsuColour colours, ScoreProcessor scoreProcessor)
{
Colour = colours.YellowLighter;
Current.BindTo(scoreProcessor.HighestCombo);
}
protected override IHasText CreateText() => new TextComponent();
private class TextComponent : CompositeDrawable, IHasText
{
public LocalisableString Text
{
get => text.Text;
set => text.Text = $"{value}x";
}
private readonly OsuSpriteText text;
public TextComponent()
{
AutoSizeAxes = Axes.Both;
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(2),
Children = new Drawable[]
{
text = new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.Numeric.With(size: 20)
},
new FillFlowContainer
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Font = OsuFont.Numeric.With(size: 8),
Text = @"longest",
},
new OsuSpriteText
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Font = OsuFont.Numeric.With(size: 8),
Text = @"combo",
Padding = new MarginPadding { Bottom = 3f }
}
}
}
}
};
}
}
}
}

View File

@ -94,6 +94,11 @@ namespace osu.Game.Screens.Play
public int RestartCount; public int RestartCount;
/// <summary>
/// Whether the <see cref="HUDOverlay"/> is currently visible.
/// </summary>
public IBindable<bool> ShowingOverlayComponents = new Bindable<bool>();
[Resolved] [Resolved]
private ScoreManager scoreManager { get; set; } private ScoreManager scoreManager { get; set; }
@ -1015,6 +1020,8 @@ namespace osu.Game.Screens.Play
}); });
HUDOverlay.IsPlaying.BindTo(localUserPlaying); HUDOverlay.IsPlaying.BindTo(localUserPlaying);
ShowingOverlayComponents.BindTo(HUDOverlay.ShowHud);
DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime); DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime);
DimmableStoryboard.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); DimmableStoryboard.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground);

View File

@ -71,6 +71,9 @@ namespace osu.Game.Screens.Play
private AudioFilter lowPassFilter = null!; private AudioFilter lowPassFilter = null!;
private AudioFilter highPassFilter = null!; private AudioFilter highPassFilter = null!;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
protected bool BackgroundBrightnessReduction protected bool BackgroundBrightnessReduction
{ {
set set

View File

@ -32,9 +32,12 @@ namespace osu.Game.Screens.Select.Carousel
[Resolved] [Resolved]
private IAPIProvider api { get; set; } = null!; private IAPIProvider api { get; set; } = null!;
[Resolved(canBeNull: true)] [Resolved]
private LoginOverlay? loginOverlay { get; set; } private LoginOverlay? loginOverlay { get; set; }
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
public UpdateBeatmapSetButton(BeatmapSetInfo beatmapSetInfo) public UpdateBeatmapSetButton(BeatmapSetInfo beatmapSetInfo)
{ {
this.beatmapSetInfo = beatmapSetInfo; this.beatmapSetInfo = beatmapSetInfo;
@ -102,17 +105,34 @@ namespace osu.Game.Screens.Select.Carousel
}, },
}); });
Action = () => Action = updateBeatmap;
{ }
if (!api.IsLoggedIn)
{
loginOverlay?.Show();
return;
}
beatmapDownloader.DownloadAsUpdate(beatmapSetInfo, preferNoVideo.Value); private bool updateConfirmed;
attachExistingDownload();
}; private void updateBeatmap()
{
if (!api.IsLoggedIn)
{
loginOverlay?.Show();
return;
}
if (dialogOverlay != null && beatmapSetInfo.Status == BeatmapOnlineStatus.LocallyModified && !updateConfirmed)
{
dialogOverlay.Push(new UpdateLocalConfirmationDialog(() =>
{
updateConfirmed = true;
updateBeatmap();
}));
return;
}
updateConfirmed = false;
beatmapDownloader.DownloadAsUpdate(beatmapSetInfo, preferNoVideo.Value);
attachExistingDownload();
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -0,0 +1,21 @@
// 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.Sprites;
using osu.Game.Overlays.Dialog;
using osu.Game.Localisation;
namespace osu.Game.Screens.Select.Carousel
{
public class UpdateLocalConfirmationDialog : DeleteConfirmationDialog
{
public UpdateLocalConfirmationDialog(Action onConfirm)
{
HeaderText = PopupDialogStrings.UpdateLocallyModifiedText;
BodyText = PopupDialogStrings.UpdateLocallyModifiedDescription;
Icon = FontAwesome.Solid.ExclamationTriangle;
DeleteAction = onConfirm;
}
}
}

View File

@ -67,9 +67,12 @@ namespace osu.Game.Skinning
return sampleInfo is StoryboardSampleInfo || beatmapHitsounds.Value; return sampleInfo is StoryboardSampleInfo || beatmapHitsounds.Value;
} }
private readonly ISkin skin;
public BeatmapSkinProvidingContainer(ISkin skin) public BeatmapSkinProvidingContainer(ISkin skin)
: base(skin) : base(skin)
{ {
this.skin = skin;
} }
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@ -84,11 +87,21 @@ namespace osu.Game.Skinning
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(SkinManager skins)
{ {
beatmapSkins.BindValueChanged(_ => TriggerSourceChanged()); beatmapSkins.BindValueChanged(_ => TriggerSourceChanged());
beatmapColours.BindValueChanged(_ => TriggerSourceChanged()); beatmapColours.BindValueChanged(_ => TriggerSourceChanged());
beatmapHitsounds.BindValueChanged(_ => TriggerSourceChanged()); beatmapHitsounds.BindValueChanged(_ => TriggerSourceChanged());
// If the beatmap skin looks to have skinnable resources, add the default classic skin as a fallback opportunity.
if (skin is LegacySkinTransformer legacySkin && legacySkin.IsProvidingLegacyResources)
{
SetSources(new[]
{
skin,
skins.DefaultClassicSkin
});
}
} }
} }
} }

View File

@ -389,18 +389,17 @@ namespace osu.Game.Skinning
if (particle != null) if (particle != null)
return new LegacyJudgementPieceNew(resultComponent.Component, createDrawable, particle); return new LegacyJudgementPieceNew(resultComponent.Component, createDrawable, particle);
else
return new LegacyJudgementPieceOld(resultComponent.Component, createDrawable); return new LegacyJudgementPieceOld(resultComponent.Component, createDrawable);
} }
return null; return null;
case SkinnableSprite.SpriteComponent sprite: case SkinnableSprite.SpriteComponent sprite:
return this.GetAnimation(sprite.LookupName, false, false); return this.GetAnimation(sprite.LookupName, false, false);
default:
throw new UnsupportedSkinComponentException(component);
} }
return null;
} }
private Texture? getParticleTexture(HitResult result) private Texture? getParticleTexture(HitResult result)

View File

@ -13,6 +13,11 @@ namespace osu.Game.Skinning
/// </summary> /// </summary>
public abstract class LegacySkinTransformer : SkinTransformer public abstract class LegacySkinTransformer : SkinTransformer
{ {
/// <summary>
/// Whether the skin being transformed is able to provide legacy resources for the ruleset.
/// </summary>
public virtual bool IsProvidingLegacyResources => this.HasFont(LegacyFont.Combo);
protected LegacySkinTransformer(ISkin skin) protected LegacySkinTransformer(ISkin skin)
: base(skin) : base(skin)
{ {

View File

@ -41,7 +41,7 @@ namespace osu.Game.Skinning
Ruleset = ruleset; Ruleset = ruleset;
Beatmap = beatmap; Beatmap = beatmap;
InternalChild = new BeatmapSkinProvidingContainer(beatmapSkin is LegacySkin ? GetRulesetTransformedSkin(beatmapSkin) : beatmapSkin) InternalChild = new BeatmapSkinProvidingContainer(GetRulesetTransformedSkin(beatmapSkin))
{ {
Child = Content = new Container Child = Content = new Container
{ {