1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 18:07:23 +08:00

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

View File

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

View File

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

View File

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

View File

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

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 sealed override DragBox CreateDragBox() => new ScrollingDragBox(Composer.Playfield);
}
}

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -65,7 +65,7 @@ namespace osu.Game.Online.Rooms
[CanBeNull]
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);
if (ruleset == null)
@ -90,6 +90,8 @@ namespace osu.Game.Online.Rooms
Position = Position,
};
scoreManager.PopulateMaximumStatistics(scoreInfo);
return scoreInfo;
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics;
using osu.Game.Graphics;
@ -22,7 +20,11 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Extensions.IEnumerableExtensions;
using System.Collections.Specialized;
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.Dialog;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Comments
@ -31,7 +33,7 @@ namespace osu.Game.Overlays.Comments
{
private const int avatar_size = 40;
public Action<DrawableComment, int> RepliesRequested;
public Action<DrawableComment, int> RepliesRequested = null!;
public readonly Comment Comment;
@ -45,13 +47,29 @@ namespace osu.Game.Overlays.Comments
private int currentPage;
private FillFlowContainer childCommentsVisibilityContainer;
private FillFlowContainer childCommentsContainer;
private LoadRepliesButton loadRepliesButton;
private ShowMoreRepliesButton showMoreButton;
private ShowRepliesButton showRepliesButton;
private ChevronButton chevronButton;
private DeletedCommentsCounter deletedCommentsCounter;
/// <summary>
/// Local field for tracking comment state. Initialized from Comment.IsDeleted, may change when deleting was requested by user.
/// </summary>
public bool WasDeleted { get; protected set; }
private FillFlowContainer childCommentsVisibilityContainer = null!;
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)
{
@ -64,8 +82,6 @@ namespace osu.Game.Overlays.Comments
LinkFlowContainer username;
FillFlowContainer info;
CommentMarkdownContainer message;
GridContainer content;
VotePill votePill;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
@ -148,9 +164,9 @@ namespace osu.Game.Overlays.Comments
},
Comment.Pinned ? new PinnedCommentNotice() : Empty(),
new ParentUsername(Comment),
new OsuSpriteText
deletedLabel = new OsuSpriteText
{
Alpha = Comment.IsDeleted ? 1 : 0,
Alpha = 0f,
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold),
Text = CommentsStrings.Deleted
}
@ -163,16 +179,37 @@ namespace osu.Game.Overlays.Comments
DocumentMargin = new MarginPadding(0),
DocumentPadding = new MarginPadding(0),
},
info = new FillFlowContainer
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
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)
username.AddUserLink(Comment.User);
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 colour = colourProvider.Foreground1;
@ -282,10 +319,13 @@ namespace osu.Game.Overlays.Comments
if (Comment.HasMessage)
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));
votePill.Hide();
actionsContainer.AddLink("Delete", deleteComment);
}
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()
{
ShowDeleted.BindValueChanged(show =>
{
if (Comment.IsDeleted)
if (WasDeleted)
this.FadeTo(show.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();
private readonly Comment parentComment;
private readonly Comment? parentComment;
public ParentUsername(Comment comment)
{
@ -445,7 +531,7 @@ namespace osu.Game.Overlays.Comments
new OsuSpriteText
{
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.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using osu.Framework;
@ -29,37 +28,41 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
{
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 IBindableList<WindowMode> windowModes = new BindableList<WindowMode>();
private Bindable<ScalingMode> scalingMode;
private Bindable<Size> sizeFullscreen;
private Bindable<ScalingMode> scalingMode = null!;
private Bindable<Size> sizeFullscreen = null!;
private readonly BindableList<Size> resolutions = new BindableList<Size>(new[] { new Size(9999, 9999) });
private readonly IBindable<FullscreenCapability> fullscreenCapability = new Bindable<FullscreenCapability>(FullscreenCapability.Capable);
[Resolved]
private OsuGameBase game { get; set; }
private OsuGameBase game { get; set; } = null!;
[Resolved]
private GameHost host { get; set; }
private GameHost host { get; set; } = null!;
private SettingsDropdown<Size> resolutionDropdown;
private SettingsDropdown<Display> displayDropdown;
private SettingsDropdown<WindowMode> windowModeDropdown;
private IWindow? window;
private Bindable<float> scalingPositionX;
private Bindable<float> scalingPositionY;
private Bindable<float> scalingSizeX;
private Bindable<float> scalingSizeY;
private SettingsDropdown<Size> resolutionDropdown = null!;
private SettingsDropdown<Display> displayDropdown = null!;
private SettingsDropdown<WindowMode> windowModeDropdown = null!;
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;
[BackgroundDependencyLoader]
private void load(FrameworkConfigManager config, OsuConfigManager osuConfig, GameHost host)
{
window = host.Window;
scalingMode = osuConfig.GetBindable<ScalingMode>(OsuSetting.Scaling);
sizeFullscreen = config.GetBindable<Size>(FrameworkSetting.SizeFullscreen);
scalingSizeX = osuConfig.GetBindable<float>(OsuSetting.ScalingSizeX);
@ -67,10 +70,11 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
scalingPositionX = osuConfig.GetBindable<float>(OsuSetting.ScalingPositionX);
scalingPositionY = osuConfig.GetBindable<float>(OsuSetting.ScalingPositionY);
if (host.Window != null)
if (window != null)
{
currentDisplay.BindTo(host.Window.CurrentDisplayBindable);
windowModes.BindTo(host.Window.SupportedWindowModes);
currentDisplay.BindTo(window.CurrentDisplayBindable);
windowModes.BindTo(window.SupportedWindowModes);
window.DisplaysChanged += onDisplaysChanged;
}
if (host.Renderer is IWindowsRenderer windowsRenderer)
@ -87,7 +91,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
displayDropdown = new DisplaySettingsDropdown
{
LabelText = GraphicsSettingsStrings.Display,
Items = host.Window?.Displays,
Items = window?.Displays,
Current = currentDisplay,
},
resolutionDropdown = new ResolutionSettingsDropdown
@ -202,19 +206,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
// initial update bypasses transforms
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()
{
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()
{
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()
{
@ -291,6 +304,14 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
preview.Expire();
}
protected override void Dispose(bool isDisposing)
{
if (window != null)
window.DisplaysChanged -= onDisplaysChanged;
base.Dispose(isDisposing);
}
private class ScalingPreview : ScalingContainer
{
public ScalingPreview()

View File

@ -1,8 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Extensions.EnumExtensions;
@ -23,25 +22,40 @@ namespace osu.Game.Overlays
{
public class SettingsToolboxGroup : Container, IExpandable
{
private readonly string title;
public const int CONTAINER_WIDTH = 270;
private const float transition_duration = 250;
private const int border_thickness = 2;
private const int header_height = 30;
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 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);
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>
/// 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>
public SettingsToolboxGroup(string title)
{
this.title = title;
AutoSizeAxes = Axes.Y;
Width = CONTAINER_WIDTH;
Masking = true;
}
[BackgroundDependencyLoader(true)]
private void load(OverlayColourProvider? colourProvider)
{
CornerRadius = corner_radius;
BorderColour = Color4.Black;
BorderThickness = border_thickness;
InternalChildren = new Drawable[]
{
new Box
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.5f,
Alpha = 0.1f,
Colour = colourProvider?.Background4 ?? Color4.Black,
},
new FillFlowContainer
{
@ -88,7 +107,7 @@ namespace osu.Game.Overlays
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 17),
Padding = new MarginPadding { Left = 10, Right = 30 },
},
new IconButton
expandButton = new IconButton
{
Origin = Anchor.Centre,
Anchor = Anchor.CentreRight,
@ -99,19 +118,7 @@ namespace osu.Game.Overlays
},
}
},
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(15),
Spacing = new Vector2(0, 15),
}
content
}
},
};
@ -175,9 +182,10 @@ namespace osu.Game.Overlays
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.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
@ -52,20 +54,32 @@ namespace osu.Game.Rulesets.Edit
}
[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,
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 },
KeyboardStep = adjust_step,
Colour = colourProvider.Background5,
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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
@ -80,7 +82,7 @@ namespace osu.Game.Rulesets.Edit
dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
[BackgroundDependencyLoader]
private void load()
private void load(OverlayColourProvider colourProvider)
{
Config = Dependencies.Get<IRulesetConfigCache>().GetConfigFor(Ruleset);
@ -116,25 +118,37 @@ namespace osu.Game.Rulesets.Edit
.WithChild(BlueprintContainer = CreateBlueprintContainer())
}
},
new ExpandingToolboxContainer(90, 200)
new Container
{
Padding = new MarginPadding(10),
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
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,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
},
}
new EditorToolboxGroup("toolbox (1-9)")
{
Child = toolboxCollection = new EditorRadioButtonCollection { RelativeSizeAxes = Axes.X }
},
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.
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -12,7 +11,6 @@ using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Beatmaps.Timing;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.OpenGL.Vertices;
@ -20,6 +18,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osuTK;
using osuTK.Graphics;
@ -84,8 +83,6 @@ namespace osu.Game.Rulesets.Mods
flashlight.Combo.BindTo(Combo);
drawableRuleset.KeyBindingInputManager.Add(flashlight);
flashlight.Breaks = drawableRuleset.Beatmap.Breaks;
}
protected abstract Flashlight CreateFlashlight();
@ -100,8 +97,6 @@ namespace osu.Game.Rulesets.Mods
public override bool RemoveCompletedTransforms => false;
public List<BreakPeriod> Breaks = new List<BreakPeriod>();
private readonly float defaultFlashlightSize;
private readonly float sizeMultiplier;
private readonly bool comboBasedSize;
@ -119,37 +114,36 @@ namespace osu.Game.Rulesets.Mods
shader = shaderManager.Load("PositionAndColour", FragmentShader);
}
[Resolved]
private Player? player { get; set; }
private readonly IBindable<bool> isBreakTime = new BindableBool();
protected override void LoadComplete()
{
base.LoadComplete();
Combo.ValueChanged += OnComboChange;
Combo.ValueChanged += _ => UpdateFlashlightSize(GetSize());
using (BeginAbsoluteSequence(0))
if (player != null)
{
foreach (var breakPeriod in Breaks)
{
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);
}
isBreakTime.BindTo(player.IsBreakTime);
isBreakTime.BindValueChanged(_ => UpdateFlashlightSize(GetSize()), true);
}
}
protected abstract void OnComboChange(ValueChangedEvent<int> e);
protected abstract void UpdateFlashlightSize(float size);
protected abstract string FragmentShader { get; }
protected float GetSizeFor(int combo)
protected float GetSize()
{
float size = defaultFlashlightSize * sizeMultiplier;
if (comboBasedSize)
size *= GetComboScaleFor(combo);
if (isBreakTime.Value)
size *= 2.5f;
else if (comboBasedSize)
size *= GetComboScaleFor(Combo.Value);
return size;
}

