Merge branch 'master' into gameplay/key-counter-abstraction

This commit is contained in:
Dean Herbert 2023-03-07 16:09:45 +09:00 committed by GitHub
commit 8f6df5ea0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
156 changed files with 2828 additions and 885 deletions

View File

@ -8,9 +8,13 @@
<!-- NullabilityInfoContextSupport is disabled by default for Android --> <!-- NullabilityInfoContextSupport is disabled by default for Android -->
<NullabilityInfoContextSupport>true</NullabilityInfoContextSupport> <NullabilityInfoContextSupport>true</NullabilityInfoContextSupport>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.131.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2023.228.0" />
</ItemGroup>
<ItemGroup>
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged. <!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="mailto" />
</intent>
</queries>
</manifest>

View File

@ -169,7 +169,7 @@ namespace osu.Desktop
case UserActivity.InGame game: case UserActivity.InGame game:
return game.BeatmapInfo; return game.BeatmapInfo;
case UserActivity.Editing edit: case UserActivity.EditingBeatmap edit:
return edit.BeatmapInfo; return edit.BeatmapInfo;
} }
@ -183,10 +183,10 @@ namespace osu.Desktop
case UserActivity.InGame game: case UserActivity.InGame game:
return game.BeatmapInfo.ToString() ?? string.Empty; return game.BeatmapInfo.ToString() ?? string.Empty;
case UserActivity.Editing edit: case UserActivity.EditingBeatmap edit:
return edit.BeatmapInfo.ToString() ?? string.Empty; return edit.BeatmapInfo.ToString() ?? string.Empty;
case UserActivity.Watching watching: case UserActivity.WatchingReplay watching:
return watching.BeatmapInfo.ToString(); return watching.BeatmapInfo.ToString();
case UserActivity.InLobby lobby: case UserActivity.InLobby lobby:

View File

@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Tests
if (withModifiedSkin) if (withModifiedSkin)
{ {
AddStep("change component scale", () => Player.ChildrenOfType<LegacyScoreCounter>().First().Scale = new Vector2(2f)); AddStep("change component scale", () => Player.ChildrenOfType<LegacyScoreCounter>().First().Scale = new Vector2(2f));
AddStep("update target", () => Player.ChildrenOfType<SkinnableTargetContainer>().ForEach(LegacySkin.UpdateDrawableTarget)); AddStep("update target", () => Player.ChildrenOfType<SkinComponentsContainer>().ForEach(LegacySkin.UpdateDrawableTarget));
AddStep("exit player", () => Player.Exit()); AddStep("exit player", () => Player.Exit());
CreateTest(); CreateTest();
} }

View File

@ -60,26 +60,24 @@ namespace osu.Game.Rulesets.Catch.Tests
[Test] [Test]
public void TestCatcherHyperStateReverted() public void TestCatcherHyperStateReverted()
{ {
DrawableCatchHitObject drawableObject1 = null;
DrawableCatchHitObject drawableObject2 = null;
JudgementResult result1 = null; JudgementResult result1 = null;
JudgementResult result2 = null; JudgementResult result2 = null;
AddStep("catch hyper fruit", () => AddStep("catch hyper fruit", () =>
{ {
attemptCatch(new Fruit { HyperDashTarget = new Fruit { X = 100 } }, out drawableObject1, out result1); result1 = attemptCatch(new Fruit { HyperDashTarget = new Fruit { X = 100 } });
}); });
AddStep("catch normal fruit", () => AddStep("catch normal fruit", () =>
{ {
attemptCatch(new Fruit(), out drawableObject2, out result2); result2 = attemptCatch(new Fruit());
}); });
AddStep("revert second result", () => AddStep("revert second result", () =>
{ {
catcher.OnRevertResult(drawableObject2, result2); catcher.OnRevertResult(result2);
}); });
checkHyperDash(true); checkHyperDash(true);
AddStep("revert first result", () => AddStep("revert first result", () =>
{ {
catcher.OnRevertResult(drawableObject1, result1); catcher.OnRevertResult(result1);
}); });
checkHyperDash(false); checkHyperDash(false);
} }
@ -87,16 +85,15 @@ namespace osu.Game.Rulesets.Catch.Tests
[Test] [Test]
public void TestCatcherAnimationStateReverted() public void TestCatcherAnimationStateReverted()
{ {
DrawableCatchHitObject drawableObject = null;
JudgementResult result = null; JudgementResult result = null;
AddStep("catch kiai fruit", () => AddStep("catch kiai fruit", () =>
{ {
attemptCatch(new TestKiaiFruit(), out drawableObject, out result); result = attemptCatch(new TestKiaiFruit());
}); });
checkState(CatcherAnimationState.Kiai); checkState(CatcherAnimationState.Kiai);
AddStep("revert result", () => AddStep("revert result", () =>
{ {
catcher.OnRevertResult(drawableObject, result); catcher.OnRevertResult(result);
}); });
checkState(CatcherAnimationState.Idle); checkState(CatcherAnimationState.Idle);
} }
@ -268,23 +265,19 @@ namespace osu.Game.Rulesets.Catch.Tests
private void checkHyperDash(bool state) => AddAssert($"catcher is {(state ? "" : "not ")}hyper dashing", () => catcher.HyperDashing == state); private void checkHyperDash(bool state) => AddAssert($"catcher is {(state ? "" : "not ")}hyper dashing", () => catcher.HyperDashing == state);
private void attemptCatch(CatchHitObject hitObject)
{
attemptCatch(() => hitObject, 1);
}
private void attemptCatch(Func<CatchHitObject> hitObject, int count) private void attemptCatch(Func<CatchHitObject> hitObject, int count)
{ {
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
attemptCatch(hitObject(), out _, out _); attemptCatch(hitObject());
} }
private void attemptCatch(CatchHitObject hitObject, out DrawableCatchHitObject drawableObject, out JudgementResult result) private JudgementResult attemptCatch(CatchHitObject hitObject)
{ {
hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
drawableObject = createDrawableObject(hitObject); var drawableObject = createDrawableObject(hitObject);
result = createResult(hitObject); var result = createResult(hitObject);
applyResult(drawableObject, result); applyResult(drawableObject, result);
return result;
} }
private void applyResult(DrawableCatchHitObject drawableObject, JudgementResult result) private void applyResult(DrawableCatchHitObject drawableObject, JudgementResult result)

View File

@ -4,6 +4,7 @@
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK.Graphics; using osuTK.Graphics;
@ -27,12 +28,12 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
{ {
if (lookup is GlobalSkinComponentLookup targetComponent) if (lookup is SkinComponentsContainerLookup containerLookup)
{ {
switch (targetComponent.Lookup) switch (containerLookup.Target)
{ {
case GlobalSkinComponentLookup.LookupType.MainHUDComponents: case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
var components = base.GetDrawableComponent(lookup) as SkinnableTargetComponentsContainer; var components = base.GetDrawableComponent(lookup) as Container;
if (providesComboCounter && components != null) if (providesComboCounter && components != null)
{ {

View File

@ -63,12 +63,12 @@ namespace osu.Game.Rulesets.Catch.UI
updateCombo(result.ComboAtJudgement + 1, judgedObject.AccentColour.Value); updateCombo(result.ComboAtJudgement + 1, judgedObject.AccentColour.Value);
} }
public void OnRevertResult(DrawableCatchHitObject judgedObject, JudgementResult result) public void OnRevertResult(JudgementResult result)
{ {
if (!result.Type.AffectsCombo() || !result.HasResult) if (!result.Type.AffectsCombo() || !result.HasResult)
return; return;
updateCombo(result.ComboAtJudgement, judgedObject.AccentColour.Value); updateCombo(result.ComboAtJudgement, null);
} }
private void updateCombo(int newCombo, Color4? hitObjectColour) private void updateCombo(int newCombo, Color4? hitObjectColour)

View File

@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Catch.UI
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
=> CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result); => CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result);
private void onRevertResult(DrawableHitObject judgedObject, JudgementResult result) private void onRevertResult(JudgementResult result)
=> CatcherArea.OnRevertResult((DrawableCatchHitObject)judgedObject, result); => CatcherArea.OnRevertResult(result);
} }
} }

View File

@ -254,7 +254,7 @@ namespace osu.Game.Rulesets.Catch.UI
} }
} }
public void OnRevertResult(DrawableCatchHitObject drawableObject, JudgementResult result) public void OnRevertResult(JudgementResult result)
{ {
var catchResult = (CatchJudgementResult)result; var catchResult = (CatchJudgementResult)result;
@ -268,8 +268,8 @@ namespace osu.Game.Rulesets.Catch.UI
SetHyperDashState(); SetHyperDashState();
} }
caughtObjectContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject, false); caughtObjectContainer.RemoveAll(d => d.HitObject == result.HitObject, false);
droppedObjectTarget.RemoveAll(d => d.HitObject == drawableObject.HitObject, false); droppedObjectTarget.RemoveAll(d => d.HitObject == result.HitObject, false);
} }
/// <summary> /// <summary>

View File

@ -73,10 +73,10 @@ namespace osu.Game.Rulesets.Catch.UI
comboDisplay.OnNewResult(hitObject, result); comboDisplay.OnNewResult(hitObject, result);
} }
public void OnRevertResult(DrawableCatchHitObject hitObject, JudgementResult result) public void OnRevertResult(JudgementResult result)
{ {
comboDisplay.OnRevertResult(hitObject, result); comboDisplay.OnRevertResult(result);
Catcher.OnRevertResult(hitObject, result); Catcher.OnRevertResult(result);
} }
protected override void Update() protected override void Update()

View File

@ -0,0 +1,67 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public partial class TestSceneObjectPlacement : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new ManiaRuleset();
[Resolved]
private OsuConfigManager config { get; set; } = null!;
[Test]
public void TestPlacementBeforeTrackStart()
{
AddStep("Seek to 0", () => EditorClock.Seek(0));
AddStep("Select note", () => InputManager.Key(Key.Number2));
AddStep("Hover negative span", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<Container>().First(x => x.Name == "Icons").Children[0]);
});
AddStep("Click", () => InputManager.Click(MouseButton.Left));
AddAssert("No notes placed", () => EditorBeatmap.HitObjects.All(x => x.StartTime >= 0));
}
[Test]
public void TestSeekOnNotePlacement()
{
double? initialTime = null;
AddStep("store initial time", () => initialTime = EditorClock.CurrentTime);
AddStep("change seek setting to true", () => config.SetValue(OsuSetting.EditorAutoSeekOnPlacement, true));
placeObject();
AddUntilStep("wait for seek to complete", () => !EditorClock.IsSeeking);
AddAssert("seeked forward to object", () => EditorClock.CurrentTime, () => Is.GreaterThan(initialTime));
}
[Test]
public void TestNoSeekOnNotePlacement()
{
double? initialTime = null;
AddStep("store initial time", () => initialTime = EditorClock.CurrentTime);
AddStep("change seek setting to false", () => config.SetValue(OsuSetting.EditorAutoSeekOnPlacement, false));
placeObject();
AddAssert("not seeking", () => !EditorClock.IsSeeking);
AddAssert("time is unchanged", () => EditorClock.CurrentTime, () => Is.EqualTo(initialTime));
}
private void placeObject()
{
AddStep("select note placement tool", () => InputManager.Key(Key.Number2));
AddStep("move mouse to centre of last column", () => InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Last().ScreenSpaceDrawQuad.Centre));
AddStep("place note", () => InputManager.Click(MouseButton.Left));
}
}
}

View File

@ -1,30 +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.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests.Editor
{
public partial class TestScenePlacementBeforeTrackStart : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new ManiaRuleset();
[Test]
public void TestPlacement()
{
AddStep("Seek to 0", () => EditorClock.Seek(0));
AddStep("Select note", () => InputManager.Key(Key.Number2));
AddStep("Hover negative span", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<Container>().First(x => x.Name == "Icons").Children[0]);
});
AddStep("Click", () => InputManager.Click(MouseButton.Left));
AddAssert("No notes placed", () => EditorBeatmap.HitObjects.All(x => x.StartTime >= 0));
}
}
}

View File

@ -69,8 +69,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary> /// </summary>
private double? releaseTime; private double? releaseTime;
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
public DrawableHoldNote() public DrawableHoldNote()
: this(null) : this(null)
{ {

View File

@ -15,13 +15,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary> /// </summary>
public partial class DrawableHoldNoteTail : DrawableNote public partial class DrawableHoldNoteTail : DrawableNote
{ {
/// <summary>
/// Lenience of release hit windows. This is to make cases where the hold note release
/// is timed alongside presses of other hit objects less awkward.
/// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps
/// </summary>
private const double release_window_lenience = 1.5;
protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteTail; protected override ManiaSkinComponents Component => ManiaSkinComponents.HoldNoteTail;
protected DrawableHoldNote HoldNote => (DrawableHoldNote)ParentHitObject; protected DrawableHoldNote HoldNote => (DrawableHoldNote)ParentHitObject;
@ -40,14 +33,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public void UpdateResult() => base.UpdateResult(true); public void UpdateResult() => base.UpdateResult(true);
public override double MaximumJudgementOffset => base.MaximumJudgementOffset * release_window_lenience;
protected override void CheckForResult(bool userTriggered, double timeOffset) protected override void CheckForResult(bool userTriggered, double timeOffset)
{ {
Debug.Assert(HitObject.HitWindows != null); Debug.Assert(HitObject.HitWindows != null);
// Factor in the release lenience // Factor in the release lenience
timeOffset /= release_window_lenience; timeOffset /= TailNote.RELEASE_WINDOW_LENIENCE;
if (!userTriggered) if (!userTriggered)
{ {

View File

@ -81,6 +81,8 @@ namespace osu.Game.Rulesets.Mania.Objects
/// </summary> /// </summary>
public TailNote Tail { get; private set; } public TailNote Tail { get; private set; }
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
/// <summary> /// <summary>
/// The time between ticks of this hold. /// The time between ticks of this hold.
/// </summary> /// </summary>

View File

@ -10,6 +10,15 @@ namespace osu.Game.Rulesets.Mania.Objects
{ {
public class TailNote : Note public class TailNote : Note
{ {
/// <summary>
/// Lenience of release hit windows. This is to make cases where the hold note release
/// is timed alongside presses of other hit objects less awkward.
/// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps
/// </summary>
public const double RELEASE_WINDOW_LENIENCE = 1.5;
public override Judgement CreateJudgement() => new ManiaJudgement(); public override Judgement CreateJudgement() => new ManiaJudgement();
public override double MaximumJudgementOffset => base.MaximumJudgementOffset * RELEASE_WINDOW_LENIENCE;
} }
} }

View File

@ -2,12 +2,15 @@
// 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.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;
@ -34,6 +37,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
private Drawable? lightContainer; private Drawable? lightContainer;
private Drawable? light; private Drawable? light;
private LegacyNoteBodyStyle? bodyStyle;
public LegacyBodyPiece() public LegacyBodyPiece()
{ {
@ -54,9 +58,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
float lightScale = GetColumnSkinConfig<float>(skin, LegacyManiaSkinConfigurationLookups.HoldNoteLightScale)?.Value float lightScale = GetColumnSkinConfig<float>(skin, LegacyManiaSkinConfigurationLookups.HoldNoteLightScale)?.Value
?? 1; ?? 1;
float minimumColumnWidth = GetColumnSkinConfig<float>(skin, LegacyManiaSkinConfigurationLookups.MinimumColumnWidth)?.Value
?? 1;
// Create a temporary animation to retrieve the number of frames, in an effort to calculate the intended frame length. // Create a temporary animation to retrieve the number of frames, in an effort to calculate the intended frame length.
// This animation is discarded and re-queried with the appropriate frame length afterwards. // This animation is discarded and re-queried with the appropriate frame length afterwards.
var tmp = skin.GetAnimation(lightImage, true, false); var tmp = skin.GetAnimation(lightImage, true, false);
@ -83,7 +84,14 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
}; };
} }
bodySprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d => bodyStyle = skin.GetConfig<ManiaSkinConfigurationLookup, LegacyNoteBodyStyle>(new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.NoteBodyStyle))?.Value;
var wrapMode = bodyStyle == LegacyNoteBodyStyle.Stretch ? WrapMode.ClampToEdge : WrapMode.Repeat;
direction.BindTo(scrollingInfo.Direction);
isHitting.BindTo(holdNote.IsHitting);
bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true).With(d =>
{ {
if (d == null) if (d == null)
return; return;
@ -94,16 +102,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
d.Anchor = Anchor.TopCentre; d.Anchor = Anchor.TopCentre;
d.RelativeSizeAxes = Axes.Both; d.RelativeSizeAxes = Axes.Both;
d.Size = Vector2.One; d.Size = Vector2.One;
d.FillMode = FillMode.Stretch;
d.Height = minimumColumnWidth / d.DrawWidth * 1.6f; // constant matching stable.
// Todo: Wrap? // Todo: Wrap?
}); });
if (bodySprite != null) if (bodySprite != null)
InternalChild = bodySprite; InternalChild = bodySprite;
direction.BindTo(scrollingInfo.Direction);
isHitting.BindTo(holdNote.IsHitting);
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -164,8 +167,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{ {
if (bodySprite != null) if (bodySprite != null)
{ {
bodySprite.Origin = Anchor.BottomCentre; bodySprite.Origin = Anchor.TopCentre;
bodySprite.Scale = new Vector2(1, -1); bodySprite.Anchor = Anchor.BottomCentre; // needs to be flipped due to scale flip in Update.
} }
if (light != null) if (light != null)
@ -176,7 +179,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
if (bodySprite != null) if (bodySprite != null)
{ {
bodySprite.Origin = Anchor.TopCentre; bodySprite.Origin = Anchor.TopCentre;
bodySprite.Scale = Vector2.One; bodySprite.Anchor = Anchor.TopCentre;
} }
if (light != null) if (light != null)
@ -207,6 +210,33 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{ {
base.Update(); base.Update();
missFadeTime.Value ??= holdNote.HoldBrokenTime; missFadeTime.Value ??= holdNote.HoldBrokenTime;
int scaleDirection = (direction.Value == ScrollingDirection.Down ? 1 : -1);
// here we go...
switch (bodyStyle)
{
case LegacyNoteBodyStyle.Stretch:
// this is how lazer works by default. nothing required.
if (bodySprite != null)
bodySprite.Scale = new Vector2(1, scaleDirection);
break;
default:
// this is where things get fucked up.
// honestly there's three modes to handle here but they seem really pointless?
// let's wait to see if anyone actually uses them in skins.
if (bodySprite != null)
{
var sprite = bodySprite as Sprite ?? bodySprite.ChildrenOfType<Sprite>().Single();
bodySprite.FillMode = FillMode.Stretch;
// i dunno this looks about right??
bodySprite.Scale = new Vector2(1, scaleDirection * 32800 / sprite.DrawHeight);
}
break;
}
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

View File

@ -0,0 +1,31 @@
// 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;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModAutopilot : OsuModTestScene
{
[Test]
public void TestInstantResume()
{
CreateModTest(new ModTestData
{
Mod = new OsuModAutopilot(),
PassCondition = () => true,
Autoplay = false,
});
AddUntilStep("wait for gameplay start", () => Player.LocalUserPlaying.Value);
AddStep("press pause", () => InputManager.PressKey(Key.Escape));
AddUntilStep("wait until paused", () => Player.GameplayClockContainer.IsPaused.Value);
AddStep("release pause", () => InputManager.ReleaseKey(Key.Escape));
AddStep("press resume", () => InputManager.PressKey(Key.Escape));
AddUntilStep("wait for resume", () => !Player.IsResuming);
AddAssert("resumed", () => !Player.GameplayClockContainer.IsPaused.Value);
}
}
}

View File

@ -0,0 +1,156 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public partial class TestSceneHitCircleLateFade : OsuTestScene
{
private float? alphaAtMiss;
[Test]
public void TestHitCircleClassicMod()
{
AddStep("Create hit circle", () =>
{
SelectedMods.Value = new Mod[] { new OsuModClassic() };
createCircle();
});
AddUntilStep("Wait until circle is missed", () => alphaAtMiss.IsNotNull());
AddAssert("Transparent when missed", () => alphaAtMiss == 0);
}
[Test]
public void TestHitCircleClassicAndFullHiddenMods()
{
AddStep("Create hit circle", () =>
{
SelectedMods.Value = new Mod[] { new OsuModHidden(), new OsuModClassic() };
createCircle();
});
AddUntilStep("Wait until circle is missed", () => alphaAtMiss.IsNotNull());
AddAssert("Transparent when missed", () => alphaAtMiss == 0);
}
[Test]
public void TestHitCircleClassicAndApproachCircleOnlyHiddenMods()
{
AddStep("Create hit circle", () =>
{
SelectedMods.Value = new Mod[] { new OsuModHidden { OnlyFadeApproachCircles = { Value = true } }, new OsuModClassic() };
createCircle();
});
AddUntilStep("Wait until circle is missed", () => alphaAtMiss.IsNotNull());
AddAssert("Transparent when missed", () => alphaAtMiss == 0);
}
[Test]
public void TestHitCircleNoMod()
{
AddStep("Create hit circle", () =>
{
SelectedMods.Value = Array.Empty<Mod>();
createCircle();
});
AddUntilStep("Wait until circle is missed", () => alphaAtMiss.IsNotNull());
AddAssert("Opaque when missed", () => alphaAtMiss == 1);
}
[Test]
public void TestSliderClassicMod()
{
AddStep("Create slider", () =>
{
SelectedMods.Value = new Mod[] { new OsuModClassic() };
createSlider();
});
AddUntilStep("Wait until head circle is missed", () => alphaAtMiss.IsNotNull());
AddAssert("Head circle transparent when missed", () => alphaAtMiss == 0);
}
[Test]
public void TestSliderNoMod()
{
AddStep("Create slider", () =>
{
SelectedMods.Value = Array.Empty<Mod>();
createSlider();
});
AddUntilStep("Wait until head circle is missed", () => alphaAtMiss.IsNotNull());
AddAssert("Head circle opaque when missed", () => alphaAtMiss == 1);
}
private void createCircle()
{
alphaAtMiss = null;
DrawableHitCircle drawableHitCircle = new DrawableHitCircle(new HitCircle
{
StartTime = Time.Current + 500,
Position = new Vector2(250)
});
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
mod.ApplyToDrawableHitObject(drawableHitCircle);
drawableHitCircle.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
drawableHitCircle.OnNewResult += (_, _) =>
{
alphaAtMiss = drawableHitCircle.Alpha;
};
Child = drawableHitCircle;
}
private void createSlider()
{
alphaAtMiss = null;
DrawableSlider drawableSlider = new DrawableSlider(new Slider
{
StartTime = Time.Current + 500,
Position = new Vector2(250),
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(0, 100),
})
});
drawableSlider.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
drawableSlider.OnLoadComplete += _ =>
{
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
mod.ApplyToDrawableHitObject(drawableSlider.HeadCircle);
drawableSlider.HeadCircle.OnNewResult += (_, _) =>
{
alphaAtMiss = drawableSlider.HeadCircle.Alpha;
};
};
Child = drawableSlider;
}
}
}

View File

@ -11,6 +11,7 @@ using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Mods
private const double flash_duration = 1000; private const double flash_duration = 1000;
private DrawableRuleset<OsuHitObject> ruleset = null!; private DrawableOsuRuleset ruleset = null!;
protected OsuAction? LastAcceptedAction { get; private set; } protected OsuAction? LastAcceptedAction { get; private set; }
@ -42,8 +43,8 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{ {
ruleset = drawableRuleset; ruleset = (DrawableOsuRuleset)drawableRuleset;
drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this)); ruleset.KeyBindingInputManager.Add(new InputInterceptor(this));
var periods = new List<Period>(); var periods = new List<Period>();

View File

@ -11,6 +11,7 @@ using osu.Game.Graphics;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
@ -55,11 +56,13 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{ {
// Grab the input manager to disable the user's cursor, and for future use // Grab the input manager to disable the user's cursor, and for future use
inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager; inputManager = ((DrawableOsuRuleset)drawableRuleset).KeyBindingInputManager;
inputManager.AllowUserCursorMovement = false; inputManager.AllowUserCursorMovement = false;
// Generate the replay frames the cursor should follow // Generate the replay frames the cursor should follow
replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap, drawableRuleset.Mods).Generate().Frames.Cast<OsuReplayFrame>().ToList(); replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap, drawableRuleset.Mods).Generate().Frames.Cast<OsuReplayFrame>().ToList();
drawableRuleset.UseResumeOverlay = false;
} }
} }
} }

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -11,6 +12,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
@ -31,6 +33,11 @@ namespace osu.Game.Rulesets.Osu.Mods
[SettingSource("Always play a slider's tail sample", "Always plays a slider's tail sample regardless of whether it was hit or not.")] [SettingSource("Always play a slider's tail sample", "Always plays a slider's tail sample regardless of whether it was hit or not.")]
public Bindable<bool> AlwaysPlayTailSample { get; } = new BindableBool(true); public Bindable<bool> AlwaysPlayTailSample { get; } = new BindableBool(true);
[SettingSource("Fade out hit circles earlier", "Make hit circles fade out into a miss, rather than after it.")]
public Bindable<bool> FadeHitCircleEarly { get; } = new Bindable<bool>(true);
private bool usingHiddenFading;
public void ApplyToHitObject(HitObject hitObject) public void ApplyToHitObject(HitObject hitObject)
{ {
switch (hitObject) switch (hitObject)
@ -51,6 +58,8 @@ namespace osu.Game.Rulesets.Osu.Mods
if (ClassicNoteLock.Value) if (ClassicNoteLock.Value)
osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy(); osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy();
usingHiddenFading = drawableRuleset.Mods.OfType<OsuModHidden>().SingleOrDefault()?.OnlyFadeApproachCircles.Value == false;
} }
public void ApplyToDrawableHitObject(DrawableHitObject obj) public void ApplyToDrawableHitObject(DrawableHitObject obj)
@ -59,12 +68,32 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
case DrawableSliderHead head: case DrawableSliderHead head:
head.TrackFollowCircle = !NoSliderHeadMovement.Value; head.TrackFollowCircle = !NoSliderHeadMovement.Value;
if (FadeHitCircleEarly.Value && !usingHiddenFading)
applyEarlyFading(head);
break; break;
case DrawableSliderTail tail: case DrawableSliderTail tail:
tail.SamplePlaysOnlyOnHit = !AlwaysPlayTailSample.Value; tail.SamplePlaysOnlyOnHit = !AlwaysPlayTailSample.Value;
break; break;
case DrawableHitCircle circle:
if (FadeHitCircleEarly.Value && !usingHiddenFading)
applyEarlyFading(circle);
break;
} }
} }
private void applyEarlyFading(DrawableHitCircle circle)
{
circle.ApplyCustomUpdateState += (o, _) =>
{
using (o.BeginAbsoluteSequence(o.StateUpdateTime))
{
double okWindow = o.HitObject.HitWindows.WindowFor(HitResult.Ok);
double lateMissFadeTime = o.HitObject.HitWindows.WindowFor(HitResult.Meh) - okWindow;
o.Delay(okWindow).FadeOut(lateMissFadeTime);
}
};
}
} }
} }

