1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 23:52:57 +08:00

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>true</NullabilityInfoContextSupport>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
</PropertyGroup>
<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>
<PropertyGroup>
<!-- 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:
return game.BeatmapInfo;
case UserActivity.Editing edit:
case UserActivity.EditingBeatmap edit:
return edit.BeatmapInfo;
}
@ -183,10 +183,10 @@ namespace osu.Desktop
case UserActivity.InGame game:
return game.BeatmapInfo.ToString() ?? string.Empty;
case UserActivity.Editing edit:
case UserActivity.EditingBeatmap edit:
return edit.BeatmapInfo.ToString() ?? string.Empty;
case UserActivity.Watching watching:
case UserActivity.WatchingReplay watching:
return watching.BeatmapInfo.ToString();
case UserActivity.InLobby lobby:

View File

@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Tests
if (withModifiedSkin)
{
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());
CreateTest();
}

View File

@ -60,26 +60,24 @@ namespace osu.Game.Rulesets.Catch.Tests
[Test]
public void TestCatcherHyperStateReverted()
{
DrawableCatchHitObject drawableObject1 = null;
DrawableCatchHitObject drawableObject2 = null;
JudgementResult result1 = null;
JudgementResult result2 = null;
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", () =>
{
attemptCatch(new Fruit(), out drawableObject2, out result2);
result2 = attemptCatch(new Fruit());
});
AddStep("revert second result", () =>
{
catcher.OnRevertResult(drawableObject2, result2);
catcher.OnRevertResult(result2);
});
checkHyperDash(true);
AddStep("revert first result", () =>
{
catcher.OnRevertResult(drawableObject1, result1);
catcher.OnRevertResult(result1);
});
checkHyperDash(false);
}
@ -87,16 +85,15 @@ namespace osu.Game.Rulesets.Catch.Tests
[Test]
public void TestCatcherAnimationStateReverted()
{
DrawableCatchHitObject drawableObject = null;
JudgementResult result = null;
AddStep("catch kiai fruit", () =>
{
attemptCatch(new TestKiaiFruit(), out drawableObject, out result);
result = attemptCatch(new TestKiaiFruit());
});
checkState(CatcherAnimationState.Kiai);
AddStep("revert result", () =>
{
catcher.OnRevertResult(drawableObject, result);
catcher.OnRevertResult(result);
});
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 attemptCatch(CatchHitObject hitObject)
{
attemptCatch(() => hitObject, 1);
}
private void attemptCatch(Func<CatchHitObject> hitObject, int count)
{
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());
drawableObject = createDrawableObject(hitObject);
result = createResult(hitObject);
var drawableObject = createDrawableObject(hitObject);
var result = createResult(hitObject);
applyResult(drawableObject, result);
return result;
}
private void applyResult(DrawableCatchHitObject drawableObject, JudgementResult result)

View File

@ -4,6 +4,7 @@
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Skinning;
using osuTK.Graphics;
@ -27,12 +28,12 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
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:
var components = base.GetDrawableComponent(lookup) as SkinnableTargetComponentsContainer;
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
var components = base.GetDrawableComponent(lookup) as Container;
if (providesComboCounter && components != null)
{

View File

@ -63,12 +63,12 @@ namespace osu.Game.Rulesets.Catch.UI
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)
return;
updateCombo(result.ComboAtJudgement, judgedObject.AccentColour.Value);
updateCombo(result.ComboAtJudgement, null);
}
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)
=> CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result);
private void onRevertResult(DrawableHitObject judgedObject, JudgementResult result)
=> CatcherArea.OnRevertResult((DrawableCatchHitObject)judgedObject, result);
private void onRevertResult(JudgementResult 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;
@ -268,8 +268,8 @@ namespace osu.Game.Rulesets.Catch.UI
SetHyperDashState();
}
caughtObjectContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject, false);
droppedObjectTarget.RemoveAll(d => d.HitObject == drawableObject.HitObject, false);
caughtObjectContainer.RemoveAll(d => d.HitObject == result.HitObject, false);
droppedObjectTarget.RemoveAll(d => d.HitObject == result.HitObject, false);
}
/// <summary>

View File

@ -73,10 +73,10 @@ namespace osu.Game.Rulesets.Catch.UI
comboDisplay.OnNewResult(hitObject, result);
}
public void OnRevertResult(DrawableCatchHitObject hitObject, JudgementResult result)
public void OnRevertResult(JudgementResult result)
{
comboDisplay.OnRevertResult(hitObject, result);
Catcher.OnRevertResult(hitObject, result);
comboDisplay.OnRevertResult(result);
Catcher.OnRevertResult(result);
}
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>
private double? releaseTime;
public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
public DrawableHoldNote()
: this(null)
{

View File

@ -15,13 +15,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary>
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 DrawableHoldNote HoldNote => (DrawableHoldNote)ParentHitObject;
@ -40,14 +33,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public void UpdateResult() => base.UpdateResult(true);
public override double MaximumJudgementOffset => base.MaximumJudgementOffset * release_window_lenience;
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
Debug.Assert(HitObject.HitWindows != null);
// Factor in the release lenience
timeOffset /= release_window_lenience;
timeOffset /= TailNote.RELEASE_WINDOW_LENIENCE;
if (!userTriggered)
{

View File

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

View File

@ -10,6 +10,15 @@ namespace osu.Game.Rulesets.Mania.Objects
{
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 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.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
@ -34,6 +37,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
private Drawable? lightContainer;
private Drawable? light;
private LegacyNoteBodyStyle? bodyStyle;
public LegacyBodyPiece()
{
@ -54,9 +58,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
float lightScale = GetColumnSkinConfig<float>(skin, LegacyManiaSkinConfigurationLookups.HoldNoteLightScale)?.Value
?? 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.
// This animation is discarded and re-queried with the appropriate frame length afterwards.
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)
return;
@ -94,16 +102,11 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
d.Anchor = Anchor.TopCentre;
d.RelativeSizeAxes = Axes.Both;
d.Size = Vector2.One;
d.FillMode = FillMode.Stretch;
d.Height = minimumColumnWidth / d.DrawWidth * 1.6f; // constant matching stable.
// Todo: Wrap?
});
if (bodySprite != null)
InternalChild = bodySprite;
direction.BindTo(scrollingInfo.Direction);
isHitting.BindTo(holdNote.IsHitting);
}
protected override void LoadComplete()
@ -164,8 +167,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
if (bodySprite != null)
{
bodySprite.Origin = Anchor.BottomCentre;
bodySprite.Scale = new Vector2(1, -1);
bodySprite.Origin = Anchor.TopCentre;
bodySprite.Anchor = Anchor.BottomCentre; // needs to be flipped due to scale flip in Update.
}
if (light != null)
@ -176,7 +179,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
if (bodySprite != null)
{
bodySprite.Origin = Anchor.TopCentre;
bodySprite.Scale = Vector2.One;
bodySprite.Anchor = Anchor.TopCentre;
}
if (light != null)
@ -207,6 +210,33 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
base.Update();
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)

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

View File