View File

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

View File

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

View File

@ -123,16 +123,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
},
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
{
@ -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[]
{

View File

@ -15,6 +15,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osuTK;
using osuTK.Input;
@ -106,11 +107,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
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)
{
bool selectionPerformed = performMouseDownActions(e);
@ -174,11 +170,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
finishSelectionMovement();
}
private MouseButtonEvent lastDragEvent;
protected override bool OnDragStart(DragStartEvent e)
{
if (e.Button == MouseButton.Right)
return false;
lastDragEvent = e;
if (movementBlueprints != null)
{
isDraggingBlueprint = true;
@ -193,22 +193,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override void OnDrag(DragEvent e)
{
if (e.Button == MouseButton.Right)
return;
if (DragBox.State == Visibility.Visible)
{
DragBox.HandleDrag(e);
UpdateSelectionFromDragBox();
}
lastDragEvent = e;
moveCurrentSelection(e);
}
protected override void OnDragEnd(DragEndEvent e)
{
if (e.Button == MouseButton.Right)
return;
lastDragEvent = null;
if (isDraggingBlueprint)
{
@ -219,6 +211,18 @@ namespace osu.Game.Screens.Edit.Compose.Components
DragBox.Hide();
}
protected override void Update()
{
base.Update();
if (lastDragEvent != null && DragBox.State == Visibility.Visible)
{
lastDragEvent.Target = this;
DragBox.HandleDrag(lastDragEvent);
UpdateSelectionFromDragBox();
}
}
/// <summary>
/// Called whenever a drag operation completes, before any change transaction is committed.
/// </summary>
@ -389,12 +393,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
foreach (var blueprint in SelectionBlueprints)
{
if (blueprint.IsSelected && !AllowDeselectionDuringDrag)
continue;
switch (blueprint.State)
{
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);
if (blueprint.IsSelected != shouldBeSelected)
blueprint.ToggleSelection();
case SelectionState.NotSelected:
if (blueprint.IsAlive && blueprint.IsPresent && quad.Contains(blueprint.ScreenSpaceSelectionPoint))
blueprint.Select();
break;
}
}
}

View File

@ -12,7 +12,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Graphics.UserInterface;
@ -37,7 +36,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected new EditorSelectionHandler SelectionHandler => (EditorSelectionHandler)base.SelectionHandler;
private PlacementBlueprint currentPlacement;
private InputManager inputManager;
/// <remarks>
/// Positional input must be received outside the container's bounds,
@ -66,8 +64,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
base.LoadComplete();
inputManager = GetContainingInputManager();
Beatmap.HitObjectAdded += hitObjectAdded;
// 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)
{
base.TransferBlueprintFor(hitObject, drawableObject);
@ -222,7 +216,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
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.
snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null);

View File

@ -8,6 +8,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
@ -27,6 +28,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
private HitObjectUsageEventBuffer usageEventBuffer;
protected InputManager InputManager { get; private set; }
protected EditorBlueprintContainer(HitObjectComposer composer)
{
Composer = composer;
@ -42,6 +45,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
base.LoadComplete();
InputManager = GetContainingInputManager();
Beatmap.HitObjectAdded += AddBlueprintFor;
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",
Current = { Value = true },
},
controlPointsCheckbox = new OsuCheckbox
{
LabelText = "Control Points",
Current = { Value = true },
},
ticksCheckbox = new OsuCheckbox
{
LabelText = "Ticks",
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)]
private Timeline timeline { get; set; }
private DragEvent lastDragEvent;
private Bindable<HitObject> placement;
private SelectionBlueprint<HitObject> placementBlueprint;
private bool hitObjectDragged;
/// <remarks>
/// Positional input must be received outside the container's bounds,
/// 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);
}
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()
{
// trigger every frame so drags continue to update selection while playback is scrolling the timeline.
if (lastDragEvent != null)
OnDrag(lastDragEvent);
if (IsDragged || hitObjectDragged)
handleScrollViaDrag();
if (Composer != null && timeline != null)
{
@ -170,7 +157,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
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)
return;
var timelineQuad = timeline.ScreenSpaceDrawQuad;
float mouseX = InputManager.CurrentState.Mouse.Position.X;
if (timeline != null)
{
var timelineQuad = timeline.ScreenSpaceDrawQuad;
float mouseX = e.ScreenSpaceMousePosition.X;
// 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));
}
// 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

View File

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

View File

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

View File

@ -78,13 +78,17 @@ namespace osu.Game.Screens.Menu
if (reverbChannel != null)
intro.LogoVisualisation.AddAmplitudeSource(reverbChannel);
Scheduler.AddDelayed(() =>
{
if (!UsingThemedIntro)
StartTrack();
// this classic intro loops forever.
Scheduler.AddDelayed(() =>
{
if (UsingThemedIntro)
{
StartTrack();
// this classic intro loops forever.
Track.Looping = true;
}
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>
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.
// 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.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
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]
private void load(OsuColour colours, ScoreProcessor scoreProcessor)
{
@ -31,17 +19,12 @@ namespace osu.Game.Screens.Play.HUD
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)
{
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;
/// <summary>
/// Whether the <see cref="HUDOverlay"/> is currently visible.
/// </summary>
public IBindable<bool> ShowingOverlayComponents = new Bindable<bool>();
[Resolved]
private ScoreManager scoreManager { get; set; }
@ -1015,6 +1020,8 @@ namespace osu.Game.Screens.Play
});
HUDOverlay.IsPlaying.BindTo(localUserPlaying);
ShowingOverlayComponents.BindTo(HUDOverlay.ShowHud);
DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime);
DimmableStoryboard.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground);

View File

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

View File

@ -32,9 +32,12 @@ namespace osu.Game.Screens.Select.Carousel
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved(canBeNull: true)]
[Resolved]
private LoginOverlay? loginOverlay { get; set; }
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
public UpdateBeatmapSetButton(BeatmapSetInfo beatmapSetInfo)
{
this.beatmapSetInfo = beatmapSetInfo;
@ -102,17 +105,34 @@ namespace osu.Game.Screens.Select.Carousel
},
});
Action = () =>
{
if (!api.IsLoggedIn)
{
loginOverlay?.Show();
return;
}
Action = updateBeatmap;
}
beatmapDownloader.DownloadAsUpdate(beatmapSetInfo, preferNoVideo.Value);
attachExistingDownload();
};
private bool updateConfirmed;
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()

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;
}
private readonly ISkin skin;
public BeatmapSkinProvidingContainer(ISkin skin)
: base(skin)
{
this.skin = skin;
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@ -84,11 +87,21 @@ namespace osu.Game.Skinning
}
[BackgroundDependencyLoader]
private void load()
private void load(SkinManager skins)
{
beatmapSkins.BindValueChanged(_ => TriggerSourceChanged());
beatmapColours.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)
return new LegacyJudgementPieceNew(resultComponent.Component, createDrawable, particle);
else
return new LegacyJudgementPieceOld(resultComponent.Component, createDrawable);
return new LegacyJudgementPieceOld(resultComponent.Component, createDrawable);
}
return null;
case SkinnableSprite.SpriteComponent sprite:
return this.GetAnimation(sprite.LookupName, false, false);
default:
throw new UnsupportedSkinComponentException(component);
}
return null;
}
private Texture? getParticleTexture(HitResult result)

View File

@ -13,6 +13,11 @@ namespace osu.Game.Skinning
/// </summary>
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)
: base(skin)
{

View File

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