View File

@ -10,6 +10,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -42,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{ {
// grab the input manager for future use. // grab the input manager for future use.
osuInputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager; osuInputManager = ((DrawableOsuRuleset)drawableRuleset).KeyBindingInputManager;
} }
public void ApplyToPlayer(Player player) public void ApplyToPlayer(Player player)

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public partial class DrawableOsuJudgement : DrawableJudgement public partial class DrawableOsuJudgement : DrawableJudgement
{ {
protected SkinnableLighting Lighting { get; private set; } internal SkinnableLighting Lighting { get; private set; }
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } private OsuConfigManager config { get; set; }

View File

@ -25,8 +25,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre; Origin = Anchor.Centre;
} }
public override double MaximumJudgementOffset => DrawableSpinner.HitObject.Duration;
/// <summary> /// <summary>
/// Apply a judgement result. /// Apply a judgement result.
/// </summary> /// </summary>

View File

@ -10,7 +10,7 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public partial class SkinnableLighting : SkinnableSprite internal partial class SkinnableLighting : SkinnableSprite
{ {
private DrawableHitObject targetObject; private DrawableHitObject targetObject;
private JudgementResult targetResult; private JudgementResult targetResult;

View File

@ -71,8 +71,8 @@ namespace osu.Game.Rulesets.Osu.Objects
double startTime = StartTime + (float)(i + 1) / totalSpins * Duration; double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
AddNested(i < SpinsRequired AddNested(i < SpinsRequired
? new SpinnerTick { StartTime = startTime } ? new SpinnerTick { StartTime = startTime, SpinnerDuration = Duration }
: new SpinnerBonusTick { StartTime = startTime }); : new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration });
} }
} }

View File

@ -11,10 +11,17 @@ namespace osu.Game.Rulesets.Osu.Objects
{ {
public class SpinnerTick : OsuHitObject public class SpinnerTick : OsuHitObject
{ {
/// <summary>
/// Duration of the <see cref="Spinner"/> containing this spinner tick.
/// </summary>
public double SpinnerDuration { get; set; }
public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement(); public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty; protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public override double MaximumJudgementOffset => SpinnerDuration;
public class OsuSpinnerTickJudgement : OsuJudgement public class OsuSpinnerTickJudgement : OsuJudgement
{ {
public override HitResult MaxResult => HitResult.SmallBonus; public override HitResult MaxResult => HitResult.SmallBonus;

View File

@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Osu.UI
{ {
protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config; protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config;
public new OsuInputManager KeyBindingInputManager => (OsuInputManager)base.KeyBindingInputManager;
public new OsuPlayfield Playfield => (OsuPlayfield)base.Playfield; public new OsuPlayfield Playfield => (OsuPlayfield)base.Playfield;
public DrawableOsuRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null) public DrawableOsuRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)

View File

@ -23,8 +23,8 @@ namespace osu.Game.Rulesets.Taiko.Mods
public void ApplyToDrawableHitObject(DrawableHitObject drawable) public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{ {
if (drawable is DrawableHit) if (drawable is DrawableTaikoHitObject hit)
drawable.SnapJudgementLocation = true; hit.SnapJudgementLocation = true;
} }
} }
} }

View File

@ -8,6 +8,6 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModRelax : ModRelax public class TaikoModRelax : ModRelax
{ {
public override LocalisableString Description => @"No ninja-like spinners, demanding drumrolls or unexpected katu's."; public override LocalisableString Description => @"No ninja-like spinners, demanding drumrolls or unexpected katus.";
} }
} }

View File

@ -37,8 +37,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumRollTick), _ => new TickPiece()); protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumRollTick), _ => new TickPiece());
public override double MaximumJudgementOffset => HitObject.HitWindow;
protected override void OnApply() protected override void OnApply()
{ {
base.OnApply(); base.OnApply();

View File

@ -25,6 +25,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
private readonly Container nonProxiedContent; private readonly Container nonProxiedContent;
/// <summary>
/// Whether the location of the hit should be snapped to the hit target before animating.
/// </summary>
/// <remarks>
/// This is how osu-stable worked, but notably is not how TnT works.
/// Not snapping results in less visual feedback on hit accuracy.
/// </remarks>
public bool SnapJudgementLocation { get; set; }
protected DrawableTaikoHitObject([CanBeNull] TaikoHitObject hitObject) protected DrawableTaikoHitObject([CanBeNull] TaikoHitObject hitObject)
: base(hitObject) : base(hitObject)
{ {

View File

@ -31,6 +31,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty; protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public override double MaximumJudgementOffset => HitWindow;
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime }; protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime };
public class StrongNestedHit : StrongNestedHitObject public class StrongNestedHit : StrongNestedHitObject

View File

@ -12,7 +12,7 @@ using osu.Game.Overlays.Settings;
namespace osu.Game.Tests.Mods namespace osu.Game.Tests.Mods
{ {
[TestFixture] [TestFixture]
public partial class SettingsSourceAttributeTest public partial class SettingSourceAttributeTest
{ {
[Test] [Test]
public void TestOrdering() public void TestOrdering()

View File

@ -7,6 +7,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Audio;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -93,6 +94,7 @@ namespace osu.Game.Tests.NonVisual
remove => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context"); remove => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context");
} }
public override IAdjustableAudioComponent Audio { get; }
public override Playfield Playfield { get; } public override Playfield Playfield { get; }
public override Container Overlays { get; } public override Container Overlays { get; }
public override Container FrameStableComponents { get; } public override Container FrameStableComponents { get; }

View File

@ -66,15 +66,15 @@ namespace osu.Game.Tests.Skins
{ {
var skin = new TestSkin(new SkinInfo(), null, storage); var skin = new TestSkin(new SkinInfo(), null, storage);
foreach (var target in skin.DrawableComponentInfo) foreach (var target in skin.LayoutInfos)
{ {
foreach (var info in target.Value) foreach (var info in target.Value.AllDrawables)
instantiatedTypes.Add(info.Type); instantiatedTypes.Add(info.Type);
} }
} }
} }
var editableTypes = SkinnableInfo.GetAllAvailableDrawables().Where(t => (Activator.CreateInstance(t) as ISkinnableDrawable)?.IsEditable == true); var editableTypes = SerialisedDrawableInfo.GetAllAvailableDrawables().Where(t => (Activator.CreateInstance(t) as ISerialisableDrawable)?.IsEditable == true);
Assert.That(instantiatedTypes, Is.EquivalentTo(editableTypes)); Assert.That(instantiatedTypes, Is.EquivalentTo(editableTypes));
} }
@ -87,8 +87,8 @@ namespace osu.Game.Tests.Skins
{ {
var skin = new TestSkin(new SkinInfo(), null, storage); var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.DrawableComponentInfo, Has.Count.EqualTo(2)); Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.MainHUDComponents], Has.Length.EqualTo(9)); Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(9));
} }
} }
@ -100,11 +100,11 @@ namespace osu.Game.Tests.Skins
{ {
var skin = new TestSkin(new SkinInfo(), null, storage); var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.DrawableComponentInfo, Has.Count.EqualTo(2)); Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.MainHUDComponents], Has.Length.EqualTo(6)); Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(6));
Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.SongSelect], Has.Length.EqualTo(1)); Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.SongSelect].AllDrawables.ToArray(), Has.Length.EqualTo(1));
var skinnableInfo = skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.SongSelect].First(); var skinnableInfo = skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.SongSelect].AllDrawables.First();
Assert.That(skinnableInfo.Type, Is.EqualTo(typeof(SkinnableSprite))); Assert.That(skinnableInfo.Type, Is.EqualTo(typeof(SkinnableSprite)));
Assert.That(skinnableInfo.Settings.First().Key, Is.EqualTo("sprite_name")); Assert.That(skinnableInfo.Settings.First().Key, Is.EqualTo("sprite_name"));
@ -115,10 +115,10 @@ namespace osu.Game.Tests.Skins
using (var storage = new ZipArchiveReader(stream)) using (var storage = new ZipArchiveReader(stream))
{ {
var skin = new TestSkin(new SkinInfo(), null, storage); var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.MainHUDComponents], Has.Length.EqualTo(8)); Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(8));
Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.MainHUDComponents].Select(i => i.Type), Contains.Item(typeof(UnstableRateCounter))); Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(UnstableRateCounter)));
Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.MainHUDComponents].Select(i => i.Type), Contains.Item(typeof(ColourHitErrorMeter))); Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(ColourHitErrorMeter)));
Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.MainHUDComponents].Select(i => i.Type), Contains.Item(typeof(LegacySongProgress))); Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(LegacySongProgress)));
} }
} }

View File

@ -1,26 +1,23 @@
// 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.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Graphics.Containers;
using osu.Framework.Lists; using osu.Framework.Lists;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Skinning.Legacy; using osu.Game.Rulesets.Osu.Skinning.Legacy;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Storyboards; using osu.Game.Storyboards;
@ -28,10 +25,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public partial class TestSceneBeatmapSkinFallbacks : OsuPlayerTestScene public partial class TestSceneBeatmapSkinFallbacks : OsuPlayerTestScene
{ {
private ISkin currentBeatmapSkin; private ISkin currentBeatmapSkin = null!;
[Resolved] [Resolved]
private SkinManager skinManager { get; set; } private SkinManager skinManager { get; set; } = null!;
protected override bool HasCustomSteps => true; protected override bool HasCustomSteps => true;
@ -39,8 +36,8 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestEmptyLegacyBeatmapSkinFallsBack() public void TestEmptyLegacyBeatmapSkinFallsBack()
{ {
CreateSkinTest(TrianglesSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null)); CreateSkinTest(TrianglesSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null));
AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinnableTargetContainer>().All(c => c.ComponentsLoaded)); AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinComponentsContainer>().All(c => c.ComponentsLoaded));
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(GlobalSkinComponentLookup.LookupType.MainHUDComponents, skinManager.CurrentSkin.Value)); AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinComponentsContainerLookup.TargetArea.MainHUDComponents, skinManager.CurrentSkin.Value));
} }
protected void CreateSkinTest(SkinInfo gameCurrentSkin, Func<ISkin> getBeatmapSkin) protected void CreateSkinTest(SkinInfo gameCurrentSkin, Func<ISkin> getBeatmapSkin)
@ -55,17 +52,17 @@ namespace osu.Game.Tests.Visual.Gameplay
}); });
} }
protected bool AssertComponentsFromExpectedSource(GlobalSkinComponentLookup.LookupType target, ISkin expectedSource) protected bool AssertComponentsFromExpectedSource(SkinComponentsContainerLookup.TargetArea target, ISkin expectedSource)
{ {
var actualComponentsContainer = Player.ChildrenOfType<SkinnableTargetContainer>().First(s => s.Target == target) var targetContainer = Player.ChildrenOfType<SkinComponentsContainer>().First(s => s.Lookup.Target == target);
.ChildrenOfType<SkinnableTargetComponentsContainer>().SingleOrDefault(); var actualComponentsContainer = targetContainer.ChildrenOfType<Container>().SingleOrDefault(c => c.Parent == targetContainer);
if (actualComponentsContainer == null) if (actualComponentsContainer == null)
return false; return false;
var actualInfo = actualComponentsContainer.CreateSkinnableInfo(); var actualInfo = actualComponentsContainer.CreateSerialisedInfo();
var expectedComponentsContainer = (SkinnableTargetComponentsContainer)expectedSource.GetDrawableComponent(new GlobalSkinComponentLookup(target)); var expectedComponentsContainer = expectedSource.GetDrawableComponent(new SkinComponentsContainerLookup(target)) as Container;
if (expectedComponentsContainer == null) if (expectedComponentsContainer == null)
return false; return false;
@ -86,23 +83,23 @@ namespace osu.Game.Tests.Visual.Gameplay
Add(expectedComponentsAdjustmentContainer); Add(expectedComponentsAdjustmentContainer);
expectedComponentsAdjustmentContainer.UpdateSubTree(); expectedComponentsAdjustmentContainer.UpdateSubTree();
var expectedInfo = expectedComponentsContainer.CreateSkinnableInfo(); var expectedInfo = expectedComponentsContainer.CreateSerialisedInfo();
Remove(expectedComponentsAdjustmentContainer, true); Remove(expectedComponentsAdjustmentContainer, true);
return almostEqual(actualInfo, expectedInfo); return almostEqual(actualInfo, expectedInfo);
} }
private static bool almostEqual(SkinnableInfo info, SkinnableInfo other) => private static bool almostEqual(SerialisedDrawableInfo drawableInfo, SerialisedDrawableInfo? other) =>
other != null other != null
&& info.Type == other.Type && drawableInfo.Type == other.Type
&& info.Anchor == other.Anchor && drawableInfo.Anchor == other.Anchor
&& info.Origin == other.Origin && drawableInfo.Origin == other.Origin
&& Precision.AlmostEquals(info.Position, other.Position, 1) && Precision.AlmostEquals(drawableInfo.Position, other.Position, 1)
&& Precision.AlmostEquals(info.Scale, other.Scale) && Precision.AlmostEquals(drawableInfo.Scale, other.Scale)
&& Precision.AlmostEquals(info.Rotation, other.Rotation) && Precision.AlmostEquals(drawableInfo.Rotation, other.Rotation)
&& info.Children.SequenceEqual(other.Children, new FuncEqualityComparer<SkinnableInfo>(almostEqual)); && drawableInfo.Children.SequenceEqual(other.Children, new FuncEqualityComparer<SerialisedDrawableInfo>(almostEqual));
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
=> new CustomSkinWorkingBeatmap(beatmap, storyboard, Clock, Audio, currentBeatmapSkin); => new CustomSkinWorkingBeatmap(beatmap, storyboard, Clock, Audio, currentBeatmapSkin);
protected override Ruleset CreatePlayerRuleset() => new TestOsuRuleset(); protected override Ruleset CreatePlayerRuleset() => new TestOsuRuleset();
@ -111,7 +108,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
private readonly ISkin beatmapSkin; private readonly ISkin beatmapSkin;
public CustomSkinWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio, ISkin beatmapSkin) public CustomSkinWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard, IFrameBasedClock referenceClock, AudioManager audio, ISkin beatmapSkin)
: base(beatmap, storyboard, referenceClock, audio) : base(beatmap, storyboard, referenceClock, audio)
{ {
this.beatmapSkin = beatmapSkin; this.beatmapSkin = beatmapSkin;

View File

@ -1,50 +1,67 @@
// 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.Diagnostics;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Storyboards;
using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
public partial class TestSceneGameplaySampleTriggerSource : PlayerTestScene public partial class TestSceneGameplaySampleTriggerSource : PlayerTestScene
{ {
private TestGameplaySampleTriggerSource sampleTriggerSource; private TestGameplaySampleTriggerSource sampleTriggerSource = null!;
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
private Beatmap beatmap; private Beatmap beatmap = null!;
[Resolved]
private AudioManager audio { get; set; } = null!;
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audio);
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{ {
ControlPointInfo controlPointInfo = new LegacyControlPointInfo();
beatmap = new Beatmap beatmap = new Beatmap
{ {
BeatmapInfo = new BeatmapInfo BeatmapInfo = new BeatmapInfo
{ {
Difficulty = new BeatmapDifficulty { CircleSize = 6, SliderMultiplier = 3 }, Difficulty = new BeatmapDifficulty { CircleSize = 6, SliderMultiplier = 3 },
Ruleset = ruleset Ruleset = ruleset
} },
ControlPointInfo = controlPointInfo
}; };
const double start_offset = 8000; const double start_offset = 8000;
const double spacing = 2000; const double spacing = 2000;
// intentionally start objects a bit late so we can test the case of no alive objects.
double t = start_offset; double t = start_offset;
beatmap.HitObjects.AddRange(new[]
beatmap.HitObjects.AddRange(new HitObject[]
{ {
new HitCircle new HitCircle
{ {
// intentionally start objects a bit late so we can test the case of no alive objects.
StartTime = t += spacing, StartTime = t += spacing,
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
}, },
@ -61,12 +78,24 @@ namespace osu.Game.Tests.Visual.Gameplay
}, },
new HitCircle new HitCircle
{ {
StartTime = t + spacing, StartTime = t += spacing,
},
new Slider
{
StartTime = t += spacing,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }),
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) }, Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) },
SampleControlPoint = new SampleControlPoint { SampleBank = "soft" }, SampleControlPoint = new SampleControlPoint { SampleBank = "soft" },
}, },
}); });
// Add a change in volume halfway through final slider.
controlPointInfo.Add(t, new SampleControlPoint
{
SampleBank = "normal",
SampleVolume = 20,
});
return beatmap; return beatmap;
} }
@ -80,42 +109,88 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestCorrectHitObject() public void TestCorrectHitObject()
{ {
HitObjectLifetimeEntry nextObjectEntry = null; waitForAliveObjectIndex(null);
checkValidObjectIndex(0);
AddAssert("no alive objects", () => getNextAliveObject() == null); seekBeforeIndex(0);
waitForAliveObjectIndex(0);
checkValidObjectIndex(0);
AddAssert("check initially correct object", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[0]); AddAssert("first object not hit", () => getNextAliveObject()?.Entry?.Result?.HasResult != true);
AddUntilStep("get next object", () => AddStep("hit first object", () =>
{ {
var nextDrawableObject = getNextAliveObject(); var next = getNextAliveObject();
if (nextDrawableObject != null) if (next != null)
{ {
nextObjectEntry = nextDrawableObject.Entry; Debug.Assert(next.Entry?.Result?.HasResult != true);
InputManager.MoveMouseTo(nextDrawableObject.ScreenSpaceDrawQuad.Centre);
return true; InputManager.MoveMouseTo(next.ScreenSpaceDrawQuad.Centre);
InputManager.Click(MouseButton.Left);
} }
return false;
}); });
AddUntilStep("hit first hitobject", () => AddAssert("first object hit", () => getNextAliveObject()?.Entry?.Result?.HasResult == true);
{
InputManager.Click(MouseButton.Left);
return nextObjectEntry.Result?.HasResult == true;
});
AddAssert("check correct object after hit", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[1]); checkValidObjectIndex(1);
AddUntilStep("check correct object after miss", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[2]); // Still object 1 as it's not hit yet.
AddUntilStep("check correct object after miss", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[3]); seekBeforeIndex(1);
waitForAliveObjectIndex(1);
checkValidObjectIndex(1);
AddUntilStep("no alive objects", () => getNextAliveObject() == null); seekBeforeIndex(2);
AddAssert("check correct object after none alive", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[3]); waitForAliveObjectIndex(2);
checkValidObjectIndex(2);
seekBeforeIndex(3);
waitForAliveObjectIndex(3);
checkValidObjectIndex(3);
seekBeforeIndex(4);
waitForAliveObjectIndex(4);
// Even before the object, we should prefer the first nested object's sample.
// This is because the (parent) object will only play its sample at the final EndTime.
AddAssert("check valid object is slider's first nested", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[4].NestedHitObjects.First()));
AddStep("seek to just before slider ends", () => Player.GameplayClockContainer.Seek(beatmap.HitObjects[4].GetEndTime() - 100));
waitForCatchUp();
AddUntilStep("wait until valid object is slider's last nested", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[4].NestedHitObjects.Last()));
// After we get far enough away, the samples of the object itself should be used, not any nested object.
AddStep("seek to further after slider", () => Player.GameplayClockContainer.Seek(beatmap.HitObjects[4].GetEndTime() + 1000));
waitForCatchUp();
AddUntilStep("wait until valid object is slider itself", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[4]));
AddStep("Seek into future", () => Player.GameplayClockContainer.Seek(beatmap.HitObjects.Last().GetEndTime() + 10000));
waitForCatchUp();
waitForAliveObjectIndex(null);
checkValidObjectIndex(4);
} }
private DrawableHitObject getNextAliveObject() => private void seekBeforeIndex(int index)
{
AddStep($"seek to just before object {index}", () => Player.GameplayClockContainer.Seek(beatmap.HitObjects[index].StartTime - 100));
waitForCatchUp();
}
private void waitForCatchUp() =>
AddUntilStep("wait for frame stable clock to catch up", () => Precision.AlmostEquals(Player.GameplayClockContainer.CurrentTime, Player.DrawableRuleset.FrameStableClock.CurrentTime));
private void waitForAliveObjectIndex(int? index)
{
if (index == null)
AddUntilStep("wait for no alive objects", getNextAliveObject, () => Is.Null);
else
AddUntilStep($"wait for next alive to be {index}", () => getNextAliveObject()?.HitObject, () => Is.EqualTo(beatmap.HitObjects[index.Value]));
}
private void checkValidObjectIndex(int index) =>
AddAssert($"check valid object is {index}", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[index]));
private DrawableHitObject? getNextAliveObject() =>
Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.FirstOrDefault(); Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.FirstOrDefault();
[Test] [Test]