@ -11,6 +11,7 @@ using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Mods
@ -55,11 +56,13 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
// 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;
// Generate the replay frames the cursor should follow
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.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
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.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
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.")]
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)
{
switch (hitObject)
@ -51,6 +58,8 @@ namespace osu.Game.Rulesets.Osu.Mods
if (ClassicNoteLock.Value)
osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy();
usingHiddenFading = drawableRuleset.Mods.OfType<OsuModHidden>().SingleOrDefault()?.OnlyFadeApproachCircles.Value == false;
}
public void ApplyToDrawableHitObject(DrawableHitObject obj)
@ -59,12 +68,32 @@ namespace osu.Game.Rulesets.Osu.Mods
{
case DrawableSliderHead head:
head.TrackFollowCircle = !NoSliderHeadMovement.Value;
if (FadeHitCircleEarly.Value && !usingHiddenFading)
applyEarlyFading(head);
break;
case DrawableSliderTail tail:
tail.SamplePlaysOnlyOnHit = !AlwaysPlayTailSample.Value;
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.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
@ -42,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
// grab the input manager for future use.
osuInputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager;
osuInputManager = ((DrawableOsuRuleset)drawableRuleset).KeyBindingInputManager;
}
public void ApplyToPlayer(Player player)

View File

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

View File

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

View File

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

View File

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

View File

@ -11,10 +11,17 @@ namespace osu.Game.Rulesets.Osu.Objects
{
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();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public override double MaximumJudgementOffset => SpinnerDuration;
public class OsuSpinnerTickJudgement : OsuJudgement
{
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;
public new OsuInputManager KeyBindingInputManager => (OsuInputManager)base.KeyBindingInputManager;
public new OsuPlayfield Playfield => (OsuPlayfield)base.Playfield;
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)
{
if (drawable is DrawableHit)
drawable.SnapJudgementLocation = true;
if (drawable is DrawableTaikoHitObject hit)
hit.SnapJudgementLocation = true;
}
}
}

View File

@ -8,6 +8,6 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
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());
public override double MaximumJudgementOffset => HitObject.HitWindow;
protected override void OnApply()
{
base.OnApply();

View File

@ -25,6 +25,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
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)
: base(hitObject)
{

View File

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

View File

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

View File

@ -7,6 +7,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using NUnit.Framework;
using osu.Framework.Audio;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Judgements;
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");
}
public override IAdjustableAudioComponent Audio { get; }
public override Playfield Playfield { get; }
public override Container Overlays { 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);
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);
}
}
}
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));
}
@ -87,8 +87,8 @@ namespace osu.Game.Tests.Skins
{
var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.DrawableComponentInfo, Has.Count.EqualTo(2));
Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.MainHUDComponents], Has.Length.EqualTo(9));
Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
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);
Assert.That(skin.DrawableComponentInfo, Has.Count.EqualTo(2));
Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.MainHUDComponents], Has.Length.EqualTo(6));
Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.SongSelect], Has.Length.EqualTo(1));
Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(6));
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.Settings.First().Key, Is.EqualTo("sprite_name"));
@ -115,10 +115,10 @@ namespace osu.Game.Tests.Skins
using (var storage = new ZipArchiveReader(stream))
{
var skin = new TestSkin(new SkinInfo(), null, storage);
Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.MainHUDComponents], Has.Length.EqualTo(8));
Assert.That(skin.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.MainHUDComponents].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.DrawableComponentInfo[GlobalSkinComponentLookup.LookupType.MainHUDComponents].Select(i => i.Type), Contains.Item(typeof(LegacySongProgress)));
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(8));
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(UnstableRateCounter)));
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(ColourHitErrorMeter)));
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.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics.Containers;
using osu.Framework.Lists;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Skinning.Legacy;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osu.Game.Storyboards;
@ -28,10 +25,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneBeatmapSkinFallbacks : OsuPlayerTestScene
{
private ISkin currentBeatmapSkin;
private ISkin currentBeatmapSkin = null!;
[Resolved]
private SkinManager skinManager { get; set; }
private SkinManager skinManager { get; set; } = null!;
protected override bool HasCustomSteps => true;
@ -39,8 +36,8 @@ namespace osu.Game.Tests.Visual.Gameplay
public void TestEmptyLegacyBeatmapSkinFallsBack()
{
CreateSkinTest(TrianglesSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null));
AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinnableTargetContainer>().All(c => c.ComponentsLoaded));
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(GlobalSkinComponentLookup.LookupType.MainHUDComponents, skinManager.CurrentSkin.Value));
AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinComponentsContainer>().All(c => c.ComponentsLoaded));
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinComponentsContainerLookup.TargetArea.MainHUDComponents, skinManager.CurrentSkin.Value));
}
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)
.ChildrenOfType<SkinnableTargetComponentsContainer>().SingleOrDefault();
var targetContainer = Player.ChildrenOfType<SkinComponentsContainer>().First(s => s.Lookup.Target == target);
var actualComponentsContainer = targetContainer.ChildrenOfType<Container>().SingleOrDefault(c => c.Parent == targetContainer);
if (actualComponentsContainer == null)
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)
return false;
@ -86,23 +83,23 @@ namespace osu.Game.Tests.Visual.Gameplay
Add(expectedComponentsAdjustmentContainer);
expectedComponentsAdjustmentContainer.UpdateSubTree();
var expectedInfo = expectedComponentsContainer.CreateSkinnableInfo();
var expectedInfo = expectedComponentsContainer.CreateSerialisedInfo();
Remove(expectedComponentsAdjustmentContainer, true);
return almostEqual(actualInfo, expectedInfo);
}
private static bool almostEqual(SkinnableInfo info, SkinnableInfo other) =>
private static bool almostEqual(SerialisedDrawableInfo drawableInfo, SerialisedDrawableInfo? other) =>
other != null
&& info.Type == other.Type
&& info.Anchor == other.Anchor
&& info.Origin == other.Origin
&& Precision.AlmostEquals(info.Position, other.Position, 1)
&& Precision.AlmostEquals(info.Scale, other.Scale)
&& Precision.AlmostEquals(info.Rotation, other.Rotation)
&& info.Children.SequenceEqual(other.Children, new FuncEqualityComparer<SkinnableInfo>(almostEqual));
&& drawableInfo.Type == other.Type
&& drawableInfo.Anchor == other.Anchor
&& drawableInfo.Origin == other.Origin
&& Precision.AlmostEquals(drawableInfo.Position, other.Position, 1)
&& Precision.AlmostEquals(drawableInfo.Scale, other.Scale)
&& Precision.AlmostEquals(drawableInfo.Rotation, other.Rotation)
&& 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);
protected override Ruleset CreatePlayerRuleset() => new TestOsuRuleset();
@ -111,7 +108,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
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)
{
this.beatmapSkin = beatmapSkin;

View File

@ -1,50 +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.
#nullable disable
using System.Diagnostics;
using System.Linq;
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.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Storyboards;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneGameplaySampleTriggerSource : PlayerTestScene
{
private TestGameplaySampleTriggerSource sampleTriggerSource;
private TestGameplaySampleTriggerSource sampleTriggerSource = null!;
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)
{
ControlPointInfo controlPointInfo = new LegacyControlPointInfo();
beatmap = new Beatmap
{
BeatmapInfo = new BeatmapInfo
{
Difficulty = new BeatmapDifficulty { CircleSize = 6, SliderMultiplier = 3 },
Ruleset = ruleset
}
},
ControlPointInfo = controlPointInfo
};
const double start_offset = 8000;
const double spacing = 2000;
// intentionally start objects a bit late so we can test the case of no alive objects.
double t = start_offset;
beatmap.HitObjects.AddRange(new[]
beatmap.HitObjects.AddRange(new HitObject[]
{
new HitCircle
{
// intentionally start objects a bit late so we can test the case of no alive objects.
StartTime = t += spacing,
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
},
@ -61,12 +78,24 @@ namespace osu.Game.Tests.Visual.Gameplay
},
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) },
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;
}
@ -80,42 +109,88 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
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;
InputManager.MoveMouseTo(nextDrawableObject.ScreenSpaceDrawQuad.Centre);
return true;
Debug.Assert(next.Entry?.Result?.HasResult != true);
InputManager.MoveMouseTo(next.ScreenSpaceDrawQuad.Centre);
InputManager.Click(MouseButton.Left);
}
return false;
});
AddUntilStep("hit first hitobject", () =>
{
InputManager.Click(MouseButton.Left);
return nextObjectEntry.Result?.HasResult == true;
});
AddAssert("first object hit", () => getNextAliveObject()?.Entry?.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]);
AddUntilStep("check correct object after miss", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[3]);
// Still object 1 as it's not hit yet.
seekBeforeIndex(1);
waitForAliveObjectIndex(1);
checkValidObjectIndex(1);
AddUntilStep("no alive objects", () => getNextAliveObject() == null);
AddAssert("check correct object after none alive", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[3]);
seekBeforeIndex(2);
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();
[Test]