View File

@ -235,8 +235,8 @@ namespace osu.Game.Tests.Visual.Gameplay
createNew(); createNew();
AddUntilStep("wait for hud load", () => hudOverlay.IsLoaded); AddUntilStep("wait for hud load", () => hudOverlay.IsLoaded);
AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType<SkinnableTargetContainer>().Single().Alpha == 0); AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().Single().Alpha == 0);
AddUntilStep("wait for hud load", () => hudOverlay.ChildrenOfType<SkinnableTargetContainer>().All(c => c.ComponentsLoaded)); AddUntilStep("wait for hud load", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().All(c => c.ComponentsLoaded));
AddStep("bind on update", () => AddStep("bind on update", () =>
{ {
@ -254,10 +254,10 @@ namespace osu.Game.Tests.Visual.Gameplay
createNew(); createNew();
AddUntilStep("wait for hud load", () => hudOverlay.IsLoaded); AddUntilStep("wait for hud load", () => hudOverlay.IsLoaded);
AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType<SkinnableTargetContainer>().Single().Alpha == 0); AddUntilStep("wait for components to be hidden", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().Single().Alpha == 0);
AddStep("reload components", () => hudOverlay.ChildrenOfType<SkinnableTargetContainer>().Single().Reload()); AddStep("reload components", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().Single().Reload());
AddUntilStep("skinnable components loaded", () => hudOverlay.ChildrenOfType<SkinnableTargetContainer>().Single().ComponentsLoaded); AddUntilStep("skinnable components loaded", () => hudOverlay.ChildrenOfType<SkinComponentsContainer>().Single().ComponentsLoaded);
} }
private void createNew(Action<HUDOverlay>? action = null) private void createNew(Action<HUDOverlay>? action = null)

View File

@ -9,6 +9,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -281,6 +282,7 @@ namespace osu.Game.Tests.Visual.Gameplay
remove => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context"); remove => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context");
} }
public override IAdjustableAudioComponent Audio { get; }
public override Playfield Playfield { get; } public override Playfield Playfield { get; }
public override Container Overlays { get; } public override Container Overlays { get; }
public override Container FrameStableComponents { get; } public override Container FrameStableComponents { get; }

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.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Screens.Play.Break;
namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneLetterboxOverlay : OsuTestScene
{
public TestSceneLetterboxOverlay()
{
AddRange(new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both
},
new LetterboxOverlay()
});
}
}
}

View File

@ -19,7 +19,6 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -37,6 +36,8 @@ namespace osu.Game.Tests.Visual.Gameplay
private TestDrawablePoolingRuleset drawableRuleset; private TestDrawablePoolingRuleset drawableRuleset;
private TestPlayfield playfield => (TestPlayfield)drawableRuleset.Playfield;
[Test] [Test]
public void TestReusedWithHitObjectsSpacedFarApart() public void TestReusedWithHitObjectsSpacedFarApart()
{ {
@ -133,29 +134,49 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("no DHOs shown", () => !this.ChildrenOfType<DrawableTestHitObject>().Any()); AddUntilStep("no DHOs shown", () => !this.ChildrenOfType<DrawableTestHitObject>().Any());
} }
[Test]
public void TestRevertResult()
{
ManualClock clock = null;
Beatmap beatmap;
createTest(beatmap = new Beatmap
{
HitObjects =
{
new TestHitObject { StartTime = 0 },
new TestHitObject { StartTime = 500 },
new TestHitObject { StartTime = 1000 },
}
}, 10, () => new FramedClock(clock = new ManualClock()));
AddStep("fast forward to end", () => clock.CurrentTime = beatmap.HitObjects[^1].GetEndTime() + 100);
AddUntilStep("all judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(3));
AddStep("rewind to middle", () => clock.CurrentTime = beatmap.HitObjects[1].StartTime - 100);
AddUntilStep("some results reverted", () => playfield.JudgedObjects.Count, () => Is.EqualTo(1));
AddStep("fast forward to end", () => clock.CurrentTime = beatmap.HitObjects[^1].GetEndTime() + 100);
AddUntilStep("all judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(3));
AddStep("disable frame stability", () => drawableRuleset.FrameStablePlayback = false);
AddStep("instant seek to start", () => clock.CurrentTime = beatmap.HitObjects[0].StartTime - 100);
AddAssert("all results reverted", () => playfield.JudgedObjects.Count, () => Is.EqualTo(0));
}
[Test] [Test]
public void TestApplyHitResultOnKilled() public void TestApplyHitResultOnKilled()
{ {
ManualClock clock = null; ManualClock clock = null;
bool anyJudged = false;
void onNewResult(JudgementResult _) => anyJudged = true;
var beatmap = new Beatmap(); var beatmap = new Beatmap();
beatmap.HitObjects.Add(new TestKilledHitObject { Duration = 20 }); beatmap.HitObjects.Add(new TestKilledHitObject { Duration = 20 });
createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock())); createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock()));
AddStep("subscribe to new result", () =>
{
anyJudged = false;
drawableRuleset.NewResult += onNewResult;
});
AddStep("skip past object", () => clock.CurrentTime = beatmap.HitObjects[0].GetEndTime() + 1000); AddStep("skip past object", () => clock.CurrentTime = beatmap.HitObjects[0].GetEndTime() + 1000);
AddAssert("object judged", () => anyJudged); AddAssert("object judged", () => playfield.JudgedObjects.Count == 1);
AddStep("clean up", () => drawableRuleset.NewResult -= onNewResult);
} }
private void createTest(IBeatmap beatmap, int poolSize, Func<IFrameBasedClock> createClock = null) private void createTest(IBeatmap beatmap, int poolSize, Func<IFrameBasedClock> createClock = null)
@ -212,12 +233,24 @@ namespace osu.Game.Tests.Visual.Gameplay
private partial class TestPlayfield : Playfield private partial class TestPlayfield : Playfield
{ {
public readonly HashSet<HitObject> JudgedObjects = new HashSet<HitObject>();
private readonly int poolSize; private readonly int poolSize;
public TestPlayfield(int poolSize) public TestPlayfield(int poolSize)
{ {
this.poolSize = poolSize; this.poolSize = poolSize;
AddInternal(HitObjectContainer); AddInternal(HitObjectContainer);
NewResult += (_, r) =>
{
Assert.That(JudgedObjects, Has.No.Member(r.HitObject));
JudgedObjects.Add(r.HitObject);
};
RevertResult += r =>
{
Assert.That(JudgedObjects, Has.Member(r.HitObject));
JudgedObjects.Remove(r.HitObject);
};
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]

View File

@ -1,9 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.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.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -12,6 +14,7 @@ using osu.Game.Overlays.Settings;
using osu.Game.Overlays.SkinEditor; using osu.Game.Overlays.SkinEditor;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Play.HUD.HitErrorMeters; using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK.Input; using osuTK.Input;
@ -20,33 +23,85 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public partial class TestSceneSkinEditor : PlayerTestScene public partial class TestSceneSkinEditor : PlayerTestScene
{ {
private SkinEditor? skinEditor; private SkinEditor skinEditor = null!;
protected override bool Autoplay => true; protected override bool Autoplay => true;
[Cached] [Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
[Cached]
public readonly EditorClipboard Clipboard = new EditorClipboard();
private SkinComponentsContainer targetContainer => Player.ChildrenOfType<SkinComponentsContainer>().First();
[SetUpSteps] [SetUpSteps]
public override void SetUpSteps() public override void SetUpSteps()
{ {
base.SetUpSteps(); base.SetUpSteps();
AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinnableTargetContainer>().All(c => c.ComponentsLoaded)); AddUntilStep("wait for hud load", () => targetContainer.ComponentsLoaded);
AddStep("reload skin editor", () => AddStep("reload skin editor", () =>
{ {
skinEditor?.Expire(); if (skinEditor.IsNotNull())
skinEditor.Expire();
Player.ScaleTo(0.4f); Player.ScaleTo(0.4f);
LoadComponentAsync(skinEditor = new SkinEditor(Player), Add); LoadComponentAsync(skinEditor = new SkinEditor(Player), Add);
}); });
AddUntilStep("wait for loaded", () => skinEditor!.IsLoaded); AddUntilStep("wait for loaded", () => skinEditor.IsLoaded);
}
[TestCase(false)]
[TestCase(true)]
public void TestBringToFront(bool alterSelectionOrder)
{
AddAssert("Ensure over three components available", () => targetContainer.Components.Count, () => Is.GreaterThan(3));
IEnumerable<ISerialisableDrawable> originalOrder = null!;
AddStep("Save order of components before operation", () => originalOrder = targetContainer.Components.Take(3).ToArray());
if (alterSelectionOrder)
AddStep("Select first three components in reverse order", () => skinEditor.SelectedComponents.AddRange(originalOrder.Reverse()));
else
AddStep("Select first three components", () => skinEditor.SelectedComponents.AddRange(originalOrder));
AddAssert("Components are not front-most", () => targetContainer.Components.TakeLast(3).ToArray(), () => Is.Not.EqualTo(skinEditor.SelectedComponents));
AddStep("Bring to front", () => skinEditor.BringSelectionToFront());
AddAssert("Ensure components are now front-most in original order", () => targetContainer.Components.TakeLast(3).ToArray(), () => Is.EqualTo(originalOrder));
AddStep("Bring to front again", () => skinEditor.BringSelectionToFront());
AddAssert("Ensure components are still front-most in original order", () => targetContainer.Components.TakeLast(3).ToArray(), () => Is.EqualTo(originalOrder));
}
[TestCase(false)]
[TestCase(true)]
public void TestSendToBack(bool alterSelectionOrder)
{
AddAssert("Ensure over three components available", () => targetContainer.Components.Count, () => Is.GreaterThan(3));
IEnumerable<ISerialisableDrawable> originalOrder = null!;
AddStep("Save order of components before operation", () => originalOrder = targetContainer.Components.TakeLast(3).ToArray());
if (alterSelectionOrder)
AddStep("Select last three components in reverse order", () => skinEditor.SelectedComponents.AddRange(originalOrder.Reverse()));
else
AddStep("Select last three components", () => skinEditor.SelectedComponents.AddRange(originalOrder));
AddAssert("Components are not back-most", () => targetContainer.Components.Take(3).ToArray(), () => Is.Not.EqualTo(skinEditor.SelectedComponents));
AddStep("Send to back", () => skinEditor.SendSelectionToBack());
AddAssert("Ensure components are now back-most in original order", () => targetContainer.Components.Take(3).ToArray(), () => Is.EqualTo(originalOrder));
AddStep("Send to back again", () => skinEditor.SendSelectionToBack());
AddAssert("Ensure components are still back-most in original order", () => targetContainer.Components.Take(3).ToArray(), () => Is.EqualTo(originalOrder));
} }
[Test] [Test]
public void TestToggleEditor() public void TestToggleEditor()
{ {
AddToggleStep("toggle editor visibility", _ => skinEditor!.ToggleVisibility()); AddToggleStep("toggle editor visibility", _ => skinEditor.ToggleVisibility());
} }
[Test] [Test]
@ -59,7 +114,7 @@ namespace osu.Game.Tests.Visual.Gameplay
var blueprint = skinEditor.ChildrenOfType<SkinBlueprint>().First(b => b.Item is BarHitErrorMeter); var blueprint = skinEditor.ChildrenOfType<SkinBlueprint>().First(b => b.Item is BarHitErrorMeter);
hitErrorMeter = (BarHitErrorMeter)blueprint.Item; hitErrorMeter = (BarHitErrorMeter)blueprint.Item;
skinEditor!.SelectedComponents.Clear(); skinEditor.SelectedComponents.Clear();
skinEditor.SelectedComponents.Add(blueprint.Item); skinEditor.SelectedComponents.Add(blueprint.Item);
}); });

View File

@ -12,6 +12,7 @@ using osu.Game.Overlays.SkinEditor;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Tests.Gameplay; using osu.Game.Tests.Gameplay;
using osuTK.Input; using osuTK.Input;
@ -32,6 +33,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached(typeof(IGameplayClock))] [Cached(typeof(IGameplayClock))]
private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock()); private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock());
[Cached]
public readonly EditorClipboard Clipboard = new EditorClipboard();
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {

View File

@ -22,12 +22,18 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached(typeof(ScoreProcessor))] [Cached(typeof(ScoreProcessor))]
private TestScoreProcessor scoreProcessor = new TestScoreProcessor(); private TestScoreProcessor scoreProcessor = new TestScoreProcessor();
private readonly OsuHitWindows hitWindows = new OsuHitWindows(); private readonly OsuHitWindows hitWindows;
private UnstableRateCounter counter; private UnstableRateCounter counter;
private double prev; private double prev;
public TestSceneUnstableRateCounter()
{
hitWindows = new OsuHitWindows();
hitWindows.SetDifficulty(5);
}
[SetUpSteps] [SetUpSteps]
public void SetUp() public void SetUp()
{ {

View File

@ -15,6 +15,7 @@ using osu.Game.Overlays.Settings;
using osu.Game.Overlays.SkinEditor; using osu.Game.Overlays.SkinEditor;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD.HitErrorMeters; using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Beatmaps.IO;
@ -188,6 +189,33 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("mod overlay closed", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden); AddUntilStep("mod overlay closed", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden);
} }
[Test]
public void TestChangeToNonSkinnableScreen()
{
advanceToSongSelect();
openSkinEditor();
AddAssert("blueprint container present", () => skinEditor.ChildrenOfType<SkinBlueprintContainer>().Count(), () => Is.EqualTo(1));
AddAssert("placeholder not present", () => skinEditor.ChildrenOfType<NonSkinnableScreenPlaceholder>().Count(), () => Is.Zero);
AddAssert("editor sidebars not empty", () => skinEditor.ChildrenOfType<EditorSidebar>().SelectMany(sidebar => sidebar.Children).Count(), () => Is.GreaterThan(0));
AddStep("add skinnable component", () =>
{
skinEditor.ChildrenOfType<SkinComponentToolbox.ToolboxComponentButton>().First().TriggerClick();
});
AddUntilStep("newly added component selected", () => skinEditor.SelectedComponents, () => Has.Count.EqualTo(1));
AddStep("exit to main menu", () => Game.ScreenStack.CurrentScreen.Exit());
AddAssert("selection cleared", () => skinEditor.SelectedComponents, () => Has.Count.Zero);
AddAssert("blueprint container not present", () => skinEditor.ChildrenOfType<SkinBlueprintContainer>().Count(), () => Is.Zero);
AddAssert("placeholder present", () => skinEditor.ChildrenOfType<NonSkinnableScreenPlaceholder>().Count(), () => Is.EqualTo(1));
AddAssert("editor sidebars empty", () => skinEditor.ChildrenOfType<EditorSidebar>().SelectMany(sidebar => sidebar.Children).Count(), () => Is.Zero);
advanceToSongSelect();
AddAssert("blueprint container present", () => skinEditor.ChildrenOfType<SkinBlueprintContainer>().Count(), () => Is.EqualTo(1));
AddAssert("placeholder not present", () => skinEditor.ChildrenOfType<NonSkinnableScreenPlaceholder>().Count(), () => Is.Zero);
AddAssert("editor sidebars not empty", () => skinEditor.ChildrenOfType<EditorSidebar>().SelectMany(sidebar => sidebar.Children).Count(), () => Is.GreaterThan(0));
}
private void advanceToSongSelect() private void advanceToSongSelect()
{ {
PushAndConfirm(() => songSelect = new TestPlaySongSelect()); PushAndConfirm(() => songSelect = new TestPlaySongSelect());

View File

@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Online
[Test] [Test]
public void TestEditActivity() public void TestEditActivity()
{ {
AddStep("Set activity", () => api.Activity.Value = new UserActivity.Editing(new BeatmapInfo())); AddStep("Set activity", () => api.Activity.Value = new UserActivity.EditingBeatmap(new BeatmapInfo()));
AddStep("Run command", () => Add(new NowPlayingCommand(new Channel()))); AddStep("Run command", () => Add(new NowPlayingCommand(new Channel())));

View File

@ -109,15 +109,16 @@ namespace osu.Game.Tests.Visual.Online
AddStep("set online status", () => status.Value = new UserStatusOnline()); AddStep("set online status", () => status.Value = new UserStatusOnline());
AddStep("idle", () => activity.Value = null); AddStep("idle", () => activity.Value = null);
AddStep("watching", () => activity.Value = new UserActivity.Watching(createScore(@"nats"))); AddStep("watching replay", () => activity.Value = new UserActivity.WatchingReplay(createScore(@"nats")));
AddStep("spectating", () => activity.Value = new UserActivity.Spectating(createScore(@"mrekk"))); AddStep("spectating user", () => activity.Value = new UserActivity.SpectatingUser(createScore(@"mrekk")));
AddStep("solo (osu!)", () => activity.Value = soloGameStatusForRuleset(0)); AddStep("solo (osu!)", () => activity.Value = soloGameStatusForRuleset(0));
AddStep("solo (osu!taiko)", () => activity.Value = soloGameStatusForRuleset(1)); AddStep("solo (osu!taiko)", () => activity.Value = soloGameStatusForRuleset(1));
AddStep("solo (osu!catch)", () => activity.Value = soloGameStatusForRuleset(2)); AddStep("solo (osu!catch)", () => activity.Value = soloGameStatusForRuleset(2));
AddStep("solo (osu!mania)", () => activity.Value = soloGameStatusForRuleset(3)); AddStep("solo (osu!mania)", () => activity.Value = soloGameStatusForRuleset(3));
AddStep("choosing", () => activity.Value = new UserActivity.ChoosingBeatmap()); AddStep("choosing", () => activity.Value = new UserActivity.ChoosingBeatmap());
AddStep("editing", () => activity.Value = new UserActivity.Editing(null)); AddStep("editing beatmap", () => activity.Value = new UserActivity.EditingBeatmap(null));
AddStep("modding", () => activity.Value = new UserActivity.Modding()); AddStep("modding beatmap", () => activity.Value = new UserActivity.ModdingBeatmap(null));
AddStep("testing beatmap", () => activity.Value = new UserActivity.TestingBeatmap(null, null));
} }
[Test] [Test]

View File

@ -516,15 +516,20 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
sets.Clear(); sets.Clear();
for (int i = 0; i < 20; i++) for (int i = 0; i < 10; i++)
{ {
var set = TestResources.CreateTestBeatmapSetInfo(5); var set = TestResources.CreateTestBeatmapSetInfo(5);
if (i >= 2 && i < 10) // A total of 6 sets have date submitted (4 don't)
// A total of 5 sets have artist string (3 of which also have date submitted)
if (i >= 2 && i < 8) // i = 2, 3, 4, 5, 6, 7 have submitted date
set.DateSubmitted = DateTimeOffset.Now.AddMinutes(i); set.DateSubmitted = DateTimeOffset.Now.AddMinutes(i);
if (i < 5) if (i < 5) // i = 0, 1, 2, 3, 4 have matching string
set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_string); set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_string);
set.Beatmaps.ForEach(b => b.Metadata.Title = $"submitted: {set.DateSubmitted}");
sets.Add(set); sets.Add(set);
} }
}); });
@ -532,15 +537,26 @@ namespace osu.Game.Tests.Visual.SongSelect
loadBeatmaps(sets); loadBeatmaps(sets);
AddStep("Sort by date submitted", () => carousel.Filter(new FilterCriteria { Sort = SortMode.DateSubmitted }, false)); AddStep("Sort by date submitted", () => carousel.Filter(new FilterCriteria { Sort = SortMode.DateSubmitted }, false));
checkVisibleItemCount(diff: false, count: 8); checkVisibleItemCount(diff: false, count: 10);
checkVisibleItemCount(diff: true, count: 5); checkVisibleItemCount(diff: true, count: 5);
AddAssert("missing date are at end",
() => carousel.Items.OfType<DrawableCarouselBeatmapSet>().Reverse().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted == null).Count(), () => Is.EqualTo(4));
AddAssert("rest are at start", () => carousel.Items.OfType<DrawableCarouselBeatmapSet>().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted != null).Count(),
() => Is.EqualTo(6));
AddStep("Sort by date submitted and string", () => carousel.Filter(new FilterCriteria AddStep("Sort by date submitted and string", () => carousel.Filter(new FilterCriteria
{ {
Sort = SortMode.DateSubmitted, Sort = SortMode.DateSubmitted,
SearchText = zzz_string SearchText = zzz_string
}, false)); }, false));
checkVisibleItemCount(diff: false, count: 3); checkVisibleItemCount(diff: false, count: 5);
checkVisibleItemCount(diff: true, count: 5); checkVisibleItemCount(diff: true, count: 5);
AddAssert("missing date are at end",
() => carousel.Items.OfType<DrawableCarouselBeatmapSet>().Reverse().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted == null).Count(), () => Is.EqualTo(2));
AddAssert("rest are at start", () => carousel.Items.OfType<DrawableCarouselBeatmapSet>().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted != null).Count(),
() => Is.EqualTo(3));
} }
[Test] [Test]
@ -1129,7 +1145,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
// until step required as we are querying against alive items, which are loaded asynchronously inside DrawableCarouselBeatmapSet. // until step required as we are querying against alive items, which are loaded asynchronously inside DrawableCarouselBeatmapSet.
AddUntilStep($"{count} {(diff ? "diffs" : "sets")} visible", () => AddUntilStep($"{count} {(diff ? "diffs" : "sets")} visible", () =>
carousel.Items.Count(s => (diff ? s.Item is CarouselBeatmap : s.Item is CarouselBeatmapSet) && s.Item.Visible) == count); carousel.Items.Count(s => (diff ? s.Item is CarouselBeatmap : s.Item is CarouselBeatmapSet) && s.Item.Visible), () => Is.EqualTo(count));
} }
private void checkSelectionIsCentered() private void checkSelectionIsCentered()
@ -1190,8 +1206,11 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
get get
{ {
foreach (var item in Scroll.Children) foreach (var item in Scroll.Children.OrderBy(c => c.Y))
{ {
if (item.Item?.Visible != true)
continue;
yield return item; yield return item;
if (item is DrawableCarouselBeatmapSet set) if (item is DrawableCarouselBeatmapSet set)

View File

@ -0,0 +1,146 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Screens.Select.FooterV2;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect
{
public partial class TestSceneSongSelectFooterV2 : OsuManualInputManagerTestScene
{
private FooterButtonRandomV2 randomButton = null!;
private FooterButtonModsV2 modsButton = null!;
private bool nextRandomCalled;
private bool previousRandomCalled;
private DummyOverlay overlay = null!;
[Cached]
private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
[SetUp]
public void SetUp() => Schedule(() =>
{
nextRandomCalled = false;
previousRandomCalled = false;
FooterV2 footer;
Children = new Drawable[]
{
footer = new FooterV2
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
},
overlay = new DummyOverlay()
};
footer.AddButton(modsButton = new FooterButtonModsV2(), overlay);
footer.AddButton(randomButton = new FooterButtonRandomV2
{
NextRandom = () => nextRandomCalled = true,
PreviousRandom = () => previousRandomCalled = true
});
footer.AddButton(new FooterButtonOptionsV2());
overlay.Hide();
});
[Test]
public void TestState()
{
AddToggleStep("set options enabled state", state => this.ChildrenOfType<FooterButtonV2>().Last().Enabled.Value = state);
}
[Test]
public void TestFooterRandom()
{
AddStep("press F2", () => InputManager.Key(Key.F2));
AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled);
}
[Test]
public void TestFooterRandomViaMouse()
{
AddStep("click button", () =>
{
InputManager.MoveMouseTo(randomButton);
InputManager.Click(MouseButton.Left);
});
AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled);
}
[Test]
public void TestFooterRewind()
{
AddStep("press Shift+F2", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.PressKey(Key.F2);
InputManager.ReleaseKey(Key.F2);
InputManager.ReleaseKey(Key.LShift);
});
AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
}
[Test]
public void TestFooterRewindViaShiftMouseLeft()
{
AddStep("shift + click button", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.MoveMouseTo(randomButton);
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.LShift);
});
AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
}
[Test]
public void TestFooterRewindViaMouseRight()
{
AddStep("right click button", () =>
{
InputManager.MoveMouseTo(randomButton);
InputManager.Click(MouseButton.Right);
});
AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
}
[Test]
public void TestOverlayPresent()
{
AddStep("Press F1", () =>
{
InputManager.MoveMouseTo(modsButton);
InputManager.Click(MouseButton.Left);
});
AddAssert("Overlay visible", () => overlay.State.Value == Visibility.Visible);
AddStep("Hide", () => overlay.Hide());
}
private partial class DummyOverlay : ShearedOverlayContainer
{
public DummyOverlay()
: base(OverlayColourScheme.Green)
{
}
[BackgroundDependencyLoader]
private void load()
{
Header.Title = "An overlay";
}
}
}
}