View File

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

View File

@ -9,6 +9,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
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");
}
public override IAdjustableAudioComponent Audio { get; }
public override Playfield Playfield { get; }
public override Container Overlays { 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.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
@ -37,6 +36,8 @@ namespace osu.Game.Tests.Visual.Gameplay
private TestDrawablePoolingRuleset drawableRuleset;
private TestPlayfield playfield => (TestPlayfield)drawableRuleset.Playfield;
[Test]
public void TestReusedWithHitObjectsSpacedFarApart()
{
@ -133,29 +134,49 @@ namespace osu.Game.Tests.Visual.Gameplay
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]
public void TestApplyHitResultOnKilled()
{
ManualClock clock = null;
bool anyJudged = false;
void onNewResult(JudgementResult _) => anyJudged = true;
var beatmap = new Beatmap();
beatmap.HitObjects.Add(new TestKilledHitObject { Duration = 20 });
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);
AddAssert("object judged", () => anyJudged);
AddStep("clean up", () => drawableRuleset.NewResult -= onNewResult);
AddAssert("object judged", () => playfield.JudgedObjects.Count == 1);
}
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
{
public readonly HashSet<HitObject> JudgedObjects = new HashSet<HitObject>();
private readonly int poolSize;
public TestPlayfield(int poolSize)
{
this.poolSize = poolSize;
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]

View File

@ -1,9 +1,11 @@
// 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.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
@ -12,6 +14,7 @@ using osu.Game.Overlays.Settings;
using osu.Game.Overlays.SkinEditor;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning;
using osuTK.Input;
@ -20,33 +23,85 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneSkinEditor : PlayerTestScene
{
private SkinEditor? skinEditor;
private SkinEditor skinEditor = null!;
protected override bool Autoplay => true;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
[Cached]
public readonly EditorClipboard Clipboard = new EditorClipboard();
private SkinComponentsContainer targetContainer => Player.ChildrenOfType<SkinComponentsContainer>().First();
[SetUpSteps]
public override void 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", () =>
{
skinEditor?.Expire();
if (skinEditor.IsNotNull())
skinEditor.Expire();
Player.ScaleTo(0.4f);
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]
public void TestToggleEditor()
{
AddToggleStep("toggle editor visibility", _ => skinEditor!.ToggleVisibility());
AddToggleStep("toggle editor visibility", _ => skinEditor.ToggleVisibility());
}
[Test]
@ -59,7 +114,7 @@ namespace osu.Game.Tests.Visual.Gameplay
var blueprint = skinEditor.ChildrenOfType<SkinBlueprint>().First(b => b.Item is BarHitErrorMeter);
hitErrorMeter = (BarHitErrorMeter)blueprint.Item;
skinEditor!.SelectedComponents.Clear();
skinEditor.SelectedComponents.Clear();
skinEditor.SelectedComponents.Add(blueprint.Item);
});

View File

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

View File

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

View File

@ -15,6 +15,7 @@ using osu.Game.Overlays.Settings;
using osu.Game.Overlays.SkinEditor;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
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);
}
[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()
{
PushAndConfirm(() => songSelect = new TestPlaySongSelect());

View File

@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Online
[Test]
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())));

View File

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

View File

@ -516,15 +516,20 @@ namespace osu.Game.Tests.Visual.SongSelect
{
sets.Clear();
for (int i = 0; i < 20; i++)
for (int i = 0; i < 10; i++)
{
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);
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.Title = $"submitted: {set.DateSubmitted}");
sets.Add(set);
}
});
@ -532,15 +537,26 @@ namespace osu.Game.Tests.Visual.SongSelect
loadBeatmaps(sets);
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);
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
{
Sort = SortMode.DateSubmitted,
SearchText = zzz_string
}, false));
checkVisibleItemCount(diff: false, count: 3);
checkVisibleItemCount(diff: false, 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]
@ -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.
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()
@ -1190,8 +1206,11 @@ namespace osu.Game.Tests.Visual.SongSelect
{
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;
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));
AddUntilStep("scrolled to 500", () => Precision.AlmostEquals(scroll.Current, 500, 0.1f));
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]
@ -71,6 +83,10 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("invoke action", () => scroll.Button.Action.Invoke());
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]
@ -85,6 +101,14 @@ namespace osu.Game.Tests.Visual.UserInterface
});
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]
@ -97,12 +121,12 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("hover button", () => InputManager.MoveMouseTo(scroll.Button));
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
{
public new ScrollToTopButton Button => base.Button;
public new ScrollBackButton Button => base.Button;
}
}
}

View File

@ -98,6 +98,9 @@ namespace osu.Game.Audio
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();
}

View File

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

View File