View File

@ -61,6 +61,18 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("scroll to 500", () => scroll.ScrollTo(500)); AddStep("scroll to 500", () => scroll.ScrollTo(500));
AddUntilStep("scrolled to 500", () => Precision.AlmostEquals(scroll.Current, 500, 0.1f)); AddUntilStep("scrolled to 500", () => Precision.AlmostEquals(scroll.Current, 500, 0.1f));
AddAssert("button is visible", () => scroll.Button.State == Visibility.Visible); AddAssert("button is visible", () => scroll.Button.State == Visibility.Visible);
AddStep("click button", () =>
{
InputManager.MoveMouseTo(scroll.Button);
InputManager.Click(MouseButton.Left);
});
AddAssert("button is visible", () => scroll.Button.State == Visibility.Visible);
AddStep("user scroll down by 1", () => InputManager.ScrollVerticalBy(-1));
AddAssert("button is hidden", () => scroll.Button.State == Visibility.Hidden);
} }
[Test] [Test]
@ -71,6 +83,10 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("invoke action", () => scroll.Button.Action.Invoke()); AddStep("invoke action", () => scroll.Button.Action.Invoke());
AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 0, 0.1f)); AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 0, 0.1f));
AddStep("invoke action", () => scroll.Button.Action.Invoke());
AddAssert("scrolled to end", () => scroll.IsScrolledToEnd());
} }
[Test] [Test]
@ -85,6 +101,14 @@ namespace osu.Game.Tests.Visual.UserInterface
}); });
AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 0, 0.1f)); AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 0, 0.1f));
AddStep("click button", () =>
{
InputManager.MoveMouseTo(scroll.Button);
InputManager.Click(MouseButton.Left);
});
AddAssert("scrolled to end", () => scroll.IsScrolledToEnd());
} }
[Test] [Test]
@ -97,12 +121,12 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("hover button", () => InputManager.MoveMouseTo(scroll.Button)); AddStep("hover button", () => InputManager.MoveMouseTo(scroll.Button));
AddRepeatStep("click button", () => InputManager.Click(MouseButton.Left), 3); AddRepeatStep("click button", () => InputManager.Click(MouseButton.Left), 3);
AddAssert("invocation count is 1", () => invocationCount == 1); AddAssert("invocation count is 3", () => invocationCount == 3);
} }
private partial class TestScrollContainer : OverlayScrollContainer private partial class TestScrollContainer : OverlayScrollContainer
{ {
public new ScrollToTopButton Button => base.Button; public new ScrollBackButton Button => base.Button;
} }
} }
} }

View File

@ -98,6 +98,9 @@ namespace osu.Game.Audio
Track.Stop(); Track.Stop();
// Ensure the track is reset immediately on stopping, so the next time it is started it has a correct time value.
Track.Seek(0);
Stopped?.Invoke(); Stopped?.Invoke();
} }

View File

@ -178,6 +178,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.EditorDim, 0.25f, 0f, 0.75f, 0.25f); SetDefault(OsuSetting.EditorDim, 0.25f, 0f, 0.75f, 0.25f);
SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f, 0f, 1f, 0.25f); SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f, 0f, 1f, 0.25f);
SetDefault(OsuSetting.EditorShowHitMarkers, true); SetDefault(OsuSetting.EditorShowHitMarkers, true);
SetDefault(OsuSetting.EditorAutoSeekOnPlacement, true);
SetDefault(OsuSetting.LastProcessedMetadataId, -1); SetDefault(OsuSetting.LastProcessedMetadataId, -1);
@ -374,6 +375,7 @@ namespace osu.Game.Configuration
SeasonalBackgroundMode, SeasonalBackgroundMode,
EditorWaveformOpacity, EditorWaveformOpacity,
EditorShowHitMarkers, EditorShowHitMarkers,
EditorAutoSeekOnPlacement,
DiscordRichPresence, DiscordRichPresence,
AutomaticallyDownloadWhenSpectating, AutomaticallyDownloadWhenSpectating,
ShowOnlineExplicitContent, ShowOnlineExplicitContent,

View File

@ -5,6 +5,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Utils; using osu.Game.Utils;
@ -43,7 +44,10 @@ namespace osu.Game.Database
{ {
string itemFilename = GetFilename(item).GetValidFilename(); string itemFilename = GetFilename(item).GetValidFilename();
IEnumerable<string> existingExports = exportStorage.GetFiles("", $"{itemFilename}*{FileExtension}"); IEnumerable<string> existingExports =
exportStorage
.GetFiles(string.Empty, $"{itemFilename}*{FileExtension}")
.Concat(exportStorage.GetDirectories(string.Empty));
string filename = NamingUtils.GetNextBestFilename(existingExports, $"{itemFilename}{FileExtension}"); string filename = NamingUtils.GetNextBestFilename(existingExports, $"{itemFilename}{FileExtension}");
using (var stream = exportStorage.CreateFileSafely(filename)) using (var stream = exportStorage.CreateFileSafely(filename))

View File

@ -1,12 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osuTK; using osuTK;
namespace osu.Game.Extensions namespace osu.Game.Extensions
@ -48,42 +43,5 @@ namespace osu.Game.Extensions
/// <returns>The delta vector in Parent's coordinates.</returns> /// <returns>The delta vector in Parent's coordinates.</returns>
public static Vector2 ScreenSpaceDeltaToParentSpace(this Drawable drawable, Vector2 delta) => public static Vector2 ScreenSpaceDeltaToParentSpace(this Drawable drawable, Vector2 delta) =>
drawable.Parent.ToLocalSpace(drawable.Parent.ToScreenSpace(Vector2.Zero) + delta); drawable.Parent.ToLocalSpace(drawable.Parent.ToScreenSpace(Vector2.Zero) + delta);
public static SkinnableInfo CreateSkinnableInfo(this Drawable component) => new SkinnableInfo(component);
public static void ApplySkinnableInfo(this Drawable component, SkinnableInfo info)
{
// todo: can probably make this better via deserialisation directly using a common interface.
component.Position = info.Position;
component.Rotation = info.Rotation;
component.Scale = info.Scale;
component.Anchor = info.Anchor;
component.Origin = info.Origin;
if (component is ISkinnableDrawable skinnable)
{
skinnable.UsesFixedAnchor = info.UsesFixedAnchor;
foreach (var (_, property) in component.GetSettingsSourceProperties())
{
var bindable = ((IBindable)property.GetValue(component)!);
if (!info.Settings.TryGetValue(property.Name.ToSnakeCase(), out object? settingValue))
{
// TODO: We probably want to restore default if not included in serialisation information.
// This is not simple to do as SetDefault() is only found in the typed Bindable<T> interface right now.
continue;
}
skinnable.CopyAdjustedSetting(bindable, settingValue);
}
}
if (component is Container container)
{
foreach (var child in info.Children)
container.Add(child.CreateInstance());
}
}
} }
} }

View File

@ -11,7 +11,6 @@ using osu.Framework.Allocation;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Rendering.Vertices; using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Colour;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -259,8 +258,6 @@ namespace osu.Game.Graphics.Backgrounds
Vector2Extensions.Transform(triangleQuad.BottomRight * size, DrawInfo.Matrix) Vector2Extensions.Transform(triangleQuad.BottomRight * size, DrawInfo.Matrix)
); );
ColourInfo colourInfo = triangleColourInfo(DrawColourInfo.Colour, triangleQuad);
RectangleF textureCoords = new RectangleF( RectangleF textureCoords = new RectangleF(
triangleQuad.TopLeft.X - topLeft.X, triangleQuad.TopLeft.X - topLeft.X,
triangleQuad.TopLeft.Y - topLeft.Y, triangleQuad.TopLeft.Y - topLeft.Y,
@ -268,23 +265,12 @@ namespace osu.Game.Graphics.Backgrounds
triangleQuad.Height triangleQuad.Height
) / relativeSize; ) / relativeSize;
renderer.DrawQuad(texture, drawQuad, colourInfo, new RectangleF(0, 0, 1, 1), vertexBatch.AddAction, textureCoords: textureCoords); renderer.DrawQuad(texture, drawQuad, DrawColourInfo.Colour.Interpolate(triangleQuad), new RectangleF(0, 0, 1, 1), vertexBatch.AddAction, textureCoords: textureCoords);
} }
shader.Unbind(); shader.Unbind();
} }
private static ColourInfo triangleColourInfo(ColourInfo source, Quad quad)
{
return new ColourInfo
{
TopLeft = source.Interpolate(quad.TopLeft),
TopRight = source.Interpolate(quad.TopRight),
BottomLeft = source.Interpolate(quad.BottomLeft),
BottomRight = source.Interpolate(quad.BottomRight)
};
}
private static Quad clampToDrawable(Vector2 topLeft, Vector2 size) private static Quad clampToDrawable(Vector2 topLeft, Vector2 size)
{ {
float leftClamped = Math.Clamp(topLeft.X, 0f, 1f); float leftClamped = Math.Clamp(topLeft.X, 0f, 1f);

View File

@ -100,9 +100,9 @@ namespace osu.Game.Graphics.Containers
/// <summary> /// <summary>
/// Abort any ongoing confirmation. Should be called when the container's interaction is no longer valid (ie. the user releases a key). /// Abort any ongoing confirmation. Should be called when the container's interaction is no longer valid (ie. the user releases a key).
/// </summary> /// </summary>
protected void AbortConfirm() protected virtual void AbortConfirm()
{ {
if (!AllowMultipleFires && Fired) return; if (!confirming || (!AllowMultipleFires && Fired)) return;
confirming = false; confirming = false;
Fired = false; Fired = false;

View File

@ -46,8 +46,8 @@ namespace osu.Game.Graphics.Containers
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
{ {
CreateHoverSounds(sampleSet),
content, content,
CreateHoverSounds(sampleSet)
}); });
} }

View File

@ -21,7 +21,7 @@ namespace osu.Game.Graphics.Containers
/// </summary> /// </summary>
public const float BREAK_LIGHTEN_AMOUNT = 0.3f; public const float BREAK_LIGHTEN_AMOUNT = 0.3f;
protected const double BACKGROUND_FADE_DURATION = 800; public const double BACKGROUND_FADE_DURATION = 800;
/// <summary> /// <summary>
/// Whether or not user-configured settings relating to brightness of elements should be ignored /// Whether or not user-configured settings relating to brightness of elements should be ignored

View File

@ -249,13 +249,7 @@ namespace osu.Game.Graphics.UserInterface
private ColourInfo getSegmentColour(SegmentInfo segment) private ColourInfo getSegmentColour(SegmentInfo segment)
{ {
var segmentColour = new ColourInfo var segmentColour = DrawColourInfo.Colour.Interpolate(new Quad(segment.Start, 0f, segment.End - segment.Start, 1f));
{
TopLeft = DrawColourInfo.Colour.Interpolate(new Vector2(segment.Start, 0f)),
TopRight = DrawColourInfo.Colour.Interpolate(new Vector2(segment.End, 0f)),
BottomLeft = DrawColourInfo.Colour.Interpolate(new Vector2(segment.Start, 1f)),
BottomRight = DrawColourInfo.Colour.Interpolate(new Vector2(segment.End, 1f))
};
var tierColour = segment.Tier >= 0 ? tierColours[segment.Tier] : new Colour4(0, 0, 0, 0); var tierColour = segment.Tier >= 0 ? tierColours[segment.Tier] : new Colour4(0, 0, 0, 0);
segmentColour.ApplyChild(tierColour); segmentColour.ApplyChild(tierColour);

View File

@ -19,6 +19,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString ShowHitMarkers => new TranslatableString(getKey(@"show_hit_markers"), @"Show hit markers"); public static LocalisableString ShowHitMarkers => new TranslatableString(getKey(@"show_hit_markers"), @"Show hit markers");
/// <summary>
/// "Automatically seek after placing objects"
/// </summary>
public static LocalisableString AutoSeekOnPlacement => new TranslatableString(getKey(@"auto_seek_on_placement"), @"Automatically seek after placing objects");
/// <summary> /// <summary>
/// "Timing" /// "Timing"
/// </summary> /// </summary>

View File

@ -61,7 +61,7 @@ namespace osu.Game.Online.Chat
beatmapInfo = game.BeatmapInfo; beatmapInfo = game.BeatmapInfo;
break; break;
case UserActivity.Editing edit: case UserActivity.EditingBeatmap edit:
verb = "editing"; verb = "editing";
beatmapInfo = edit.BeatmapInfo; beatmapInfo = edit.BeatmapInfo;
break; break;

View File

@ -60,6 +60,7 @@ using osu.Game.Screens.Menu;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Skinning;
using osu.Game.Updater; using osu.Game.Updater;
using osu.Game.Users; using osu.Game.Users;
using osu.Game.Utils; using osu.Game.Utils;
@ -501,6 +502,23 @@ namespace osu.Game
/// <param name="version">The build version of the update stream</param> /// <param name="version">The build version of the update stream</param>
public void ShowChangelogBuild(string updateStream, string version) => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowBuild(updateStream, version)); public void ShowChangelogBuild(string updateStream, string version) => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowBuild(updateStream, version));
/// <summary>
/// Present a skin select immediately.
/// </summary>
/// <param name="skin">The skin to select.</param>
public void PresentSkin(SkinInfo skin)
{
var databasedSkin = SkinManager.Query(s => s.ID == skin.ID);
if (databasedSkin == null)
{
Logger.Log("The requested skin could not be loaded.", LoggingTarget.Information);
return;
}
SkinManager.CurrentSkinInfo.Value = databasedSkin;
}
/// <summary> /// <summary>
/// Present a beatmap at song select immediately. /// Present a beatmap at song select immediately.
/// The user should have already requested this interactively. /// The user should have already requested this interactively.
@ -777,6 +795,7 @@ namespace osu.Game
// todo: all archive managers should be able to be looped here. // todo: all archive managers should be able to be looped here.
SkinManager.PostNotification = n => Notifications.Post(n); SkinManager.PostNotification = n => Notifications.Post(n);
SkinManager.PresentImport = items => PresentSkin(items.First().Value);
BeatmapManager.PostNotification = n => Notifications.Post(n); BeatmapManager.PostNotification = n => Notifications.Post(n);
BeatmapManager.PresentImport = items => PresentBeatmap(items.First().Value); BeatmapManager.PresentImport = items => PresentBeatmap(items.First().Value);

View File

@ -39,6 +39,7 @@ namespace osu.Game.Overlays.Chat
private const float chatting_text_width = 220; private const float chatting_text_width = 220;
private const float search_icon_width = 40; private const float search_icon_width = 40;
private const float padding = 5;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider) private void load(OverlayColourProvider colourProvider)
@ -71,9 +72,10 @@ namespace osu.Game.Overlays.Chat
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Width = chatting_text_width, Width = chatting_text_width,
Masking = true, Masking = true,
Padding = new MarginPadding { Right = 5 }, Padding = new MarginPadding { Horizontal = padding },
Child = chattingText = new OsuSpriteText Child = chattingText = new OsuSpriteText
{ {
MaxWidth = chatting_text_width - padding * 2,
Font = OsuFont.Torus.With(size: 20), Font = OsuFont.Torus.With(size: 20),
Colour = colourProvider.Background1, Colour = colourProvider.Background1,
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
@ -97,7 +99,7 @@ namespace osu.Game.Overlays.Chat
new Container new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = 5 }, Padding = new MarginPadding { Right = padding },
Child = chatTextBox = new ChatTextBox Child = chatTextBox = new ChatTextBox
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,

View File

@ -57,6 +57,7 @@ namespace osu.Game.Overlays.Dialog
private Sample confirmSample; private Sample confirmSample;
private double lastTickPlaybackTime; private double lastTickPlaybackTime;
private AudioFilter lowPassFilter = null!; private AudioFilter lowPassFilter = null!;
private bool mouseDown;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio) private void load(AudioManager audio)
@ -73,6 +74,12 @@ namespace osu.Game.Overlays.Dialog
Progress.BindValueChanged(progressChanged); Progress.BindValueChanged(progressChanged);
} }
protected override void AbortConfirm()
{
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
base.AbortConfirm();
}
protected override void Confirm() protected override void Confirm()
{ {
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF); lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
@ -83,6 +90,7 @@ namespace osu.Game.Overlays.Dialog
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)
{ {
BeginConfirm(); BeginConfirm();
mouseDown = true;
return true; return true;
} }
@ -90,11 +98,28 @@ namespace osu.Game.Overlays.Dialog
{ {
if (!e.HasAnyButtonPressed) if (!e.HasAnyButtonPressed)
{ {
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
AbortConfirm(); AbortConfirm();
mouseDown = false;
} }
} }
protected override bool OnHover(HoverEvent e)
{
if (mouseDown)
BeginConfirm();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
if (!mouseDown) return;
AbortConfirm();
}
private void progressChanged(ValueChangedEvent<double> progress) private void progressChanged(ValueChangedEvent<double> progress)
{ {
if (progress.NewValue < progress.OldValue) return; if (progress.NewValue < progress.OldValue) return;

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -27,7 +28,14 @@ namespace osu.Game.Overlays.Mods
public Color4 AccentColour public Color4 AccentColour
{ {
get => headerBackground.Colour; get => headerBackground.Colour;
set => headerBackground.Colour = value; set
{
headerBackground.Colour = value;
var hsv = new Colour4(value.R, value.G, value.B, 1f).ToHSV();
var trianglesColour = Colour4.FromHSV(hsv.X, hsv.Y + 0.2f, hsv.Z - 0.1f);
triangles.Colour = ColourInfo.GradientVertical(trianglesColour, trianglesColour.MultiplyAlpha(0f));
}
} }
/// <summary> /// <summary>
@ -44,6 +52,7 @@ namespace osu.Game.Overlays.Mods
private readonly Box headerBackground; private readonly Box headerBackground;
private readonly Container contentContainer; private readonly Container contentContainer;
private readonly Box contentBackground; private readonly Box contentBackground;
private readonly TrianglesV2 triangles;
private const float header_height = 42; private const float header_height = 42;
@ -73,6 +82,13 @@ namespace osu.Game.Overlays.Mods
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = header_height + ModSelectPanel.CORNER_RADIUS Height = header_height + ModSelectPanel.CORNER_RADIUS
}, },
triangles = new TrianglesV2
{
RelativeSizeAxes = Axes.X,
Height = header_height,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
Velocity = 0.7f,
},
headerText = new OsuTextFlowContainer(t => headerText = new OsuTextFlowContainer(t =>
{ {
t.Font = OsuFont.TorusAlternate.With(size: 17); t.Font = OsuFont.TorusAlternate.With(size: 17);

View File

@ -49,7 +49,7 @@ namespace osu.Game.Overlays.Notifications
} }
} }
public string CompletionText { get; set; } = "Task has completed!"; public LocalisableString CompletionText { get; set; } = "Task has completed!";
private float progress; private float progress;

View File

@ -5,6 +5,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -21,25 +22,29 @@ using osuTK.Graphics;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
/// <summary> /// <summary>
/// <see cref="UserTrackingScrollContainer"/> which provides <see cref="ScrollToTopButton"/>. Mostly used in <see cref="FullscreenOverlay{T}"/>. /// <see cref="UserTrackingScrollContainer"/> which provides <see cref="ScrollBackButton"/>. Mostly used in <see cref="FullscreenOverlay{T}"/>.
/// </summary> /// </summary>
public partial class OverlayScrollContainer : UserTrackingScrollContainer public partial class OverlayScrollContainer : UserTrackingScrollContainer
{ {
/// <summary> /// <summary>
/// Scroll position at which the <see cref="ScrollToTopButton"/> will be shown. /// Scroll position at which the <see cref="ScrollBackButton"/> will be shown.
/// </summary> /// </summary>
private const int button_scroll_position = 200; private const int button_scroll_position = 200;
protected readonly ScrollToTopButton Button; protected ScrollBackButton Button;
public OverlayScrollContainer() private readonly Bindable<float?> lastScrollTarget = new Bindable<float?>();
[BackgroundDependencyLoader]
private void load()
{ {
AddInternal(Button = new ScrollToTopButton AddInternal(Button = new ScrollBackButton
{ {
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight, Origin = Anchor.BottomRight,
Margin = new MarginPadding(20), Margin = new MarginPadding(20),
Action = scrollToTop Action = scrollBack,
LastScrollTarget = { BindTarget = lastScrollTarget }
}); });
} }
@ -53,16 +58,31 @@ namespace osu.Game.Overlays
return; return;
} }
Button.State = Target > button_scroll_position ? Visibility.Visible : Visibility.Hidden; Button.State = Target > button_scroll_position || lastScrollTarget.Value != null ? Visibility.Visible : Visibility.Hidden;
} }
private void scrollToTop() protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default)
{ {
ScrollToStart(); base.OnUserScroll(value, animated, distanceDecay);
Button.State = Visibility.Hidden;
lastScrollTarget.Value = null;
} }
public partial class ScrollToTopButton : OsuHoverContainer private void scrollBack()
{
if (lastScrollTarget.Value == null)
{
lastScrollTarget.Value = Target;
ScrollToStart();
}
else
{
ScrollTo(lastScrollTarget.Value.Value);
lastScrollTarget.Value = null;
}
}
public partial class ScrollBackButton : OsuHoverContainer
{ {
private const int fade_duration = 500; private const int fade_duration = 500;
@ -88,8 +108,11 @@ namespace osu.Game.Overlays
private readonly Container content; private readonly Container content;
private readonly Box background; private readonly Box background;
private readonly SpriteIcon spriteIcon;
public ScrollToTopButton() public Bindable<float?> LastScrollTarget = new Bindable<float?>();
public ScrollBackButton()
: base(HoverSampleSet.ScrollToTop) : base(HoverSampleSet.ScrollToTop)
{ {
Size = new Vector2(50); Size = new Vector2(50);
@ -113,7 +136,7 @@ namespace osu.Game.Overlays
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}, },
new SpriteIcon spriteIcon = new SpriteIcon
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -134,6 +157,17 @@ namespace osu.Game.Overlays
flashColour = colourProvider.Light1; flashColour = colourProvider.Light1;
} }
protected override void LoadComplete()
{
base.LoadComplete();
LastScrollTarget.BindValueChanged(target =>
{
spriteIcon.RotateTo(target.NewValue != null ? 180 : 0, fade_duration, Easing.OutQuint);
TooltipText = target.NewValue != null ? CommonStrings.ButtonsBackToPrevious : CommonStrings.ButtonsBackToTop;
}, true);
}
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)
{ {
background.FlashColour(flashColour, 800, Easing.OutQuint); background.FlashColour(flashColour, 800, Easing.OutQuint);

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Input.Handlers.Tablet; using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -66,7 +67,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = colour.Gray1, Colour = colour.Gray1,
}, },
usableAreaContainer = new Container usableAreaContainer = new UsableAreaContainer(handler)
{ {
Origin = Anchor.Centre, Origin = Anchor.Centre,
Children = new Drawable[] Children = new Drawable[]
@ -225,4 +226,28 @@ namespace osu.Game.Overlays.Settings.Sections.Input
tabletContainer.Scale = new Vector2(1 / adjust); tabletContainer.Scale = new Vector2(1 / adjust);
} }
} }
public partial class UsableAreaContainer : Container
{
private readonly Bindable<Vector2> areaOffset;
public UsableAreaContainer(ITabletHandler tabletHandler)
{
areaOffset = tabletHandler.AreaOffset.GetBoundCopy();
}
protected override bool OnDragStart(DragStartEvent e) => true;
protected override void OnDrag(DragEvent e)
{
var newPos = Position + e.Delta;
this.MoveTo(Vector2.Clamp(newPos, Vector2.Zero, Parent.Size));
}
protected override void OnDragEnd(DragEndEvent e)
{
areaOffset.Value = Position;
base.OnDragEnd(e);
}
}
} }

View File

@ -0,0 +1,75 @@
// 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.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
using osuTK;
namespace osu.Game.Overlays.SkinEditor
{
public partial class NonSkinnableScreenPlaceholder : CompositeDrawable
{
[Resolved]
private SkinEditorOverlay? skinEditorOverlay { get; set; }
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Box
{
Colour = colourProvider.Dark6,
RelativeSizeAxes = Axes.Both,
Alpha = 0.95f,
},
new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0, 5),
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new SpriteIcon
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Icon = FontAwesome.Solid.ExclamationCircle,
Size = new Vector2(24),
Y = -5,
},
new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold, size: 18))
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
TextAnchor = Anchor.Centre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Text = "Please navigate to a skinnable screen using the scene library",
},
new RoundedButton
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Width = 200,
Margin = new MarginPadding { Top = 20 },
Action = () => skinEditorOverlay?.Hide(),
Text = "Return to game"
}
}
},
};
}
}
}

View File

@ -5,40 +5,53 @@ using System;
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.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Overlays.SkinEditor namespace osu.Game.Overlays.SkinEditor
{ {
public partial class SkinBlueprint : SelectionBlueprint<ISkinnableDrawable> public partial class SkinBlueprint : SelectionBlueprint<ISerialisableDrawable>
{ {
private Container box = null!; private Container box = null!;
private Container outlineBox = null!;
private AnchorOriginVisualiser anchorOriginVisualiser = null!; private AnchorOriginVisualiser anchorOriginVisualiser = null!;
private OsuSpriteText label = null!;
private Drawable drawable => (Drawable)Item; private Drawable drawable => (Drawable)Item;
protected override bool ShouldBeAlive => drawable.IsAlive && Item.IsPresent; protected override bool ShouldBeAlive => drawable.IsAlive && Item.IsPresent;
[Resolved] private Quad drawableQuad;
private OsuColour colours { get; set; } = null!;
public SkinBlueprint(ISkinnableDrawable component) public override Quad ScreenSpaceDrawQuad => drawableQuad;
public override Quad SelectionQuad => drawable.ScreenSpaceDrawQuad;
public override bool Contains(Vector2 screenSpacePos) => drawableQuad.Contains(screenSpacePos);
public override Vector2 ScreenSpaceSelectionPoint => drawable.ToScreenSpace(drawable.OriginPosition);
protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) =>
drawableQuad.Contains(screenSpacePos);
public SkinBlueprint(ISerialisableDrawable component)
: base(component) : base(component)
{ {
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(OsuColour colours)
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
@ -46,26 +59,30 @@ namespace osu.Game.Overlays.SkinEditor
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
outlineBox = new Container new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Masking = true, Masking = true,
BorderThickness = 3, CornerRadius = 3,
BorderColour = Color4.White, BorderThickness = SelectionBox.BORDER_RADIUS / 2,
BorderColour = ColourInfo.GradientVertical(colours.Pink4.Darken(0.4f), colours.Pink4),
Children = new Drawable[] Children = new Drawable[]
{ {
new Box new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Alpha = 0f, Blending = BlendingParameters.Additive,
Alpha = 0.2f,
Colour = ColourInfo.GradientVertical(colours.Pink2, colours.Pink4),
AlwaysPresent = true, AlwaysPresent = true,
}, },
} }
}, },
new OsuSpriteText label = new OsuSpriteText
{ {
Text = Item.GetType().Name, Text = Item.GetType().Name,
Font = OsuFont.Default.With(size: 10, weight: FontWeight.Bold), Font = OsuFont.Default.With(size: 10, weight: FontWeight.Bold),
Alpha = 0,
Anchor = Anchor.BottomRight, Anchor = Anchor.BottomRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
}, },
@ -83,7 +100,18 @@ namespace osu.Game.Overlays.SkinEditor
base.LoadComplete(); base.LoadComplete();
updateSelectedState(); updateSelectedState();
this.FadeInFromZero(200, Easing.OutQuint); }
protected override bool OnHover(HoverEvent e)
{
updateSelectedState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateSelectedState();
base.OnHoverLost(e);
} }
protected override void OnSelected() protected override void OnSelected()
@ -100,73 +128,73 @@ namespace osu.Game.Overlays.SkinEditor
private void updateSelectedState() private void updateSelectedState()
{ {
outlineBox.FadeColour(colours.Pink.Opacity(IsSelected ? 1 : 0.5f), 200, Easing.OutQuint);
outlineBox.Child.FadeTo(IsSelected ? 0.2f : 0, 200, Easing.OutQuint);
anchorOriginVisualiser.FadeTo(IsSelected ? 1 : 0, 200, Easing.OutQuint); anchorOriginVisualiser.FadeTo(IsSelected ? 1 : 0, 200, Easing.OutQuint);
label.FadeTo(IsSelected || IsHovered ? 1 : 0, 200, Easing.OutQuint);
} }
private Quad drawableQuad;
public override Quad ScreenSpaceDrawQuad => drawableQuad;
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
drawableQuad = drawable.ScreenSpaceDrawQuad; drawableQuad = drawable.ToScreenSpace(
var quad = ToLocalSpace(drawable.ScreenSpaceDrawQuad); drawable.DrawRectangle
.Inflate(SkinSelectionHandler.INFLATE_SIZE));
box.Position = drawable.ToSpaceOfOtherDrawable(Vector2.Zero, this); var localSpaceQuad = ToLocalSpace(drawableQuad);
box.Size = quad.Size;
box.Position = localSpaceQuad.TopLeft;
box.Size = localSpaceQuad.Size;
box.Rotation = drawable.Rotation; box.Rotation = drawable.Rotation;
box.Scale = new Vector2(MathF.Sign(drawable.Scale.X), MathF.Sign(drawable.Scale.Y)); box.Scale = new Vector2(MathF.Sign(drawable.Scale.X), MathF.Sign(drawable.Scale.Y));
} }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => drawable.ReceivePositionalInputAt(screenSpacePos);
public override Vector2 ScreenSpaceSelectionPoint => drawable.ToScreenSpace(drawable.OriginPosition);
public override Quad SelectionQuad => drawable.ScreenSpaceDrawQuad;
} }
internal partial class AnchorOriginVisualiser : CompositeDrawable internal partial class AnchorOriginVisualiser : CompositeDrawable
{ {
private readonly Drawable drawable; private readonly Drawable drawable;
private readonly Box originBox; private Drawable originBox = null!;
private readonly Box anchorBox; private Drawable anchorBox = null!;
private readonly Box anchorLine; private Drawable anchorLine = null!;
public AnchorOriginVisualiser(Drawable drawable) public AnchorOriginVisualiser(Drawable drawable)
{ {
this.drawable = drawable; this.drawable = drawable;
}
InternalChildren = new Drawable[] [BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Color4 anchorColour = colours.Red1;
Color4 originColour = colours.Red3;
InternalChildren = new[]
{ {
anchorLine = new Box anchorLine = new Circle
{ {
Height = 2, Height = 3f,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Colour = Color4.Yellow, Colour = ColourInfo.GradientHorizontal(originColour.Opacity(0.5f), originColour),
EdgeSmoothness = Vector2.One
}, },
originBox = new Box originBox = new Circle
{ {
Colour = Color4.Red, Colour = originColour,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(5), Size = new Vector2(7),
}, },
anchorBox = new Box anchorBox = new Circle
{ {
Colour = Color4.Red, Colour = anchorColour,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(5), Size = new Vector2(10),
}, },
}; };
} }
private Vector2? anchorPosition;
private Vector2? originPositionInDrawableSpace;
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
@ -174,8 +202,13 @@ namespace osu.Game.Overlays.SkinEditor
if (drawable.Parent == null) if (drawable.Parent == null)
return; return;
originBox.Position = drawable.ToSpaceOfOtherDrawable(drawable.OriginPosition, this); var newAnchor = drawable.Parent.ToSpaceOfOtherDrawable(drawable.AnchorPosition, this);
anchorBox.Position = drawable.Parent.ToSpaceOfOtherDrawable(drawable.AnchorPosition, this); anchorPosition = tweenPosition(anchorPosition ?? newAnchor, newAnchor);
anchorBox.Position = anchorPosition.Value;
// for the origin, tween in the drawable's local space to avoid unwanted tweening when the drawable is being dragged.
originPositionInDrawableSpace = originPositionInDrawableSpace != null ? tweenPosition(originPositionInDrawableSpace.Value, drawable.OriginPosition) : drawable.OriginPosition;
originBox.Position = drawable.ToSpaceOfOtherDrawable(originPositionInDrawableSpace.Value, this);
var point1 = ToLocalSpace(anchorBox.ScreenSpaceDrawQuad.Centre); var point1 = ToLocalSpace(anchorBox.ScreenSpaceDrawQuad.Centre);
var point2 = ToLocalSpace(originBox.ScreenSpaceDrawQuad.Centre); var point2 = ToLocalSpace(originBox.ScreenSpaceDrawQuad.Centre);
@ -184,5 +217,11 @@ namespace osu.Game.Overlays.SkinEditor
anchorLine.Width = (point2 - point1).Length; anchorLine.Width = (point2 - point1).Length;
anchorLine.Rotation = MathHelper.RadiansToDegrees(MathF.Atan2(point2.Y - point1.Y, point2.X - point1.X)); anchorLine.Rotation = MathHelper.RadiansToDegrees(MathF.Atan2(point2.Y - point1.Y, point2.X - point1.X));
} }
private Vector2 tweenPosition(Vector2 oldPosition, Vector2 newPosition)
=> new Vector2(
(float)Interpolation.DampContinuously(oldPosition.X, newPosition.X, 25, Clock.ElapsedFrameTime),
(float)Interpolation.DampContinuously(oldPosition.Y, newPosition.Y, 25, Clock.ElapsedFrameTime)
);
} }
} }

View File

@ -7,15 +7,7 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -24,18 +16,18 @@ using osuTK.Input;
namespace osu.Game.Overlays.SkinEditor namespace osu.Game.Overlays.SkinEditor
{ {
public partial class SkinBlueprintContainer : BlueprintContainer<ISkinnableDrawable> public partial class SkinBlueprintContainer : BlueprintContainer<ISerialisableDrawable>
{ {
private readonly Drawable target; private readonly ISerialisableDrawableContainer targetContainer;
private readonly List<BindableList<ISkinnableDrawable>> targetComponents = new List<BindableList<ISkinnableDrawable>>(); private readonly List<BindableList<ISerialisableDrawable>> targetComponents = new List<BindableList<ISerialisableDrawable>>();
[Resolved] [Resolved]
private SkinEditor editor { get; set; } = null!; private SkinEditor editor { get; set; } = null!;
public SkinBlueprintContainer(Drawable target) public SkinBlueprintContainer(ISerialisableDrawableContainer targetContainer)
{ {
this.target = target; this.targetContainer = targetContainer;
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -44,22 +36,10 @@ namespace osu.Game.Overlays.SkinEditor
SelectedItems.BindTo(editor.SelectedComponents); SelectedItems.BindTo(editor.SelectedComponents);
// track each target container on the current screen. var bindableList = new BindableList<ISerialisableDrawable> { BindTarget = targetContainer.Components };
var targetContainers = target.ChildrenOfType<ISkinnableTarget>().ToArray(); bindableList.BindCollectionChanged(componentsChanged, true);
if (targetContainers.Length == 0) targetComponents.Add(bindableList);
{
AddInternal(new NonSkinnableScreenPlaceholder());
return;
}
foreach (var targetContainer in targetContainers)
{
var bindableList = new BindableList<ISkinnableDrawable> { BindTarget = targetContainer.Components };
bindableList.BindCollectionChanged(componentsChanged, true);
targetComponents.Add(bindableList);
}
} }
private void componentsChanged(object? sender, NotifyCollectionChangedEventArgs e) => Schedule(() => private void componentsChanged(object? sender, NotifyCollectionChangedEventArgs e) => Schedule(() =>
@ -69,7 +49,7 @@ namespace osu.Game.Overlays.SkinEditor
case NotifyCollectionChangedAction.Add: case NotifyCollectionChangedAction.Add:
Debug.Assert(e.NewItems != null); Debug.Assert(e.NewItems != null);
foreach (var item in e.NewItems.Cast<ISkinnableDrawable>()) foreach (var item in e.NewItems.Cast<ISerialisableDrawable>())
AddBlueprintFor(item); AddBlueprintFor(item);
break; break;
@ -77,7 +57,7 @@ namespace osu.Game.Overlays.SkinEditor
case NotifyCollectionChangedAction.Reset: case NotifyCollectionChangedAction.Reset:
Debug.Assert(e.OldItems != null); Debug.Assert(e.OldItems != null);
foreach (var item in e.OldItems.Cast<ISkinnableDrawable>()) foreach (var item in e.OldItems.Cast<ISerialisableDrawable>())
RemoveBlueprintFor(item); RemoveBlueprintFor(item);
break; break;
@ -85,16 +65,16 @@ namespace osu.Game.Overlays.SkinEditor
Debug.Assert(e.NewItems != null); Debug.Assert(e.NewItems != null);
Debug.Assert(e.OldItems != null); Debug.Assert(e.OldItems != null);
foreach (var item in e.OldItems.Cast<ISkinnableDrawable>()) foreach (var item in e.OldItems.Cast<ISerialisableDrawable>())
RemoveBlueprintFor(item); RemoveBlueprintFor(item);
foreach (var item in e.NewItems.Cast<ISkinnableDrawable>()) foreach (var item in e.NewItems.Cast<ISerialisableDrawable>())
AddBlueprintFor(item); AddBlueprintFor(item);
break; break;
} }
}); });
protected override void AddBlueprintFor(ISkinnableDrawable item) protected override void AddBlueprintFor(ISerialisableDrawable item)
{ {
if (!item.IsEditable) if (!item.IsEditable)
return; return;
@ -145,12 +125,12 @@ namespace osu.Game.Overlays.SkinEditor
// convert to game space coordinates // convert to game space coordinates
delta = firstBlueprint.ToScreenSpace(delta) - firstBlueprint.ToScreenSpace(Vector2.Zero); delta = firstBlueprint.ToScreenSpace(delta) - firstBlueprint.ToScreenSpace(Vector2.Zero);
SelectionHandler.HandleMovement(new MoveSelectionEvent<ISkinnableDrawable>(firstBlueprint, delta)); SelectionHandler.HandleMovement(new MoveSelectionEvent<ISerialisableDrawable>(firstBlueprint, delta));
} }
protected override SelectionHandler<ISkinnableDrawable> CreateSelectionHandler() => new SkinSelectionHandler(); protected override SelectionHandler<ISerialisableDrawable> CreateSelectionHandler() => new SkinSelectionHandler();
protected override SelectionBlueprint<ISkinnableDrawable> CreateBlueprintFor(ISkinnableDrawable component) protected override SelectionBlueprint<ISerialisableDrawable> CreateBlueprintFor(ISerialisableDrawable component)
=> new SkinBlueprint(component); => new SkinBlueprint(component);
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
@ -160,65 +140,5 @@ namespace osu.Game.Overlays.SkinEditor
foreach (var list in targetComponents) foreach (var list in targetComponents)
list.UnbindAll(); list.UnbindAll();
} }
public partial class NonSkinnableScreenPlaceholder : CompositeDrawable
{
[Resolved]
private SkinEditorOverlay? skinEditorOverlay { get; set; }
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Box
{
Colour = colourProvider.Dark6,
RelativeSizeAxes = Axes.Both,
Alpha = 0.95f,
},
new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0, 5),
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new SpriteIcon
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Icon = FontAwesome.Solid.ExclamationCircle,
Size = new Vector2(24),
Y = -5,
},
new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold, size: 18))
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
TextAnchor = Anchor.Centre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Text = "Please navigate to a skinnable screen using the scene library",
},
new RoundedButton
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Width = 200,
Margin = new MarginPadding { Top = 20 },
Action = () => skinEditorOverlay?.Hide(),
Text = "Return to game"
}
}
},
};
}
}
} }
} }