@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using osu.Framework.Platform;
using osu.Game.Extensions;
using osu.Game.Utils;
@ -43,7 +44,10 @@ namespace osu.Game.Database
{
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}");
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.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
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;
namespace osu.Game.Extensions
@ -48,42 +43,5 @@ namespace osu.Game.Extensions
/// <returns>The delta vector in Parent's coordinates.</returns>
public static Vector2 ScreenSpaceDeltaToParentSpace(this Drawable drawable, Vector2 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 osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Colour;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -259,8 +258,6 @@ namespace osu.Game.Graphics.Backgrounds
Vector2Extensions.Transform(triangleQuad.BottomRight * size, DrawInfo.Matrix)
);
ColourInfo colourInfo = triangleColourInfo(DrawColourInfo.Colour, triangleQuad);
RectangleF textureCoords = new RectangleF(
triangleQuad.TopLeft.X - topLeft.X,
triangleQuad.TopLeft.Y - topLeft.Y,
@ -268,23 +265,12 @@ namespace osu.Game.Graphics.Backgrounds
triangleQuad.Height
) / 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();
}
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)
{
float leftClamped = Math.Clamp(topLeft.X, 0f, 1f);

View File

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

View File

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

View File

@ -21,7 +21,7 @@ namespace osu.Game.Graphics.Containers
/// </summary>
public const float BREAK_LIGHTEN_AMOUNT = 0.3f;
protected const double BACKGROUND_FADE_DURATION = 800;
public const double BACKGROUND_FADE_DURATION = 800;
/// <summary>
/// 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)
{
var segmentColour = new ColourInfo
{
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 segmentColour = DrawColourInfo.Colour.Interpolate(new Quad(segment.Start, 0f, segment.End - segment.Start, 1f));
var tierColour = segment.Tier >= 0 ? tierColours[segment.Tier] : new Colour4(0, 0, 0, 0);
segmentColour.ApplyChild(tierColour);

View File

@ -19,6 +19,11 @@ namespace osu.Game.Localisation
/// </summary>
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>
/// "Timing"
/// </summary>

View File

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

View File

@ -60,6 +60,7 @@ using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
using osu.Game.Skinning;
using osu.Game.Updater;
using osu.Game.Users;
using osu.Game.Utils;
@ -501,6 +502,23 @@ namespace osu.Game
/// <param name="version">The build version of the update stream</param>
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>
/// Present a beatmap at song select immediately.
/// 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.
SkinManager.PostNotification = n => Notifications.Post(n);
SkinManager.PresentImport = items => PresentSkin(items.First().Value);
BeatmapManager.PostNotification = n => Notifications.Post(n);
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 search_icon_width = 40;
private const float padding = 5;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
@ -71,9 +72,10 @@ namespace osu.Game.Overlays.Chat
RelativeSizeAxes = Axes.Y,
Width = chatting_text_width,
Masking = true,
Padding = new MarginPadding { Right = 5 },
Padding = new MarginPadding { Horizontal = padding },
Child = chattingText = new OsuSpriteText
{
MaxWidth = chatting_text_width - padding * 2,
Font = OsuFont.Torus.With(size: 20),
Colour = colourProvider.Background1,
Anchor = Anchor.CentreRight,
@ -97,7 +99,7 @@ namespace osu.Game.Overlays.Chat
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = 5 },
Padding = new MarginPadding { Right = padding },
Child = chatTextBox = new ChatTextBox
{
Anchor = Anchor.CentreLeft,

View File

@ -57,6 +57,7 @@ namespace osu.Game.Overlays.Dialog
private Sample confirmSample;
private double lastTickPlaybackTime;
private AudioFilter lowPassFilter = null!;
private bool mouseDown;
[BackgroundDependencyLoader]
private void load(AudioManager audio)
@ -73,6 +74,12 @@ namespace osu.Game.Overlays.Dialog
Progress.BindValueChanged(progressChanged);
}
protected override void AbortConfirm()
{
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
base.AbortConfirm();
}
protected override void Confirm()
{
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
@ -83,6 +90,7 @@ namespace osu.Game.Overlays.Dialog
protected override bool OnMouseDown(MouseDownEvent e)
{
BeginConfirm();
mouseDown = true;
return true;
}
@ -90,11 +98,28 @@ namespace osu.Game.Overlays.Dialog
{
if (!e.HasAnyButtonPressed)
{
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
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)
{
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.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
using osuTK;
using osuTK.Graphics;
@ -27,7 +28,14 @@ namespace osu.Game.Overlays.Mods
public Color4 AccentColour
{
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>
@ -44,6 +52,7 @@ namespace osu.Game.Overlays.Mods
private readonly Box headerBackground;
private readonly Container contentContainer;
private readonly Box contentBackground;
private readonly TrianglesV2 triangles;
private const float header_height = 42;
@ -73,6 +82,13 @@ namespace osu.Game.Overlays.Mods
RelativeSizeAxes = Axes.X,
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 =>
{
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;

View File

@ -5,6 +5,7 @@
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -21,25 +22,29 @@ using osuTK.Graphics;
namespace osu.Game.Overlays
{
/// <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>
public partial class OverlayScrollContainer : UserTrackingScrollContainer
{
/// <summary>
/// Scroll position at which the <see cref="ScrollToTopButton"/> will be shown.
/// Scroll position at which the <see cref="ScrollBackButton"/> will be shown.
/// </summary>
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,
Origin = Anchor.BottomRight,
Margin = new MarginPadding(20),
Action = scrollToTop
Action = scrollBack,
LastScrollTarget = { BindTarget = lastScrollTarget }
});
}
@ -53,16 +58,31 @@ namespace osu.Game.Overlays
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();
Button.State = Visibility.Hidden;
base.OnUserScroll(value, animated, distanceDecay);
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;
@ -88,8 +108,11 @@ namespace osu.Game.Overlays
private readonly Container content;
private readonly Box background;
private readonly SpriteIcon spriteIcon;
public ScrollToTopButton()
public Bindable<float?> LastScrollTarget = new Bindable<float?>();
public ScrollBackButton()
: base(HoverSampleSet.ScrollToTop)
{
Size = new Vector2(50);
@ -113,7 +136,7 @@ namespace osu.Game.Overlays
{
RelativeSizeAxes = Axes.Both
},
new SpriteIcon
spriteIcon = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -134,6 +157,17 @@ namespace osu.Game.Overlays
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)
{
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.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Utils;
using osu.Game.Graphics;
@ -66,7 +67,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
RelativeSizeAxes = Axes.Both,
Colour = colour.Gray1,
},
usableAreaContainer = new Container
usableAreaContainer = new UsableAreaContainer(handler)
{
Origin = Anchor.Centre,
Children = new Drawable[]
@ -225,4 +226,28 @@ namespace osu.Game.Overlays.Settings.Sections.Input
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.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.SkinEditor
{
public partial class SkinBlueprint : SelectionBlueprint<ISkinnableDrawable>
public partial class SkinBlueprint : SelectionBlueprint<ISerialisableDrawable>
{
private Container box = null!;
private Container outlineBox = null!;
private AnchorOriginVisualiser anchorOriginVisualiser = null!;
private OsuSpriteText label = null!;
private Drawable drawable => (Drawable)Item;
protected override bool ShouldBeAlive => drawable.IsAlive && Item.IsPresent;
[Resolved]
private OsuColour colours { get; set; } = null!;
private Quad drawableQuad;
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)
{
}
[BackgroundDependencyLoader]
private void load()
private void load(OsuColour colours)
{
InternalChildren = new Drawable[]
{
@ -46,26 +59,30 @@ namespace osu.Game.Overlays.SkinEditor
{
Children = new Drawable[]
{
outlineBox = new Container
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = 3,
BorderColour = Color4.White,
CornerRadius = 3,
BorderThickness = SelectionBox.BORDER_RADIUS / 2,
BorderColour = ColourInfo.GradientVertical(colours.Pink4.Darken(0.4f), colours.Pink4),
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0f,
Blending = BlendingParameters.Additive,
Alpha = 0.2f,
Colour = ColourInfo.GradientVertical(colours.Pink2, colours.Pink4),
AlwaysPresent = true,
},
}
},
new OsuSpriteText
label = new OsuSpriteText
{
Text = Item.GetType().Name,
Font = OsuFont.Default.With(size: 10, weight: FontWeight.Bold),
Alpha = 0,
Anchor = Anchor.BottomRight,
Origin = Anchor.TopRight,
},
@ -83,7 +100,18 @@ namespace osu.Game.Overlays.SkinEditor
base.LoadComplete();
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()
@ -100,73 +128,73 @@ namespace osu.Game.Overlays.SkinEditor
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);
label.FadeTo(IsSelected || IsHovered ? 1 : 0, 200, Easing.OutQuint);
}
private Quad drawableQuad;
public override Quad ScreenSpaceDrawQuad => drawableQuad;
protected override void Update()
{
base.Update();
drawableQuad = drawable.ScreenSpaceDrawQuad;
var quad = ToLocalSpace(drawable.ScreenSpaceDrawQuad);
drawableQuad = drawable.ToScreenSpace(
drawable.DrawRectangle
.Inflate(SkinSelectionHandler.INFLATE_SIZE));
box.Position = drawable.ToSpaceOfOtherDrawable(Vector2.Zero, this);
box.Size = quad.Size;
var localSpaceQuad = ToLocalSpace(drawableQuad);
box.Position = localSpaceQuad.TopLeft;
box.Size = localSpaceQuad.Size;
box.Rotation = drawable.Rotation;
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
{
private readonly Drawable drawable;
private readonly Box originBox;
private Drawable originBox = null!;
private readonly Box anchorBox;
private readonly Box anchorLine;
private Drawable anchorBox = null!;
private Drawable anchorLine = null!;
public AnchorOriginVisualiser(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,
Colour = Color4.Yellow,
EdgeSmoothness = Vector2.One
Colour = ColourInfo.GradientHorizontal(originColour.Opacity(0.5f), originColour),
},
originBox = new Box
originBox = new Circle
{
Colour = Color4.Red,
Colour = originColour,
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,
Size = new Vector2(5),
Size = new Vector2(10),
},
};
}
private Vector2? anchorPosition;
private Vector2? originPositionInDrawableSpace;
protected override void Update()
{
base.Update();
@ -174,8 +202,13 @@ namespace osu.Game.Overlays.SkinEditor
if (drawable.Parent == null)
return;
originBox.Position = drawable.ToSpaceOfOtherDrawable(drawable.OriginPosition, this);
anchorBox.Position = drawable.Parent.ToSpaceOfOtherDrawable(drawable.AnchorPosition, this);
var newAnchor = 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 point2 = ToLocalSpace(originBox.ScreenSpaceDrawQuad.Centre);
@ -184,5 +217,11 @@ namespace osu.Game.Overlays.SkinEditor
anchorLine.Width = (point2 - point1).Length;
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 osu.Framework.Allocation;
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.Testing;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Skinning;
@ -24,18 +16,18 @@ using osuTK.Input;
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]
private SkinEditor editor { get; set; } = null!;
public SkinBlueprintContainer(Drawable target)
public SkinBlueprintContainer(ISerialisableDrawableContainer targetContainer)
{
this.target = target;
this.targetContainer = targetContainer;
}
protected override void LoadComplete()
@ -44,22 +36,10 @@ namespace osu.Game.Overlays.SkinEditor
SelectedItems.BindTo(editor.SelectedComponents);
// track each target container on the current screen.
var targetContainers = target.ChildrenOfType<ISkinnableTarget>().ToArray();
var bindableList = new BindableList<ISerialisableDrawable> { BindTarget = targetContainer.Components };
bindableList.BindCollectionChanged(componentsChanged, true);
if (targetContainers.Length == 0)
{
AddInternal(new NonSkinnableScreenPlaceholder());
return;
}
foreach (var targetContainer in targetContainers)
{
var bindableList = new BindableList<ISkinnableDrawable> { BindTarget = targetContainer.Components };
bindableList.BindCollectionChanged(componentsChanged, true);
targetComponents.Add(bindableList);
}
targetComponents.Add(bindableList);
}
private void componentsChanged(object? sender, NotifyCollectionChangedEventArgs e) => Schedule(() =>
@ -69,7 +49,7 @@ namespace osu.Game.Overlays.SkinEditor
case NotifyCollectionChangedAction.Add:
Debug.Assert(e.NewItems != null);
foreach (var item in e.NewItems.Cast<ISkinnableDrawable>())
foreach (var item in e.NewItems.Cast<ISerialisableDrawable>())
AddBlueprintFor(item);
break;
@ -77,7 +57,7 @@ namespace osu.Game.Overlays.SkinEditor
case NotifyCollectionChangedAction.Reset:
Debug.Assert(e.OldItems != null);
foreach (var item in e.OldItems.Cast<ISkinnableDrawable>())
foreach (var item in e.OldItems.Cast<ISerialisableDrawable>())
RemoveBlueprintFor(item);
break;
@ -85,16 +65,16 @@ namespace osu.Game.Overlays.SkinEditor
Debug.Assert(e.NewItems != null);
Debug.Assert(e.OldItems != null);
foreach (var item in e.OldItems.Cast<ISkinnableDrawable>())
foreach (var item in e.OldItems.Cast<ISerialisableDrawable>())
RemoveBlueprintFor(item);
foreach (var item in e.NewItems.Cast<ISkinnableDrawable>())
foreach (var item in e.NewItems.Cast<ISerialisableDrawable>())
AddBlueprintFor(item);
break;
}
});
protected override void AddBlueprintFor(ISkinnableDrawable item)
protected override void AddBlueprintFor(ISerialisableDrawable item)
{
if (!item.IsEditable)
return;
@ -145,12 +125,12 @@ namespace osu.Game.Overlays.SkinEditor
// convert to game space coordinates
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);
protected override void Dispose(bool isDisposing)
@ -160,65 +140,5 @@ namespace osu.Game.Overlays.SkinEditor
foreach (var list in targetComponents)
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.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Game.Graphics;
using osu.Framework.Threading;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osuTK;
@ -22,12 +23,12 @@ namespace osu.Game.Overlays.SkinEditor
{
public Action<Type>? RequestPlacement;
private readonly CompositeDrawable? target;
private readonly SkinComponentsContainer? target;
private FillFlowContainer fill = null!;
public SkinComponentToolbox(CompositeDrawable? target = null)
: base(SkinEditorStrings.Components)
public SkinComponentToolbox(SkinComponentsContainer? target = null)
: base(target?.Lookup.Ruleset == null ? SkinEditorStrings.Components : LocalisableString.Interpolate($"{SkinEditorStrings.Components} ({target.Lookup.Ruleset.Name})"))
{
this.target = target;
}
@ -50,7 +51,7 @@ namespace osu.Game.Overlays.SkinEditor
{
fill.Clear();
var skinnableTypes = SkinnableInfo.GetAllAvailableDrawables();
var skinnableTypes = SerialisedDrawableInfo.GetAllAvailableDrawables(target?.Lookup.Ruleset);
foreach (var type in skinnableTypes)
attemptAddComponent(type);
}
@ -61,11 +62,12 @@ namespace osu.Game.Overlays.SkinEditor
{
Drawable instance = (Drawable)Activator.CreateInstance(type)!;
if (!((ISkinnableDrawable)instance).IsEditable) return;
if (!((ISerialisableDrawable)instance).IsEditable) return;
fill.Add(new ToolboxComponentButton(instance, target)
{
RequestPlacement = t => RequestPlacement?.Invoke(t)
RequestPlacement = t => RequestPlacement?.Invoke(t),
Expanding = contractOtherButtons,
});
}
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 Action<Type>? RequestPlacement;
public Action<ToolboxComponentButton>? Expanding;
private readonly Drawable component;
private readonly CompositeDrawable? dependencySource;
private Container innerContainer = null!;
private ScheduledDelegate? expandContractAction;
private const float contracted_size = 60;
private const float expanded_size = 120;
@ -102,20 +118,45 @@ namespace osu.Game.Overlays.SkinEditor
Height = contracted_size;
}
private const double animation_duration = 500;
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);
}
protected override void OnHoverLost(HoverLostEvent 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]
private void load(OverlayColourProvider colourProvider, OsuColour colours)
private void load(OverlayColourProvider colourProvider)
{
BackgroundColour = colourProvider.Background3;

View File

@ -7,6 +7,7 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -24,6 +25,7 @@ using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Overlays.OSD;
using osu.Game.Overlays.Settings;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Edit.Components.Menus;
@ -38,7 +40,7 @@ namespace osu.Game.Overlays.SkinEditor
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;
@ -60,12 +62,17 @@ namespace osu.Game.Overlays.SkinEditor
[Resolved]
private RealmAccess realm { get; set; } = null!;
[Resolved]
private EditorClipboard clipboard { get; set; } = null!;
[Resolved]
private SkinEditorOverlay? skinEditorOverlay { get; set; }
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
private readonly Bindable<SkinComponentsContainerLookup?> selectedTarget = new Bindable<SkinComponentsContainerLookup?>();
private bool hasBegunMutating;
private Container? content;
@ -78,6 +85,15 @@ namespace osu.Game.Overlays.SkinEditor
private EditorMenuItem undoMenuItem = 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]
private OnScreenDisplay? onScreenDisplay { get; set; }
@ -143,6 +159,11 @@ namespace osu.Game.Overlays.SkinEditor
{
undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo),
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();
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();
game?.RegisterImportHandler(this);
@ -218,12 +254,26 @@ namespace osu.Game.Overlays.SkinEditor
}, true);
SelectedComponents.BindCollectionChanged((_, _) => Scheduler.AddOnce(populateSettings), true);
selectedTarget.BindValueChanged(targetChanged, true);
}
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
{
switch (e.Action)
{
case PlatformAction.Cut:
Cut();
return true;
case PlatformAction.Copy:
Copy();
return true;
case PlatformAction.Paste:
Paste();
return true;
case PlatformAction.Undo:
Undo();
return true;
@ -253,28 +303,83 @@ namespace osu.Game.Overlays.SkinEditor
changeHandler?.Dispose();
SelectedComponents.Clear();
// 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(populateSettings);
void loadBlueprintContainer()
{
Debug.Assert(content != null);
selectedTarget.Default = getFirstTarget()?.Lookup;
changeHandler = new SkinEditorChangeHandler(targetScreen);
changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
if (!availableTargets.Any(t => t.Lookup.Equals(selectedTarget.Value)))
selectedTarget.SetDefault();
}
}
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;
}
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))
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();
var targetContainer = getTarget(selectedTarget.Value);
if (targetContainer == null)
return;
return false;
var drawableComponent = (Drawable)component;
@ -325,10 +428,18 @@ namespace osu.Game.Overlays.SkinEditor
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);
return true;
}
private void populateSettings()
@ -339,28 +450,70 @@ namespace osu.Game.Overlays.SkinEditor
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()
{
ISkinnableTarget[] targetContainers = availableTargets.ToArray();
SkinComponentsContainer[] targetContainers = availableTargets.ToArray();
foreach (var t in targetContainers)
{
currentSkin.Value.ResetDrawableTarget(t);
// 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 Redo() => changeHandler?.RestoreState(1);
@ -370,7 +523,7 @@ namespace osu.Game.Overlays.SkinEditor
if (!hasBegunMutating)
return;
ISkinnableTarget[] targetContainers = availableTargets.ToArray();
SkinComponentsContainer[] targetContainers = availableTargets.ToArray();
foreach (var t in targetContainers)
currentSkin.Value.UpdateDrawableTarget(t);
@ -400,10 +553,57 @@ namespace osu.Game.Overlays.SkinEditor
this.FadeOut(TRANSITION_DURATION, Easing.OutQuint);
}
public void DeleteItems(ISkinnableDrawable[] items)
public void DeleteItems(ISerialisableDrawable[] items)
{
changeHandler?.BeginChange();
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
@ -440,6 +640,7 @@ namespace osu.Game.Overlays.SkinEditor
Position = skinnableTarget.ToLocalSpace(GetContainingInputManager().CurrentState.Mouse.Position),
};
SelectedComponents.Clear();
placeComponent(sprite, false);
SkinSelectionHandler.ApplyClosestAnchor(sprite);

View File

@ -9,19 +9,17 @@ using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Extensions;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
namespace osu.Game.Overlays.SkinEditor
{
public partial class SkinEditorChangeHandler : EditorChangeHandler
{
private readonly ISkinnableTarget? firstTarget;
private readonly ISerialisableDrawableContainer? firstTarget;
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
private readonly BindableList<ISkinnableDrawable>? components;
private readonly BindableList<ISerialisableDrawable>? components;
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`.
// 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)
return;
components = new BindableList<ISkinnableDrawable> { BindTarget = firstTarget.Components };
components = new BindableList<ISerialisableDrawable> { BindTarget = firstTarget.Components };
components.BindCollectionChanged((_, _) => SaveState());
}
@ -43,7 +41,7 @@ namespace osu.Game.Overlays.SkinEditor
if (firstTarget == null)
return;
var skinnableInfos = firstTarget.CreateSkinnableInfo().ToArray();
var skinnableInfos = firstTarget.CreateSerialisedInfo().ToArray();
string json = JsonConvert.SerializeObject(skinnableInfos, new JsonSerializerSettings { Formatting = Formatting.Indented });
stream.Write(Encoding.UTF8.GetBytes(json));
}
@ -53,12 +51,12 @@ namespace osu.Game.Overlays.SkinEditor
if (firstTarget == null)
return;
var deserializedContent = JsonConvert.DeserializeObject<IEnumerable<SkinnableInfo>>(Encoding.UTF8.GetString(newState));
var deserializedContent = JsonConvert.DeserializeObject<IEnumerable<SerialisedDrawableInfo>>(Encoding.UTF8.GetString(newState));
if (deserializedContent == null)
return;
SkinnableInfo[] skinnableInfo = deserializedContent.ToArray();
SerialisedDrawableInfo[] skinnableInfo = deserializedContent.ToArray();
Drawable[] targetComponents = firstTarget.Components.OfType<Drawable>().ToArray();
if (!skinnableInfo.Select(s => s.Type).SequenceEqual(targetComponents.Select(d => d.GetType())))
@ -71,7 +69,7 @@ namespace osu.Game.Overlays.SkinEditor
int i = 0;
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.Input.Bindings;
using osu.Game.Screens;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components;
using osuTK;
@ -28,6 +29,9 @@ namespace osu.Game.Overlays.SkinEditor
private SkinEditor? skinEditor;
[Cached]
public readonly EditorClipboard Clipboard = new EditorClipboard();
[Resolved]
private OsuGame game { get; set; } = null!;

View File

@ -13,13 +13,14 @@ using osu.Framework.Utils;
using osu.Game.Extensions;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Overlays.SkinEditor
{
public partial class SkinSelectionHandler : SelectionHandler<ISkinnableDrawable>
public partial class SkinSelectionHandler : SelectionHandler<ISerialisableDrawable>
{
[Resolved]
private SkinEditor skinEditor { get; set; } = null!;
@ -147,7 +148,7 @@ namespace osu.Game.Overlays.SkinEditor
return true;
}
public override bool HandleMovement(MoveSelectionEvent<ISkinnableDrawable> moveEvent)
public override bool HandleMovement(MoveSelectionEvent<ISerialisableDrawable> moveEvent)
{
foreach (var c in SelectedBlueprints)
{
@ -178,10 +179,10 @@ namespace osu.Game.Overlays.SkinEditor
SelectionBox.CanReverse = false;
}
protected override void DeleteItems(IEnumerable<ISkinnableDrawable> items) =>
protected override void DeleteItems(IEnumerable<ISerialisableDrawable> items) =>
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())
{
@ -206,10 +207,18 @@ namespace osu.Game.Overlays.SkinEditor
((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))
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[]
{

View File

@ -17,6 +17,7 @@ using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Overlays;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Edit.Tools;
@ -70,6 +71,7 @@ namespace osu.Game.Rulesets.Edit
private FillFlowContainer togglesCollection;
private IBindable<bool> hasTiming;
private Bindable<bool> autoSeekOnPlacement;
protected HitObjectComposer(Ruleset ruleset)
: base(ruleset)
@ -80,8 +82,10 @@ namespace osu.Game.Rulesets.Edit
dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
[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);
try
@ -365,7 +369,7 @@ namespace osu.Game.Rulesets.Edit
{
EditorBeatmap.Add(hitObject);
if (EditorClock.CurrentTime < hitObject.StartTime)
if (autoSeekOnPlacement.Value && EditorClock.CurrentTime < hitObject.StartTime)
EditorClock.SeekSmoothlyTo(hitObject.StartTime);
}
}

View File

@ -3,6 +3,7 @@
#nullable disable
using System;
using JetBrains.Annotations;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
@ -33,16 +34,30 @@ namespace osu.Game.Rulesets.Judgements
public readonly Judgement Judgement;
/// <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"/>.
/// </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>
/// The absolute time at which this <see cref="JudgementResult"/> occurred.
/// Equal to the (end) time of the <see cref="HitObject"/> + <see cref="TimeOffset"/>.
/// The offset of <see cref="TimeAbsolute"/> from the end time of <see cref="HitObject"/>, clamped by <see cref="osu.Game.Rulesets.Objects.HitObject.MaximumJudgementOffset"/>.
/// </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>
/// The combo prior to this <see cref="JudgementResult"/> occurring.
@ -83,6 +98,13 @@ namespace osu.Game.Rulesets.Judgements
{
HitObject = hitObject;
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})";

View File

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

View File

@ -67,7 +67,8 @@ namespace osu.Game.Rulesets.Mods
{
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);
}

View File

@ -82,6 +82,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// <summary>
/// Invoked by this or a nested <see cref="DrawableHitObject"/> prior to a <see cref="JudgementResult"/> being reverted.
/// </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;
/// <summary>
@ -222,6 +225,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
ensureEntryHasResult();
entry.RevertResult += onRevertResult;
foreach (var h in HitObject.NestedHitObjects)
{
var pooledDrawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h, this);
@ -234,7 +239,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
OnNestedDrawableCreated?.Invoke(drawableNested);
drawableNested.OnNewResult += onNewResult;
drawableNested.OnRevertResult += onRevertResult;
drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState;
// 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)
{
obj.OnNewResult -= onNewResult;
obj.OnRevertResult -= onRevertResult;
obj.ApplyCustomUpdateState -= onApplyCustomUpdateState;
}
@ -317,6 +320,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
HitObject.DefaultsApplied -= onDefaultsApplied;
entry.RevertResult -= onRevertResult;
OnFree();
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 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);
@ -577,26 +586,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
#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;
protected override void UpdateAfterChildren()
@ -650,27 +639,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
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>
/// Applies the <see cref="Result"/> of this <see cref="DrawableHitObject"/>, notifying responders such as
/// 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}]).");
}
Result.TimeOffset = Math.Min(MaximumJudgementOffset, Time.Current - HitObject.GetEndTime());
Result.RawTime = Time.Current;
if (Result.HasResult)
updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss);

View File

@ -200,6 +200,14 @@ namespace osu.Game.Rulesets.Objects
[NotNull]
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()
{
var slidingSamples = new List<HitSampleInfo>();

View File

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

View File

@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.UI
/// <summary>
/// The key conversion input manager for this DrawableRuleset.
/// </summary>
public PassThroughInputManager KeyBindingInputManager;
protected PassThroughInputManager KeyBindingInputManager;
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 IAdjustableAudioComponent Audio => audioContainer;
private readonly AudioContainer audioContainer = new AudioContainer { RelativeSizeAxes = Axes.Both };
public override Container FrameStableComponents { get; } = new Container { RelativeSizeAxes = Axes.Both };
public override IFrameStableClock FrameStableClock => frameStabilityContainer;
@ -102,14 +106,6 @@ namespace osu.Game.Rulesets.UI
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>
/// Creates a ruleset visualisation for the provided ruleset and beatmap.
/// </summary>
@ -134,7 +130,7 @@ namespace osu.Game.Rulesets.UI
playfield = new Lazy<Playfield>(() => CreatePlayfield().With(p =>
{
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]
private void load(CancellationToken? cancellationToken)
{
AudioContainer audioContainer;
InternalChild = frameStabilityContainer = new FrameStabilityContainer(GameplayStartTime)
{
FrameStablePlayback = FrameStablePlayback,
Children = new Drawable[]
{
FrameStableComponents,
audioContainer = new AudioContainer
{
RelativeSizeAxes = Axes.Both,
Child = KeyBindingInputManager
.WithChild(CreatePlayfieldAdjustmentContainer()
.WithChild(Playfield)
),
},
Overlays,
audioContainer.WithChild(KeyBindingInputManager
.WithChildren(new Drawable[]
{
CreatePlayfieldAdjustmentContainer()
.WithChild(Playfield),
Overlays
})),
}
};
Audio = audioContainer;
if ((ResumeOverlay = CreateResumeOverlay()) != null)
{
AddInternal(CreateInputManager()
@ -230,7 +220,7 @@ namespace osu.Game.Rulesets.UI
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.ResumeAction = continueResume;
@ -436,13 +426,18 @@ namespace osu.Game.Rulesets.UI
/// </summary>
public readonly BindableBool IsPaused = new BindableBool();
/// <summary>
/// Audio adjustments which are applied to the playfield.
/// </summary>
public abstract IAdjustableAudioComponent Audio { get; }
/// <summary>
/// The playfield.
/// </summary>
public abstract Playfield Playfield { get; }
/// <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>
public abstract Container Overlays { get; }
@ -507,6 +502,15 @@ namespace osu.Game.Rulesets.UI
/// </summary>
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>
/// Returns first available <see cref="HitWindows"/> provided by a <see cref="HitObject"/>.
/// </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;
/// <summary>

View File

@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.UI
@ -68,27 +69,61 @@ namespace osu.Game.Rulesets.UI
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.
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 (hitObject == null)
if (drawableHitObject != null)
{
// 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);
// A hit object may have a more valid nested object.
drawableHitObject = getMostValidNestedDrawable(drawableHitObject);
// In the case there are no unjudged objects, the last hit object should be used instead.
fallbackObject ??= hitObjectContainer.Entries.LastOrDefault();
}
hitObject = fallbackObject?.HitObject;
return drawableHitObject.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()

View File

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

View File

@ -22,6 +22,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning;
using osuTK;
using osu.Game.Rulesets.Objects.Pooling;
using osu.Framework.Extensions.ObjectExtensions;
namespace osu.Game.Rulesets.UI
{
@ -35,9 +36,9 @@ namespace osu.Game.Rulesets.UI
public event Action<DrawableHitObject, JudgementResult> NewResult;
/// <summary>
/// Invoked when a <see cref="DrawableHitObject"/> judgement is reverted.
/// Invoked when a judgement result is reverted.
/// </summary>
public event Action<DrawableHitObject, JudgementResult> RevertResult;
public event Action<JudgementResult> RevertResult;
/// <summary>
/// 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 Stack<HitObjectLifetimeEntry> judgedEntries;
/// <summary>
/// Creates a new <see cref="Playfield"/>.
/// </summary>
@ -107,14 +110,15 @@ namespace osu.Game.Rulesets.UI
hitObjectContainerLazy = new Lazy<HitObjectContainer>(() => CreateHitObjectContainer().With(h =>
{
h.NewResult += (d, r) => NewResult?.Invoke(d, r);
h.RevertResult += (d, r) => RevertResult?.Invoke(d, r);
h.NewResult += onNewResult;
h.HitObjectUsageBegan += o => HitObjectUsageBegan?.Invoke(o);
h.HitObjectUsageFinished += o => HitObjectUsageFinished?.Invoke(o);
}));
entryManager.OnEntryAdded += onEntryAdded;
entryManager.OnEntryRemoved += onEntryRemoved;
judgedEntries = new Stack<HitObjectLifetimeEntry>();
}
[BackgroundDependencyLoader]
@ -224,7 +228,7 @@ namespace osu.Game.Rulesets.UI
otherPlayfield.DisplayJudgements.BindTo(DisplayJudgements);
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.HitObjectUsageFinished += h => HitObjectUsageFinished?.Invoke(h);
@ -252,6 +256,18 @@ namespace osu.Game.Rulesets.UI
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>
@ -443,6 +459,25 @@ namespace osu.Game.Rulesets.UI
#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
/// <summary>

View File

@ -232,8 +232,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
double computedStartTime = computeDisplayStartTime(entry);
// 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 - judgementOffset, computedStartTime);
entry.LifetimeStart = Math.Min(entry.HitObject.StartTime - entry.HitObject.MaximumJudgementOffset, computedStartTime);
}
private void updateLayoutRecursive(DrawableHitObject hitObject, double? parentHitObjectStartTime = null)

View File

@ -32,6 +32,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary>
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>
/// The currently selected 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++)
selectionRect = RectangleF.Union(selectionRect, ToLocalSpace(selectedBlueprints[i].SelectionQuad).AABBFloat);
selectionRect = selectionRect.Inflate(5f);
selectionRect = selectionRect.Inflate(INFLATE_SIZE);
SelectionBox.Position = selectionRect.Location;
SelectionBox.Size = selectionRect.Size;

View File

@ -157,7 +157,16 @@ namespace osu.Game.Screens.Edit
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)
=> dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
@ -176,6 +185,7 @@ namespace osu.Game.Screens.Edit
private Bindable<float> editorBackgroundDim;
private Bindable<bool> editorHitMarkers;
private Bindable<bool> editorAutoSeekOnPlacement;
public Editor(EditorLoader loader = null)
{
@ -263,6 +273,7 @@ namespace osu.Game.Screens.Edit
editorBackgroundDim = config.GetBindable<float>(OsuSetting.EditorDim);
editorHitMarkers = config.GetBindable<bool>(OsuSetting.EditorShowHitMarkers);
editorAutoSeekOnPlacement = config.GetBindable<bool>(OsuSetting.EditorAutoSeekOnPlacement);
AddInternal(new OsuContextMenuContainer
{
@ -320,6 +331,10 @@ namespace osu.Game.Screens.Edit
new ToggleMenuItem(EditorStrings.ShowHitMarkers)
{
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.Overlays;
using osu.Game.Screens.Play;
using osu.Game.Users;
namespace osu.Game.Screens.Edit.GameplayTest
{
@ -15,6 +16,8 @@ namespace osu.Game.Screens.Edit.GameplayTest
private readonly Editor editor;
private readonly EditorState editorState;
protected override UserActivity InitialActivity => new UserActivity.TestingBeatmap(Beatmap.Value.BeatmapInfo, Ruleset.Value);
[Resolved]
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.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@ -22,29 +20,21 @@ namespace osu.Game.Screens.Play.Break
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Container
new Box
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
RelativeSizeAxes = Axes.X,
Height = height,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(Color4.Black, transparent_black),
}
Colour = ColourInfo.GradientVertical(Color4.Black, transparent_black),
},
new Container
new Box
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Height = height,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(transparent_black, Color4.Black),
}
Colour = ColourInfo.GradientVertical(transparent_black, Color4.Black),
}
};
}

View File

@ -16,7 +16,7 @@ using osuTK;
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;

View File

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

View File

@ -7,7 +7,7 @@ using osu.Game.Skinning;
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; }

View File

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

View File

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

View File

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

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