View File

@ -2,17 +2,18 @@
// 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.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.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Graphics; using osu.Framework.Threading;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
@ -22,12 +23,12 @@ namespace osu.Game.Overlays.SkinEditor
{ {
public Action<Type>? RequestPlacement; public Action<Type>? RequestPlacement;
private readonly CompositeDrawable? target; private readonly SkinComponentsContainer? target;
private FillFlowContainer fill = null!; private FillFlowContainer fill = null!;
public SkinComponentToolbox(CompositeDrawable? target = null) public SkinComponentToolbox(SkinComponentsContainer? target = null)
: base(SkinEditorStrings.Components) : base(target?.Lookup.Ruleset == null ? SkinEditorStrings.Components : LocalisableString.Interpolate($"{SkinEditorStrings.Components} ({target.Lookup.Ruleset.Name})"))
{ {
this.target = target; this.target = target;
} }
@ -50,7 +51,7 @@ namespace osu.Game.Overlays.SkinEditor
{ {
fill.Clear(); fill.Clear();
var skinnableTypes = SkinnableInfo.GetAllAvailableDrawables(); var skinnableTypes = SerialisedDrawableInfo.GetAllAvailableDrawables(target?.Lookup.Ruleset);
foreach (var type in skinnableTypes) foreach (var type in skinnableTypes)
attemptAddComponent(type); attemptAddComponent(type);
} }
@ -61,11 +62,12 @@ namespace osu.Game.Overlays.SkinEditor
{ {
Drawable instance = (Drawable)Activator.CreateInstance(type)!; Drawable instance = (Drawable)Activator.CreateInstance(type)!;
if (!((ISkinnableDrawable)instance).IsEditable) return; if (!((ISerialisableDrawable)instance).IsEditable) return;
fill.Add(new ToolboxComponentButton(instance, target) fill.Add(new ToolboxComponentButton(instance, target)
{ {
RequestPlacement = t => RequestPlacement?.Invoke(t) RequestPlacement = t => RequestPlacement?.Invoke(t),
Expanding = contractOtherButtons,
}); });
} }
catch (DependencyNotRegisteredException) catch (DependencyNotRegisteredException)
@ -79,15 +81,29 @@ namespace osu.Game.Overlays.SkinEditor
} }
} }
private void contractOtherButtons(ToolboxComponentButton obj)
{
foreach (var b in fill.OfType<ToolboxComponentButton>())
{
if (b == obj)
continue;
b.Contract();
}
}
public partial class ToolboxComponentButton : OsuButton public partial class ToolboxComponentButton : OsuButton
{ {
public Action<Type>? RequestPlacement; public Action<Type>? RequestPlacement;
public Action<ToolboxComponentButton>? Expanding;
private readonly Drawable component; private readonly Drawable component;
private readonly CompositeDrawable? dependencySource; private readonly CompositeDrawable? dependencySource;
private Container innerContainer = null!; private Container innerContainer = null!;
private ScheduledDelegate? expandContractAction;
private const float contracted_size = 60; private const float contracted_size = 60;
private const float expanded_size = 120; private const float expanded_size = 120;
@ -102,20 +118,45 @@ namespace osu.Game.Overlays.SkinEditor
Height = contracted_size; Height = contracted_size;
} }
private const double animation_duration = 500;
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
this.Delay(300).ResizeHeightTo(expanded_size, 500, Easing.OutQuint); expandContractAction?.Cancel();
expandContractAction = Scheduler.AddDelayed(() =>
{
this.ResizeHeightTo(expanded_size, animation_duration, Easing.OutQuint);
Expanding?.Invoke(this);
}, 100);
return base.OnHover(e); return base.OnHover(e);
} }
protected override void OnHoverLost(HoverLostEvent e) protected override void OnHoverLost(HoverLostEvent e)
{ {
base.OnHoverLost(e); base.OnHoverLost(e);
this.ResizeHeightTo(contracted_size, 500, Easing.OutQuint);
expandContractAction?.Cancel();
// If no other component is selected for too long, force a contract.
// Otherwise we will generally contract when Contract() is called from outside.
expandContractAction = Scheduler.AddDelayed(Contract, 1000);
}
public void Contract()
{
// Cheap debouncing to avoid stacking animations.
// The only place this is nulled is at the end of this method.
if (expandContractAction == null)
return;
this.ResizeHeightTo(contracted_size, animation_duration, Easing.OutQuint);
expandContractAction?.Cancel();
expandContractAction = null;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider, OsuColour colours) private void load(OverlayColourProvider colourProvider)
{ {
BackgroundColour = colourProvider.Background3; BackgroundColour = colourProvider.Background3;

View File

@ -7,6 +7,7 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -24,6 +25,7 @@ using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Overlays.OSD; using osu.Game.Overlays.OSD;
using osu.Game.Overlays.Settings;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Edit.Components.Menus; using osu.Game.Screens.Edit.Components.Menus;
@ -38,7 +40,7 @@ namespace osu.Game.Overlays.SkinEditor
public const float MENU_HEIGHT = 40; public const float MENU_HEIGHT = 40;
public readonly BindableList<ISkinnableDrawable> SelectedComponents = new BindableList<ISkinnableDrawable>(); public readonly BindableList<ISerialisableDrawable> SelectedComponents = new BindableList<ISerialisableDrawable>();
protected override bool StartHidden => true; protected override bool StartHidden => true;
@ -60,12 +62,17 @@ namespace osu.Game.Overlays.SkinEditor
[Resolved] [Resolved]
private RealmAccess realm { get; set; } = null!; private RealmAccess realm { get; set; } = null!;
[Resolved]
private EditorClipboard clipboard { get; set; } = null!;
[Resolved] [Resolved]
private SkinEditorOverlay? skinEditorOverlay { get; set; } private SkinEditorOverlay? skinEditorOverlay { get; set; }
[Cached] [Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
private readonly Bindable<SkinComponentsContainerLookup?> selectedTarget = new Bindable<SkinComponentsContainerLookup?>();
private bool hasBegunMutating; private bool hasBegunMutating;
private Container? content; private Container? content;
@ -78,6 +85,15 @@ namespace osu.Game.Overlays.SkinEditor
private EditorMenuItem undoMenuItem = null!; private EditorMenuItem undoMenuItem = null!;
private EditorMenuItem redoMenuItem = null!; private EditorMenuItem redoMenuItem = null!;
private EditorMenuItem cutMenuItem = null!;
private EditorMenuItem copyMenuItem = null!;
private EditorMenuItem cloneMenuItem = null!;
private EditorMenuItem pasteMenuItem = null!;
private readonly BindableWithCurrent<bool> canCut = new BindableWithCurrent<bool>();
private readonly BindableWithCurrent<bool> canCopy = new BindableWithCurrent<bool>();
private readonly BindableWithCurrent<bool> canPaste = new BindableWithCurrent<bool>();
[Resolved] [Resolved]
private OnScreenDisplay? onScreenDisplay { get; set; } private OnScreenDisplay? onScreenDisplay { get; set; }
@ -143,6 +159,11 @@ namespace osu.Game.Overlays.SkinEditor
{ {
undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo), undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo),
redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo), redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo),
new EditorMenuItemSpacer(),
cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut),
copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy),
pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste),
cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone),
} }
}, },
} }
@ -201,6 +222,21 @@ namespace osu.Game.Overlays.SkinEditor
{ {
base.LoadComplete(); base.LoadComplete();
canCut.Current.BindValueChanged(cut => cutMenuItem.Action.Disabled = !cut.NewValue, true);
canCopy.Current.BindValueChanged(copy =>
{
copyMenuItem.Action.Disabled = !copy.NewValue;
cloneMenuItem.Action.Disabled = !copy.NewValue;
}, true);
canPaste.Current.BindValueChanged(paste => pasteMenuItem.Action.Disabled = !paste.NewValue, true);
SelectedComponents.BindCollectionChanged((_, _) =>
{
canCopy.Value = canCut.Value = SelectedComponents.Any();
}, true);
clipboard.Content.BindValueChanged(content => canPaste.Value = !string.IsNullOrEmpty(content.NewValue), true);
Show(); Show();
game?.RegisterImportHandler(this); game?.RegisterImportHandler(this);
@ -218,12 +254,26 @@ namespace osu.Game.Overlays.SkinEditor
}, true); }, true);
SelectedComponents.BindCollectionChanged((_, _) => Scheduler.AddOnce(populateSettings), true); SelectedComponents.BindCollectionChanged((_, _) => Scheduler.AddOnce(populateSettings), true);
selectedTarget.BindValueChanged(targetChanged, true);
} }
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e) public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
{ {
switch (e.Action) switch (e.Action)
{ {
case PlatformAction.Cut:
Cut();
return true;
case PlatformAction.Copy:
Copy();
return true;
case PlatformAction.Paste:
Paste();
return true;
case PlatformAction.Undo: case PlatformAction.Undo:
Undo(); Undo();
return true; return true;
@ -253,28 +303,83 @@ namespace osu.Game.Overlays.SkinEditor
changeHandler?.Dispose(); changeHandler?.Dispose();
SelectedComponents.Clear();
// Immediately clear the previous blueprint container to ensure it doesn't try to interact with the old target. // Immediately clear the previous blueprint container to ensure it doesn't try to interact with the old target.
content?.Clear(); if (content?.Child is SkinBlueprintContainer)
content.Clear();
Scheduler.AddOnce(loadBlueprintContainer); Scheduler.AddOnce(loadBlueprintContainer);
Scheduler.AddOnce(populateSettings); Scheduler.AddOnce(populateSettings);
void loadBlueprintContainer() void loadBlueprintContainer()
{ {
Debug.Assert(content != null); selectedTarget.Default = getFirstTarget()?.Lookup;
changeHandler = new SkinEditorChangeHandler(targetScreen); if (!availableTargets.Any(t => t.Lookup.Equals(selectedTarget.Value)))
changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true); selectedTarget.SetDefault();
changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true); }
}
content.Child = new SkinBlueprintContainer(targetScreen); private void targetChanged(ValueChangedEvent<SkinComponentsContainerLookup?> target)
{
foreach (var toolbox in componentsSidebar.OfType<SkinComponentToolbox>())
toolbox.Expire();
componentsSidebar.Child = new SkinComponentToolbox(getFirstTarget() as CompositeDrawable) componentsSidebar.Clear();
SelectedComponents.Clear();
Debug.Assert(content != null);
var skinComponentsContainer = getTarget(target.NewValue);
if (target.NewValue == null || skinComponentsContainer == null)
{
content.Child = new NonSkinnableScreenPlaceholder();
return;
}
changeHandler = new SkinEditorChangeHandler(skinComponentsContainer);
changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
content.Child = new SkinBlueprintContainer(skinComponentsContainer);
componentsSidebar.Children = new[]
{
new EditorSidebarSection("Current working layer")
{ {
RequestPlacement = placeComponent Children = new Drawable[]
}; {
new SettingsDropdown<SkinComponentsContainerLookup?>
{
Items = availableTargets.Select(t => t.Lookup),
Current = selectedTarget,
}
}
},
};
// If the new target has a ruleset, let's show ruleset-specific items at the top, and the rest below.
if (target.NewValue.Ruleset != null)
{
componentsSidebar.Add(new SkinComponentToolbox(skinComponentsContainer)
{
RequestPlacement = requestPlacement
});
}
// Remove the ruleset from the lookup to get base components.
componentsSidebar.Add(new SkinComponentToolbox(getTarget(new SkinComponentsContainerLookup(target.NewValue.Target)))
{
RequestPlacement = requestPlacement
});
void requestPlacement(Type type)
{
if (!(Activator.CreateInstance(type) is ISerialisableDrawable component))
throw new InvalidOperationException($"Attempted to instantiate a component for placement which was not an {typeof(ISerialisableDrawable)}.");
SelectedComponents.Clear();
placeComponent(component);
} }
} }
@ -300,20 +405,18 @@ namespace osu.Game.Overlays.SkinEditor
hasBegunMutating = true; hasBegunMutating = true;
} }
private void placeComponent(Type type) /// <summary>
/// Attempt to place a given component in the current target. If successful, the new component will be added to <see cref="SelectedComponents"/>.
/// </summary>
/// <param name="component">The component to be placed.</param>
/// <param name="applyDefaults">Whether to apply default anchor / origin / position values.</param>
/// <returns>Whether placement succeeded. Could fail if no target is available, or if the current target has missing dependency requirements for the component.</returns>
private bool placeComponent(ISerialisableDrawable component, bool applyDefaults = true)
{ {
if (!(Activator.CreateInstance(type) is ISkinnableDrawable component)) var targetContainer = getTarget(selectedTarget.Value);
throw new InvalidOperationException($"Attempted to instantiate a component for placement which was not an {typeof(ISkinnableDrawable)}.");
placeComponent(component);
}
private void placeComponent(ISkinnableDrawable component, bool applyDefaults = true)
{
var targetContainer = getFirstTarget();
if (targetContainer == null) if (targetContainer == null)
return; return false;
var drawableComponent = (Drawable)component; var drawableComponent = (Drawable)component;
@ -325,10 +428,18 @@ namespace osu.Game.Overlays.SkinEditor
drawableComponent.Y = targetContainer.DrawSize.Y / 2; drawableComponent.Y = targetContainer.DrawSize.Y / 2;
} }
targetContainer.Add(component); try
{
targetContainer.Add(component);
}
catch
{
// May fail if dependencies are not available, for instance.
return false;
}
SelectedComponents.Clear();
SelectedComponents.Add(component); SelectedComponents.Add(component);
return true;
} }
private void populateSettings() private void populateSettings()
@ -339,28 +450,70 @@ namespace osu.Game.Overlays.SkinEditor
settingsSidebar.Add(new SkinSettingsToolbox(component)); settingsSidebar.Add(new SkinSettingsToolbox(component));
} }
private IEnumerable<ISkinnableTarget> availableTargets => targetScreen.ChildrenOfType<ISkinnableTarget>(); private IEnumerable<SkinComponentsContainer> availableTargets => targetScreen.ChildrenOfType<SkinComponentsContainer>();
private ISkinnableTarget? getFirstTarget() => availableTargets.FirstOrDefault(); private SkinComponentsContainer? getFirstTarget() => availableTargets.FirstOrDefault();
private ISkinnableTarget? getTarget(GlobalSkinComponentLookup.LookupType target) private SkinComponentsContainer? getTarget(SkinComponentsContainerLookup? target)
{ {
return availableTargets.FirstOrDefault(c => c.Target == target); return availableTargets.FirstOrDefault(c => c.Lookup.Equals(target));
} }
private void revert() private void revert()
{ {
ISkinnableTarget[] targetContainers = availableTargets.ToArray(); SkinComponentsContainer[] targetContainers = availableTargets.ToArray();
foreach (var t in targetContainers) foreach (var t in targetContainers)
{ {
currentSkin.Value.ResetDrawableTarget(t); currentSkin.Value.ResetDrawableTarget(t);
// add back default components // add back default components
getTarget(t.Target)?.Reload(); getTarget(t.Lookup)?.Reload();
} }
} }
protected void Cut()
{
Copy();
DeleteItems(SelectedComponents.ToArray());
}
protected void Copy()
{
clipboard.Content.Value = JsonConvert.SerializeObject(SelectedComponents.Cast<Drawable>().Select(s => s.CreateSerialisedInfo()).ToArray());
}
protected void Clone()
{
// Avoid attempting to clone if copying is not available (as it may result in pasting something unexpected).
if (!canCopy.Value)
return;
Copy();
Paste();
}
protected void Paste()
{
changeHandler?.BeginChange();
var drawableInfo = JsonConvert.DeserializeObject<SerialisedDrawableInfo[]>(clipboard.Content.Value);
if (drawableInfo == null)
return;
var instances = drawableInfo.Select(d => d.CreateInstance())
.OfType<ISerialisableDrawable>()
.ToArray();
SelectedComponents.Clear();
foreach (var i in instances)
placeComponent(i, false);
changeHandler?.EndChange();
}
protected void Undo() => changeHandler?.RestoreState(-1); protected void Undo() => changeHandler?.RestoreState(-1);
protected void Redo() => changeHandler?.RestoreState(1); protected void Redo() => changeHandler?.RestoreState(1);
@ -370,7 +523,7 @@ namespace osu.Game.Overlays.SkinEditor
if (!hasBegunMutating) if (!hasBegunMutating)
return; return;
ISkinnableTarget[] targetContainers = availableTargets.ToArray(); SkinComponentsContainer[] targetContainers = availableTargets.ToArray();
foreach (var t in targetContainers) foreach (var t in targetContainers)
currentSkin.Value.UpdateDrawableTarget(t); currentSkin.Value.UpdateDrawableTarget(t);
@ -400,10 +553,57 @@ namespace osu.Game.Overlays.SkinEditor
this.FadeOut(TRANSITION_DURATION, Easing.OutQuint); this.FadeOut(TRANSITION_DURATION, Easing.OutQuint);
} }
public void DeleteItems(ISkinnableDrawable[] items) public void DeleteItems(ISerialisableDrawable[] items)
{ {
changeHandler?.BeginChange();
foreach (var item in items) foreach (var item in items)
availableTargets.FirstOrDefault(t => t.Components.Contains(item))?.Remove(item); availableTargets.FirstOrDefault(t => t.Components.Contains(item))?.Remove(item, true);
changeHandler?.EndChange();
}
public void BringSelectionToFront()
{
if (getTarget(selectedTarget.Value) is not SkinComponentsContainer target)
return;
changeHandler?.BeginChange();
// Iterating by target components order ensures we maintain the same order across selected components, regardless
// of the order they were selected in.
foreach (var d in target.Components.ToArray())
{
if (!SelectedComponents.Contains(d))
continue;
target.Remove(d, false);
// Selection would be reset by the remove.
SelectedComponents.Add(d);
target.Add(d);
}
changeHandler?.EndChange();
}
public void SendSelectionToBack()
{
if (getTarget(selectedTarget.Value) is not SkinComponentsContainer target)
return;
changeHandler?.BeginChange();
foreach (var d in target.Components.ToArray())
{
if (SelectedComponents.Contains(d))
continue;
target.Remove(d, false);
target.Add(d);
}
changeHandler?.EndChange();
} }
#region Drag & drop import handling #region Drag & drop import handling
@ -440,6 +640,7 @@ namespace osu.Game.Overlays.SkinEditor
Position = skinnableTarget.ToLocalSpace(GetContainingInputManager().CurrentState.Mouse.Position), Position = skinnableTarget.ToLocalSpace(GetContainingInputManager().CurrentState.Mouse.Position),
}; };
SelectedComponents.Clear();
placeComponent(sprite, false); placeComponent(sprite, false);
SkinSelectionHandler.ApplyClosestAnchor(sprite); SkinSelectionHandler.ApplyClosestAnchor(sprite);

View File

@ -9,19 +9,17 @@ using Newtonsoft.Json;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Extensions;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Overlays.SkinEditor namespace osu.Game.Overlays.SkinEditor
{ {
public partial class SkinEditorChangeHandler : EditorChangeHandler public partial class SkinEditorChangeHandler : EditorChangeHandler
{ {
private readonly ISkinnableTarget? firstTarget; private readonly ISerialisableDrawableContainer? firstTarget;
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
private readonly BindableList<ISkinnableDrawable>? components; private readonly BindableList<ISerialisableDrawable>? components;
public SkinEditorChangeHandler(Drawable targetScreen) public SkinEditorChangeHandler(Drawable targetScreen)
{ {
@ -29,12 +27,12 @@ namespace osu.Game.Overlays.SkinEditor
// In the future we'll want this to cover all changes, even to skin's `InstantiationInfo`. // In the future we'll want this to cover all changes, even to skin's `InstantiationInfo`.
// We'll also need to consider cases where multiple targets are on screen at the same time. // We'll also need to consider cases where multiple targets are on screen at the same time.
firstTarget = targetScreen.ChildrenOfType<ISkinnableTarget>().FirstOrDefault(); firstTarget = targetScreen.ChildrenOfType<ISerialisableDrawableContainer>().FirstOrDefault();
if (firstTarget == null) if (firstTarget == null)
return; return;
components = new BindableList<ISkinnableDrawable> { BindTarget = firstTarget.Components }; components = new BindableList<ISerialisableDrawable> { BindTarget = firstTarget.Components };
components.BindCollectionChanged((_, _) => SaveState()); components.BindCollectionChanged((_, _) => SaveState());
} }
@ -43,7 +41,7 @@ namespace osu.Game.Overlays.SkinEditor
if (firstTarget == null) if (firstTarget == null)
return; return;
var skinnableInfos = firstTarget.CreateSkinnableInfo().ToArray(); var skinnableInfos = firstTarget.CreateSerialisedInfo().ToArray();
string json = JsonConvert.SerializeObject(skinnableInfos, new JsonSerializerSettings { Formatting = Formatting.Indented }); string json = JsonConvert.SerializeObject(skinnableInfos, new JsonSerializerSettings { Formatting = Formatting.Indented });
stream.Write(Encoding.UTF8.GetBytes(json)); stream.Write(Encoding.UTF8.GetBytes(json));
} }
@ -53,12 +51,12 @@ namespace osu.Game.Overlays.SkinEditor
if (firstTarget == null) if (firstTarget == null)
return; return;
var deserializedContent = JsonConvert.DeserializeObject<IEnumerable<SkinnableInfo>>(Encoding.UTF8.GetString(newState)); var deserializedContent = JsonConvert.DeserializeObject<IEnumerable<SerialisedDrawableInfo>>(Encoding.UTF8.GetString(newState));
if (deserializedContent == null) if (deserializedContent == null)
return; return;
SkinnableInfo[] skinnableInfo = deserializedContent.ToArray(); SerialisedDrawableInfo[] skinnableInfo = deserializedContent.ToArray();
Drawable[] targetComponents = firstTarget.Components.OfType<Drawable>().ToArray(); Drawable[] targetComponents = firstTarget.Components.OfType<Drawable>().ToArray();
if (!skinnableInfo.Select(s => s.Type).SequenceEqual(targetComponents.Select(d => d.GetType()))) if (!skinnableInfo.Select(s => s.Type).SequenceEqual(targetComponents.Select(d => d.GetType())))
@ -71,7 +69,7 @@ namespace osu.Game.Overlays.SkinEditor
int i = 0; int i = 0;
foreach (var drawable in targetComponents) foreach (var drawable in targetComponents)
drawable.ApplySkinnableInfo(skinnableInfo[i++]); drawable.ApplySerialisedInfo(skinnableInfo[i++]);
} }
} }
} }

View File

@ -11,6 +11,7 @@ using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Screens; using osu.Game.Screens;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components;
using osuTK; using osuTK;
@ -28,6 +29,9 @@ namespace osu.Game.Overlays.SkinEditor
private SkinEditor? skinEditor; private SkinEditor? skinEditor;
[Cached]
public readonly EditorClipboard Clipboard = new EditorClipboard();
[Resolved] [Resolved]
private OsuGame game { get; set; } = null!; private OsuGame game { get; set; } = null!;

View File

@ -13,13 +13,14 @@ using osu.Framework.Utils;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
namespace osu.Game.Overlays.SkinEditor namespace osu.Game.Overlays.SkinEditor
{ {
public partial class SkinSelectionHandler : SelectionHandler<ISkinnableDrawable> public partial class SkinSelectionHandler : SelectionHandler<ISerialisableDrawable>
{ {
[Resolved] [Resolved]
private SkinEditor skinEditor { get; set; } = null!; private SkinEditor skinEditor { get; set; } = null!;
@ -147,7 +148,7 @@ namespace osu.Game.Overlays.SkinEditor
return true; return true;
} }
public override bool HandleMovement(MoveSelectionEvent<ISkinnableDrawable> moveEvent) public override bool HandleMovement(MoveSelectionEvent<ISerialisableDrawable> moveEvent)
{ {
foreach (var c in SelectedBlueprints) foreach (var c in SelectedBlueprints)
{ {
@ -178,10 +179,10 @@ namespace osu.Game.Overlays.SkinEditor
SelectionBox.CanReverse = false; SelectionBox.CanReverse = false;
} }
protected override void DeleteItems(IEnumerable<ISkinnableDrawable> items) => protected override void DeleteItems(IEnumerable<ISerialisableDrawable> items) =>
skinEditor.DeleteItems(items.ToArray()); skinEditor.DeleteItems(items.ToArray());
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<ISkinnableDrawable>> selection) protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<ISerialisableDrawable>> selection)
{ {
var closestItem = new TernaryStateRadioMenuItem("Closest", MenuItemType.Standard, _ => applyClosestAnchors()) var closestItem = new TernaryStateRadioMenuItem("Closest", MenuItemType.Standard, _ => applyClosestAnchors())
{ {
@ -206,10 +207,18 @@ namespace osu.Game.Overlays.SkinEditor
((Drawable)blueprint.Item).Position = Vector2.Zero; ((Drawable)blueprint.Item).Position = Vector2.Zero;
}); });
yield return new EditorMenuItemSpacer();
yield return new OsuMenuItem("Bring to front", MenuItemType.Standard, () => skinEditor.BringSelectionToFront());
yield return new OsuMenuItem("Send to back", MenuItemType.Standard, () => skinEditor.SendSelectionToBack());
yield return new EditorMenuItemSpacer();
foreach (var item in base.GetContextMenuItemsForSelection(selection)) foreach (var item in base.GetContextMenuItemsForSelection(selection))
yield return item; yield return item;
IEnumerable<TernaryStateMenuItem> createAnchorItems(Func<ISkinnableDrawable, Anchor, bool> checkFunction, Action<Anchor> applyFunction) IEnumerable<TernaryStateMenuItem> createAnchorItems(Func<ISerialisableDrawable, Anchor, bool> checkFunction, Action<Anchor> applyFunction)
{ {
var displayableAnchors = new[] var displayableAnchors = new[]
{ {

View File

@ -17,6 +17,7 @@ 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.Configuration;
using osu.Game.Overlays; 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;
@ -70,6 +71,7 @@ namespace osu.Game.Rulesets.Edit
private FillFlowContainer togglesCollection; private FillFlowContainer togglesCollection;
private IBindable<bool> hasTiming; private IBindable<bool> hasTiming;
private Bindable<bool> autoSeekOnPlacement;
protected HitObjectComposer(Ruleset ruleset) protected HitObjectComposer(Ruleset ruleset)
: base(ruleset) : base(ruleset)
@ -80,8 +82,10 @@ namespace osu.Game.Rulesets.Edit
dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider) private void load(OverlayColourProvider colourProvider, OsuConfigManager config)
{ {
autoSeekOnPlacement = config.GetBindable<bool>(OsuSetting.EditorAutoSeekOnPlacement);
Config = Dependencies.Get<IRulesetConfigCache>().GetConfigFor(Ruleset); Config = Dependencies.Get<IRulesetConfigCache>().GetConfigFor(Ruleset);
try try
@ -365,7 +369,7 @@ namespace osu.Game.Rulesets.Edit
{ {
EditorBeatmap.Add(hitObject); EditorBeatmap.Add(hitObject);
if (EditorClock.CurrentTime < hitObject.StartTime) if (autoSeekOnPlacement.Value && EditorClock.CurrentTime < hitObject.StartTime)
EditorClock.SeekSmoothlyTo(hitObject.StartTime); EditorClock.SeekSmoothlyTo(hitObject.StartTime);
} }
} }

View File

@ -3,6 +3,7 @@
#nullable disable #nullable disable
using System;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -33,16 +34,30 @@ namespace osu.Game.Rulesets.Judgements
public readonly Judgement Judgement; public readonly Judgement Judgement;
/// <summary> /// <summary>
/// The offset from a perfect hit at which this <see cref="JudgementResult"/> occurred. /// The time at which this <see cref="JudgementResult"/> occurred.
/// Populated when this <see cref="JudgementResult"/> is applied via <see cref="DrawableHitObject.ApplyResult"/>. /// Populated when this <see cref="JudgementResult"/> is applied via <see cref="DrawableHitObject.ApplyResult"/>.
/// </summary> /// </summary>
public double TimeOffset { get; internal set; } /// <remarks>
/// This is used instead of <see cref="TimeAbsolute"/> to check whether this <see cref="JudgementResult"/> should be reverted.
/// </remarks>
internal double? RawTime { get; set; }
/// <summary> /// <summary>
/// The absolute time at which this <see cref="JudgementResult"/> occurred. /// The offset of <see cref="TimeAbsolute"/> from the end time of <see cref="HitObject"/>, clamped by <see cref="osu.Game.Rulesets.Objects.HitObject.MaximumJudgementOffset"/>.
/// Equal to the (end) time of the <see cref="HitObject"/> + <see cref="TimeOffset"/>.
/// </summary> /// </summary>
public double TimeAbsolute => HitObject.GetEndTime() + TimeOffset; public double TimeOffset
{
get => RawTime != null ? Math.Min(RawTime.Value - HitObject.GetEndTime(), HitObject.MaximumJudgementOffset) : 0;
internal set => RawTime = HitObject.GetEndTime() + value;
}
/// <summary>
/// The absolute time at which this <see cref="JudgementResult"/> occurred, clamped by the end time of <see cref="HitObject"/> plus <see cref="osu.Game.Rulesets.Objects.HitObject.MaximumJudgementOffset"/>.
/// </summary>
/// <remarks>
/// The end time of <see cref="HitObject"/> is returned if this result is not populated yet.
/// </remarks>
public double TimeAbsolute => RawTime != null ? Math.Min(RawTime.Value, HitObject.GetEndTime() + HitObject.MaximumJudgementOffset) : HitObject.GetEndTime();
/// <summary> /// <summary>
/// The combo prior to this <see cref="JudgementResult"/> occurring. /// The combo prior to this <see cref="JudgementResult"/> occurring.
@ -83,6 +98,13 @@ namespace osu.Game.Rulesets.Judgements
{ {
HitObject = hitObject; HitObject = hitObject;
Judgement = judgement; Judgement = judgement;
Reset();
}
internal void Reset()
{
Type = HitResult.None;
RawTime = null;
} }
public override string ToString() => $"{Type} (Score:{Judgement.NumericResultFor(this)} HP:{Judgement.HealthIncreaseFor(this)} {Judgement})"; public override string ToString() => $"{Type} (Score:{Judgement.NumericResultFor(this)} HP:{Judgement.HealthIncreaseFor(this)} {Judgement})";

View File

@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Mods
flashlight.Colour = Color4.Black; flashlight.Colour = Color4.Black;
flashlight.Combo.BindTo(Combo); flashlight.Combo.BindTo(Combo);
drawableRuleset.KeyBindingInputManager.Add(flashlight); drawableRuleset.Overlays.Add(flashlight);
} }
protected abstract Flashlight CreateFlashlight(); protected abstract Flashlight CreateFlashlight();

View File

@ -67,7 +67,8 @@ namespace osu.Game.Rulesets.Mods
{ {
MetronomeBeat metronomeBeat; MetronomeBeat metronomeBeat;
drawableRuleset.Overlays.Add(metronomeBeat = new MetronomeBeat(drawableRuleset.Beatmap.HitObjects.First().StartTime)); // Importantly, this is added to FrameStableComponents and not Overlays as the latter would cause it to be self-muted by the mod's volume adjustment.
drawableRuleset.FrameStableComponents.Add(metronomeBeat = new MetronomeBeat(drawableRuleset.Beatmap.HitObjects.First().StartTime));
metronomeBeat.AddAdjustment(AdjustableProperty.Volume, metronomeVolumeAdjust); metronomeBeat.AddAdjustment(AdjustableProperty.Volume, metronomeVolumeAdjust);
} }

View File

@ -82,6 +82,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// <summary> /// <summary>
/// Invoked by this or a nested <see cref="DrawableHitObject"/> prior to a <see cref="JudgementResult"/> being reverted. /// Invoked by this or a nested <see cref="DrawableHitObject"/> prior to a <see cref="JudgementResult"/> being reverted.
/// </summary> /// </summary>
/// <remarks>
/// This is only invoked if this <see cref="DrawableHitObject"/> is alive when the result is reverted.
/// </remarks>
public event Action<DrawableHitObject, JudgementResult> OnRevertResult; public event Action<DrawableHitObject, JudgementResult> OnRevertResult;
/// <summary> /// <summary>
@ -222,6 +225,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
ensureEntryHasResult(); ensureEntryHasResult();
entry.RevertResult += onRevertResult;
foreach (var h in HitObject.NestedHitObjects) foreach (var h in HitObject.NestedHitObjects)
{ {
var pooledDrawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h, this); var pooledDrawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h, this);
@ -234,7 +239,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
OnNestedDrawableCreated?.Invoke(drawableNested); OnNestedDrawableCreated?.Invoke(drawableNested);
drawableNested.OnNewResult += onNewResult; drawableNested.OnNewResult += onNewResult;
drawableNested.OnRevertResult += onRevertResult;
drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState; drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState;
// This is only necessary for non-pooled DHOs. For pooled DHOs, this is handled inside GetPooledDrawableRepresentation(). // This is only necessary for non-pooled DHOs. For pooled DHOs, this is handled inside GetPooledDrawableRepresentation().
@ -308,7 +312,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
foreach (var obj in nestedHitObjects) foreach (var obj in nestedHitObjects)
{ {
obj.OnNewResult -= onNewResult; obj.OnNewResult -= onNewResult;
obj.OnRevertResult -= onRevertResult;
obj.ApplyCustomUpdateState -= onApplyCustomUpdateState; obj.ApplyCustomUpdateState -= onApplyCustomUpdateState;
} }
@ -317,6 +320,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
HitObject.DefaultsApplied -= onDefaultsApplied; HitObject.DefaultsApplied -= onDefaultsApplied;
entry.RevertResult -= onRevertResult;
OnFree(); OnFree();
ParentHitObject = null; ParentHitObject = null;
@ -365,7 +370,11 @@ namespace osu.Game.Rulesets.Objects.Drawables
private void onNewResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnNewResult?.Invoke(drawableHitObject, result); private void onNewResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnNewResult?.Invoke(drawableHitObject, result);
private void onRevertResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnRevertResult?.Invoke(drawableHitObject, result); private void onRevertResult()
{
updateState(ArmedState.Idle);
OnRevertResult?.Invoke(this, Result);
}
private void onApplyCustomUpdateState(DrawableHitObject drawableHitObject, ArmedState state) => ApplyCustomUpdateState?.Invoke(drawableHitObject, state); private void onApplyCustomUpdateState(DrawableHitObject drawableHitObject, ArmedState state) => ApplyCustomUpdateState?.Invoke(drawableHitObject, state);
@ -577,26 +586,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
#endregion #endregion
protected override void Update()
{
base.Update();
if (Result != null && Result.HasResult)
{
double endTime = HitObject.GetEndTime();
if (Result.TimeOffset + endTime > Time.Current)
{
OnRevertResult?.Invoke(this, Result);
Result.TimeOffset = 0;
Result.Type = HitResult.None;
updateState(ArmedState.Idle);
}
}
}
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false; public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => false;
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()
@ -650,27 +639,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
UpdateResult(false); UpdateResult(false);
} }
/// <summary>
/// The maximum offset from the end time of <see cref="HitObject"/> at which this <see cref="DrawableHitObject"/> can be judged.
/// The time offset of <see cref="Result"/> will be clamped to this value during <see cref="ApplyResult"/>.
/// <para>
/// Defaults to the miss window of <see cref="HitObject"/>.
/// </para>
/// </summary>
/// <remarks>
/// This does not affect the time offset provided to invocations of <see cref="CheckForResult"/>.
/// </remarks>
public virtual double MaximumJudgementOffset => HitObject.HitWindows?.WindowFor(HitResult.Miss) ?? 0;
/// <summary>
/// Whether the location of the hit should be snapped to the hit target before animating.
/// </summary>
/// <remarks>
/// This is how osu-stable worked, but notably is not how TnT works.
/// It results in less visual feedback on hit accuracy.
/// </remarks>
public bool SnapJudgementLocation { get; set; }
/// <summary> /// <summary>
/// Applies the <see cref="Result"/> of this <see cref="DrawableHitObject"/>, notifying responders such as /// Applies the <see cref="Result"/> of this <see cref="DrawableHitObject"/>, notifying responders such as
/// the <see cref="ScoreProcessor"/> of the <see cref="JudgementResult"/>. /// the <see cref="ScoreProcessor"/> of the <see cref="JudgementResult"/>.
@ -692,7 +660,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
$"{GetType().ReadableName()} applied an invalid hit result (was: {Result.Type}, expected: [{Result.Judgement.MinResult} ... {Result.Judgement.MaxResult}])."); $"{GetType().ReadableName()} applied an invalid hit result (was: {Result.Type}, expected: [{Result.Judgement.MinResult} ... {Result.Judgement.MaxResult}]).");
} }
Result.TimeOffset = Math.Min(MaximumJudgementOffset, Time.Current - HitObject.GetEndTime()); Result.RawTime = Time.Current;
if (Result.HasResult) if (Result.HasResult)
updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss); updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss);

View File

@ -200,6 +200,14 @@ namespace osu.Game.Rulesets.Objects
[NotNull] [NotNull]
protected virtual HitWindows CreateHitWindows() => new HitWindows(); protected virtual HitWindows CreateHitWindows() => new HitWindows();
/// <summary>
/// The maximum offset from the end time of <see cref="HitObject"/> at which this <see cref="HitObject"/> can be judged.
/// <para>
/// Defaults to the miss window.
/// </para>
/// </summary>
public virtual double MaximumJudgementOffset => HitWindows?.WindowFor(HitResult.Miss) ?? 0;
public IList<HitSampleInfo> CreateSlidingSamples() public IList<HitSampleInfo> CreateSlidingSamples()
{ {
var slidingSamples = new List<HitSampleInfo>(); var slidingSamples = new List<HitSampleInfo>();

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Performance;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
@ -26,6 +27,8 @@ namespace osu.Game.Rulesets.Objects
private readonly IBindable<double> startTimeBindable = new BindableDouble(); private readonly IBindable<double> startTimeBindable = new BindableDouble();
internal event Action? RevertResult;
/// <summary> /// <summary>
/// Creates a new <see cref="HitObjectLifetimeEntry"/>. /// Creates a new <see cref="HitObjectLifetimeEntry"/>.
/// </summary> /// </summary>
@ -95,5 +98,7 @@ namespace osu.Game.Rulesets.Objects
/// Set <see cref="LifetimeEntry.LifetimeStart"/> using <see cref="InitialLifetimeOffset"/>. /// Set <see cref="LifetimeEntry.LifetimeStart"/> using <see cref="InitialLifetimeOffset"/>.
/// </summary> /// </summary>
internal void SetInitialLifetime() => LifetimeStart = HitObject.StartTime - InitialLifetimeOffset; internal void SetInitialLifetime() => LifetimeStart = HitObject.StartTime - InitialLifetimeOffset;
internal void OnRevertResult() => RevertResult?.Invoke();
} }
} }

View File

@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.UI
/// <summary> /// <summary>
/// The key conversion input manager for this DrawableRuleset. /// The key conversion input manager for this DrawableRuleset.
/// </summary> /// </summary>
public PassThroughInputManager KeyBindingInputManager; protected PassThroughInputManager KeyBindingInputManager;
public override double GameplayStartTime => Objects.FirstOrDefault()?.StartTime - 2000 ?? 0; public override double GameplayStartTime => Objects.FirstOrDefault()?.StartTime - 2000 ?? 0;
@ -66,6 +66,10 @@ namespace osu.Game.Rulesets.UI
public override Container Overlays { get; } = new Container { RelativeSizeAxes = Axes.Both }; public override Container Overlays { get; } = new Container { RelativeSizeAxes = Axes.Both };
public override IAdjustableAudioComponent Audio => audioContainer;
private readonly AudioContainer audioContainer = new AudioContainer { RelativeSizeAxes = Axes.Both };
public override Container FrameStableComponents { get; } = new Container { RelativeSizeAxes = Axes.Both }; public override Container FrameStableComponents { get; } = new Container { RelativeSizeAxes = Axes.Both };
public override IFrameStableClock FrameStableClock => frameStabilityContainer; public override IFrameStableClock FrameStableClock => frameStabilityContainer;
@ -102,14 +106,6 @@ namespace osu.Game.Rulesets.UI
private DrawableRulesetDependencies dependencies; private DrawableRulesetDependencies dependencies;
/// <summary>
/// Audio adjustments which are applied to the playfield.
/// </summary>
/// <remarks>
/// Does not affect <see cref="Overlays"/>.
/// </remarks>
public IAdjustableAudioComponent Audio { get; private set; }
/// <summary> /// <summary>
/// Creates a ruleset visualisation for the provided ruleset and beatmap. /// Creates a ruleset visualisation for the provided ruleset and beatmap.
/// </summary> /// </summary>
@ -134,7 +130,7 @@ namespace osu.Game.Rulesets.UI
playfield = new Lazy<Playfield>(() => CreatePlayfield().With(p => playfield = new Lazy<Playfield>(() => CreatePlayfield().With(p =>
{ {
p.NewResult += (_, r) => NewResult?.Invoke(r); p.NewResult += (_, r) => NewResult?.Invoke(r);
p.RevertResult += (_, r) => RevertResult?.Invoke(r); p.RevertResult += r => RevertResult?.Invoke(r);
})); }));
} }
@ -172,28 +168,22 @@ namespace osu.Game.Rulesets.UI
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(CancellationToken? cancellationToken) private void load(CancellationToken? cancellationToken)
{ {
AudioContainer audioContainer;
InternalChild = frameStabilityContainer = new FrameStabilityContainer(GameplayStartTime) InternalChild = frameStabilityContainer = new FrameStabilityContainer(GameplayStartTime)
{ {
FrameStablePlayback = FrameStablePlayback, FrameStablePlayback = FrameStablePlayback,
Children = new Drawable[] Children = new Drawable[]
{ {
FrameStableComponents, FrameStableComponents,
audioContainer = new AudioContainer audioContainer.WithChild(KeyBindingInputManager
{ .WithChildren(new Drawable[]
RelativeSizeAxes = Axes.Both, {
Child = KeyBindingInputManager CreatePlayfieldAdjustmentContainer()
.WithChild(CreatePlayfieldAdjustmentContainer() .WithChild(Playfield),
.WithChild(Playfield) Overlays
), })),
},
Overlays,
} }
}; };
Audio = audioContainer;
if ((ResumeOverlay = CreateResumeOverlay()) != null) if ((ResumeOverlay = CreateResumeOverlay()) != null)
{ {
AddInternal(CreateInputManager() AddInternal(CreateInputManager()
@ -230,7 +220,7 @@ namespace osu.Game.Rulesets.UI
public override void RequestResume(Action continueResume) public override void RequestResume(Action continueResume)
{ {
if (ResumeOverlay != null && (Cursor == null || (Cursor.LastFrameState == Visibility.Visible && Contains(Cursor.ActiveCursor.ScreenSpaceDrawQuad.Centre)))) if (ResumeOverlay != null && UseResumeOverlay && (Cursor == null || (Cursor.LastFrameState == Visibility.Visible && Contains(Cursor.ActiveCursor.ScreenSpaceDrawQuad.Centre))))
{ {
ResumeOverlay.GameplayCursor = Cursor; ResumeOverlay.GameplayCursor = Cursor;
ResumeOverlay.ResumeAction = continueResume; ResumeOverlay.ResumeAction = continueResume;
@ -436,13 +426,18 @@ namespace osu.Game.Rulesets.UI
/// </summary> /// </summary>
public readonly BindableBool IsPaused = new BindableBool(); public readonly BindableBool IsPaused = new BindableBool();
/// <summary>
/// Audio adjustments which are applied to the playfield.
/// </summary>
public abstract IAdjustableAudioComponent Audio { get; }
/// <summary> /// <summary>
/// The playfield. /// The playfield.
/// </summary> /// </summary>
public abstract Playfield Playfield { get; } public abstract Playfield Playfield { get; }
/// <summary> /// <summary>
/// Content to be placed above hitobjects. Will be affected by frame stability. /// Content to be placed above hitobjects. Will be affected by frame stability and adjustments applied to <see cref="Audio"/>.
/// </summary> /// </summary>
public abstract Container Overlays { get; } public abstract Container Overlays { get; }
@ -507,6 +502,15 @@ namespace osu.Game.Rulesets.UI
/// </summary> /// </summary>
public ResumeOverlay ResumeOverlay { get; protected set; } public ResumeOverlay ResumeOverlay { get; protected set; }
/// <summary>
/// Whether the <see cref="ResumeOverlay"/> should be used to return the user's cursor position to its previous location after a pause.
/// </summary>
/// <remarks>
/// Defaults to <c>true</c>.
/// Even if <c>true</c>, will not have any effect if the ruleset does not have a resume overlay (see <see cref="CreateResumeOverlay"/>).
/// </remarks>
public bool UseResumeOverlay { get; set; } = true;
/// <summary> /// <summary>
/// Returns first available <see cref="HitWindows"/> provided by a <see cref="HitObject"/>. /// Returns first available <see cref="HitWindows"/> provided by a <see cref="HitObject"/>.
/// </summary> /// </summary>
@ -531,6 +535,11 @@ namespace osu.Game.Rulesets.UI
} }
} }
/// <summary>
/// Create an optional resume overlay, which is displayed when a player requests to resume gameplay during non-break time.
/// This can be used to force the player to return their hands / cursor to the position they left off, to avoid players
/// using pauses as a means of adjusting their inputs (aka "pause buffering").
/// </summary>
protected virtual ResumeOverlay CreateResumeOverlay() => null; protected virtual ResumeOverlay CreateResumeOverlay() => null;
/// <summary> /// <summary>

View File

@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Rulesets.UI namespace osu.Game.Rulesets.UI
@ -68,27 +69,61 @@ namespace osu.Game.Rulesets.UI
protected HitObject GetMostValidObject() protected HitObject GetMostValidObject()
{ {
// The most optimal lookup case we have is when an object is alive. There are usually very few alive objects so there's no drawbacks in attempting this lookup each time. // The most optimal lookup case we have is when an object is alive. There are usually very few alive objects so there's no drawbacks in attempting this lookup each time.
var hitObject = hitObjectContainer.AliveObjects.FirstOrDefault(h => h.Result?.HasResult != true)?.HitObject; var drawableHitObject = hitObjectContainer.AliveObjects.FirstOrDefault(h => h.Result?.HasResult != true);
// In the case a next object isn't available in drawable form, we need to do a somewhat expensive traversal to get a valid sound to play. if (drawableHitObject != null)
if (hitObject == null)
{ {
// This lookup can be skipped if the last entry is still valid (in the future and not yet hit). // A hit object may have a more valid nested object.
if (fallbackObject == null || fallbackObject.Result?.HasResult == true) drawableHitObject = getMostValidNestedDrawable(drawableHitObject);
{
// We need to use lifetime entries to find the next object (we can't just use `hitObjectContainer.Objects` due to pooling - it may even be empty).
// If required, we can make this lookup more efficient by adding support to get next-future-entry in LifetimeEntryManager.
fallbackObject = hitObjectContainer.Entries
.Where(e => e.Result?.HasResult != true).MinBy(e => e.HitObject.StartTime);
// In the case there are no unjudged objects, the last hit object should be used instead. return drawableHitObject.HitObject;
fallbackObject ??= hitObjectContainer.Entries.LastOrDefault();
}
hitObject = fallbackObject?.HitObject;
} }
return hitObject; // In the case a next object isn't available in drawable form, we need to do a somewhat expensive traversal to get a valid sound to play.
// This lookup can be skipped if the last entry is still valid (in the future and not yet hit).
if (fallbackObject == null || fallbackObject.Result?.HasResult == true)
{
// We need to use lifetime entries to find the next object (we can't just use `hitObjectContainer.Objects` due to pooling - it may even be empty).
// If required, we can make this lookup more efficient by adding support to get next-future-entry in LifetimeEntryManager.
fallbackObject = hitObjectContainer.Entries
.Where(e => e.Result?.HasResult != true).MinBy(e => e.HitObject.StartTime);
if (fallbackObject != null)
return getEarliestNestedObject(fallbackObject.HitObject);
// In the case there are no non-judged objects, the last hit object should be used instead.
fallbackObject ??= hitObjectContainer.Entries.LastOrDefault();
}
if (fallbackObject == null)
return null;
bool fallbackHasResult = fallbackObject.Result?.HasResult == true;
// If the fallback has been judged then we want the sample from the object itself.
if (fallbackHasResult)
return fallbackObject.HitObject;
// Else we want the earliest (including nested).
// In cases of nested objects, they will always have earlier sample data than their parent object.
return getEarliestNestedObject(fallbackObject.HitObject);
}
private DrawableHitObject getMostValidNestedDrawable(DrawableHitObject o)
{
var nestedWithoutResult = o.NestedHitObjects.FirstOrDefault(n => n.Result?.HasResult != true);
if (nestedWithoutResult == null)
return o;
return getMostValidNestedDrawable(nestedWithoutResult);
}
private HitObject getEarliestNestedObject(HitObject hitObject)
{
var nested = hitObject.NestedHitObjects.FirstOrDefault();
return nested != null ? getEarliestNestedObject(nested) : hitObject;
} }
private SkinnableSound getNextSample() private SkinnableSound getNextSample()

View File

@ -28,11 +28,6 @@ namespace osu.Game.Rulesets.UI
/// </summary> /// </summary>
public event Action<DrawableHitObject, JudgementResult> NewResult; public event Action<DrawableHitObject, JudgementResult> NewResult;
/// <summary>
/// Invoked when a <see cref="DrawableHitObject"/> judgement is reverted.
/// </summary>
public event Action<DrawableHitObject, JudgementResult> RevertResult;
/// <summary> /// <summary>
/// Invoked when a <see cref="HitObject"/> becomes used by a <see cref="DrawableHitObject"/>. /// Invoked when a <see cref="HitObject"/> becomes used by a <see cref="DrawableHitObject"/>.
/// </summary> /// </summary>
@ -111,7 +106,6 @@ namespace osu.Game.Rulesets.UI
private void addDrawable(DrawableHitObject drawable) private void addDrawable(DrawableHitObject drawable)
{ {
drawable.OnNewResult += onNewResult; drawable.OnNewResult += onNewResult;
drawable.OnRevertResult += onRevertResult;
bindStartTime(drawable); bindStartTime(drawable);
AddInternal(drawable); AddInternal(drawable);
@ -120,7 +114,6 @@ namespace osu.Game.Rulesets.UI
private void removeDrawable(DrawableHitObject drawable) private void removeDrawable(DrawableHitObject drawable)
{ {
drawable.OnNewResult -= onNewResult; drawable.OnNewResult -= onNewResult;
drawable.OnRevertResult -= onRevertResult;
unbindStartTime(drawable); unbindStartTime(drawable);
@ -154,7 +147,6 @@ namespace osu.Game.Rulesets.UI
#endregion #endregion
private void onNewResult(DrawableHitObject d, JudgementResult r) => NewResult?.Invoke(d, r); private void onNewResult(DrawableHitObject d, JudgementResult r) => NewResult?.Invoke(d, r);
private void onRevertResult(DrawableHitObject d, JudgementResult r) => RevertResult?.Invoke(d, r);
#region Comparator + StartTime tracking #region Comparator + StartTime tracking

View File

@ -22,6 +22,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
using osu.Game.Rulesets.Objects.Pooling; using osu.Game.Rulesets.Objects.Pooling;
using osu.Framework.Extensions.ObjectExtensions;
namespace osu.Game.Rulesets.UI namespace osu.Game.Rulesets.UI
{ {
@ -35,9 +36,9 @@ namespace osu.Game.Rulesets.UI
public event Action<DrawableHitObject, JudgementResult> NewResult; public event Action<DrawableHitObject, JudgementResult> NewResult;
/// <summary> /// <summary>
/// Invoked when a <see cref="DrawableHitObject"/> judgement is reverted. /// Invoked when a judgement result is reverted.
/// </summary> /// </summary>
public event Action<DrawableHitObject, JudgementResult> RevertResult; public event Action<JudgementResult> RevertResult;
/// <summary> /// <summary>
/// The <see cref="DrawableHitObject"/> contained in this Playfield. /// The <see cref="DrawableHitObject"/> contained in this Playfield.
@ -98,6 +99,8 @@ namespace osu.Game.Rulesets.UI
private readonly HitObjectEntryManager entryManager = new HitObjectEntryManager(); private readonly HitObjectEntryManager entryManager = new HitObjectEntryManager();
private readonly Stack<HitObjectLifetimeEntry> judgedEntries;
/// <summary> /// <summary>
/// Creates a new <see cref="Playfield"/>. /// Creates a new <see cref="Playfield"/>.
/// </summary> /// </summary>
@ -107,14 +110,15 @@ namespace osu.Game.Rulesets.UI
hitObjectContainerLazy = new Lazy<HitObjectContainer>(() => CreateHitObjectContainer().With(h => hitObjectContainerLazy = new Lazy<HitObjectContainer>(() => CreateHitObjectContainer().With(h =>
{ {
h.NewResult += (d, r) => NewResult?.Invoke(d, r); h.NewResult += onNewResult;
h.RevertResult += (d, r) => RevertResult?.Invoke(d, r);
h.HitObjectUsageBegan += o => HitObjectUsageBegan?.Invoke(o); h.HitObjectUsageBegan += o => HitObjectUsageBegan?.Invoke(o);
h.HitObjectUsageFinished += o => HitObjectUsageFinished?.Invoke(o); h.HitObjectUsageFinished += o => HitObjectUsageFinished?.Invoke(o);
})); }));
entryManager.OnEntryAdded += onEntryAdded; entryManager.OnEntryAdded += onEntryAdded;
entryManager.OnEntryRemoved += onEntryRemoved; entryManager.OnEntryRemoved += onEntryRemoved;
judgedEntries = new Stack<HitObjectLifetimeEntry>();
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -224,7 +228,7 @@ namespace osu.Game.Rulesets.UI
otherPlayfield.DisplayJudgements.BindTo(DisplayJudgements); otherPlayfield.DisplayJudgements.BindTo(DisplayJudgements);
otherPlayfield.NewResult += (d, r) => NewResult?.Invoke(d, r); otherPlayfield.NewResult += (d, r) => NewResult?.Invoke(d, r);
otherPlayfield.RevertResult += (d, r) => RevertResult?.Invoke(d, r); otherPlayfield.RevertResult += r => RevertResult?.Invoke(r);
otherPlayfield.HitObjectUsageBegan += h => HitObjectUsageBegan?.Invoke(h); otherPlayfield.HitObjectUsageBegan += h => HitObjectUsageBegan?.Invoke(h);
otherPlayfield.HitObjectUsageFinished += h => HitObjectUsageFinished?.Invoke(h); otherPlayfield.HitObjectUsageFinished += h => HitObjectUsageFinished?.Invoke(h);
@ -252,6 +256,18 @@ namespace osu.Game.Rulesets.UI
updatable.Update(this); updatable.Update(this);
} }
} }
// When rewinding, revert future judgements in the reverse order.
while (judgedEntries.Count > 0)
{
var result = judgedEntries.Peek().Result;
Debug.Assert(result?.RawTime != null);
if (Time.Current >= result.RawTime.Value)
break;
revertResult(judgedEntries.Pop());
}
} }
/// <summary> /// <summary>
@ -443,6 +459,25 @@ namespace osu.Game.Rulesets.UI
#endregion #endregion
private void onNewResult(DrawableHitObject drawable, JudgementResult result)
{
Debug.Assert(result != null && drawable.Entry?.Result == result && result.RawTime != null);
judgedEntries.Push(drawable.Entry.AsNonNull());
NewResult?.Invoke(drawable, result);
}
private void revertResult(HitObjectLifetimeEntry entry)
{
var result = entry.Result;
Debug.Assert(result != null);
RevertResult?.Invoke(result);
entry.OnRevertResult();
result.Reset();
}
#region Editor logic #region Editor logic
/// <summary> /// <summary>

View File

@ -232,8 +232,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
double computedStartTime = computeDisplayStartTime(entry); double computedStartTime = computeDisplayStartTime(entry);
// always load the hitobject before its first judgement offset // always load the hitobject before its first judgement offset
double judgementOffset = entry.HitObject.HitWindows?.WindowFor(Scoring.HitResult.Miss) ?? 0; entry.LifetimeStart = Math.Min(entry.HitObject.StartTime - entry.HitObject.MaximumJudgementOffset, computedStartTime);
entry.LifetimeStart = Math.Min(entry.HitObject.StartTime - judgementOffset, computedStartTime);
} }
private void updateLayoutRecursive(DrawableHitObject hitObject, double? parentHitObjectStartTime = null) private void updateLayoutRecursive(DrawableHitObject hitObject, double? parentHitObjectStartTime = null)

View File

@ -32,6 +32,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary> /// </summary>
public abstract partial class SelectionHandler<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IKeyBindingHandler<GlobalAction>, IHasContextMenu public abstract partial class SelectionHandler<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>, IKeyBindingHandler<GlobalAction>, IHasContextMenu
{ {
/// <summary>
/// How much padding around the selection area is added.
/// </summary>
public const float INFLATE_SIZE = 5;
/// <summary> /// <summary>
/// The currently selected blueprints. /// The currently selected blueprints.
/// Should be used when operations are dealing directly with the visible blueprints. /// Should be used when operations are dealing directly with the visible blueprints.
@ -346,7 +351,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
for (int i = 1; i < selectedBlueprints.Count; i++) for (int i = 1; i < selectedBlueprints.Count; i++)
selectionRect = RectangleF.Union(selectionRect, ToLocalSpace(selectedBlueprints[i].SelectionQuad).AABBFloat); selectionRect = RectangleF.Union(selectionRect, ToLocalSpace(selectedBlueprints[i].SelectionQuad).AABBFloat);
selectionRect = selectionRect.Inflate(5f); selectionRect = selectionRect.Inflate(INFLATE_SIZE);
SelectionBox.Position = selectionRect.Location; SelectionBox.Position = selectionRect.Location;
SelectionBox.Size = selectionRect.Size; SelectionBox.Size = selectionRect.Size;

View File

@ -157,7 +157,16 @@ namespace osu.Game.Screens.Edit
private bool isNewBeatmap; private bool isNewBeatmap;
protected override UserActivity InitialActivity => new UserActivity.Editing(Beatmap.Value.BeatmapInfo); protected override UserActivity InitialActivity
{
get
{
if (Beatmap.Value.Metadata.Author.OnlineID == api.LocalUser.Value.OnlineID)
return new UserActivity.EditingBeatmap(Beatmap.Value.BeatmapInfo);
return new UserActivity.ModdingBeatmap(Beatmap.Value.BeatmapInfo);
}
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); => dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
@ -176,6 +185,7 @@ namespace osu.Game.Screens.Edit
private Bindable<float> editorBackgroundDim; private Bindable<float> editorBackgroundDim;
private Bindable<bool> editorHitMarkers; private Bindable<bool> editorHitMarkers;
private Bindable<bool> editorAutoSeekOnPlacement;
public Editor(EditorLoader loader = null) public Editor(EditorLoader loader = null)
{ {
@ -263,6 +273,7 @@ namespace osu.Game.Screens.Edit
editorBackgroundDim = config.GetBindable<float>(OsuSetting.EditorDim); editorBackgroundDim = config.GetBindable<float>(OsuSetting.EditorDim);
editorHitMarkers = config.GetBindable<bool>(OsuSetting.EditorShowHitMarkers); editorHitMarkers = config.GetBindable<bool>(OsuSetting.EditorShowHitMarkers);
editorAutoSeekOnPlacement = config.GetBindable<bool>(OsuSetting.EditorAutoSeekOnPlacement);
AddInternal(new OsuContextMenuContainer AddInternal(new OsuContextMenuContainer
{ {
@ -320,6 +331,10 @@ namespace osu.Game.Screens.Edit
new ToggleMenuItem(EditorStrings.ShowHitMarkers) new ToggleMenuItem(EditorStrings.ShowHitMarkers)
{ {
State = { BindTarget = editorHitMarkers }, State = { BindTarget = editorHitMarkers },
},
new ToggleMenuItem(EditorStrings.AutoSeekOnPlacement)
{
State = { BindTarget = editorAutoSeekOnPlacement },
} }
} }
}, },

View File

@ -7,6 +7,7 @@ using osu.Framework.Screens;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Users;
namespace osu.Game.Screens.Edit.GameplayTest namespace osu.Game.Screens.Edit.GameplayTest
{ {
@ -15,6 +16,8 @@ namespace osu.Game.Screens.Edit.GameplayTest
private readonly Editor editor; private readonly Editor editor;
private readonly EditorState editorState; private readonly EditorState editorState;
protected override UserActivity InitialActivity => new UserActivity.TestingBeatmap(Beatmap.Value.BeatmapInfo, Ruleset.Value);
[Resolved] [Resolved]
private MusicController musicController { get; set; } = null!; private MusicController musicController { get; set; } = 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.Colour; using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -22,29 +20,21 @@ namespace osu.Game.Screens.Play.Break
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new Container new Box
{ {
Anchor = Anchor.TopLeft, Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft, Origin = Anchor.TopLeft,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = height, Height = height,
Child = new Box Colour = ColourInfo.GradientVertical(Color4.Black, transparent_black),
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(Color4.Black, transparent_black),
}
}, },
new Container new Box
{ {
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = height, Height = height,
Child = new Box Colour = ColourInfo.GradientVertical(transparent_black, Color4.Black),
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(transparent_black, Color4.Black),
}
} }
}; };
} }

View File

@ -16,7 +16,7 @@ using osuTK;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public partial class BPMCounter : RollingCounter<double>, ISkinnableDrawable public partial class BPMCounter : RollingCounter<double>, ISerialisableDrawable
{ {
protected override double RollingDuration => 750; protected override double RollingDuration => 750;

View File

@ -14,7 +14,7 @@ using osuTK;
namespace osu.Game.Screens.Play.HUD.ClicksPerSecond namespace osu.Game.Screens.Play.HUD.ClicksPerSecond
{ {
public partial class ClicksPerSecondCounter : RollingCounter<int>, ISkinnableDrawable public partial class ClicksPerSecondCounter : RollingCounter<int>, ISerialisableDrawable
{ {
[Resolved] [Resolved]
private ClicksPerSecondCalculator calculator { get; set; } = null!; private ClicksPerSecondCalculator calculator { get; set; } = null!;

View File

@ -7,7 +7,7 @@ using osu.Game.Skinning;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public abstract partial class ComboCounter : RollingCounter<int>, ISkinnableDrawable public abstract partial class ComboCounter : RollingCounter<int>, ISerialisableDrawable
{ {
public bool UsesFixedAnchor { get; set; } public bool UsesFixedAnchor { get; set; }

View File

@ -9,7 +9,7 @@ using osu.Game.Skinning;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public partial class DefaultAccuracyCounter : GameplayAccuracyCounter, ISkinnableDrawable public partial class DefaultAccuracyCounter : GameplayAccuracyCounter, ISerialisableDrawable
{ {
public bool UsesFixedAnchor { get; set; } public bool UsesFixedAnchor { get; set; }

View File

@ -19,7 +19,7 @@ using osu.Game.Skinning;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public partial class DefaultHealthDisplay : HealthDisplay, IHasAccentColour, ISkinnableDrawable public partial class DefaultHealthDisplay : HealthDisplay, IHasAccentColour, ISerialisableDrawable
{ {
/// <summary> /// <summary>
/// The base opacity of the glow. /// The base opacity of the glow.

View File

@ -10,7 +10,7 @@ using osu.Game.Skinning;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public partial class DefaultScoreCounter : GameplayScoreCounter, ISkinnableDrawable public partial class DefaultScoreCounter : GameplayScoreCounter, ISerialisableDrawable
{ {
public DefaultScoreCounter() public DefaultScoreCounter()
{ {

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