1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-28 09:02:58 +08:00

Merge branch 'master' into skin-editor-layer-select

This commit is contained in:
Bartłomiej Dach 2023-02-21 21:38:51 +01:00
commit 051eb18b33
No known key found for this signature in database
6 changed files with 318 additions and 14 deletions

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

@ -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

@ -12,6 +12,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;
@ -27,6 +28,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
[Cached]
public readonly EditorClipboard Clipboard = new EditorClipboard();
[SetUpSteps]
public override void SetUpSteps()
{

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

@ -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;
@ -61,6 +62,9 @@ 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; }
@ -81,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; }
@ -146,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),
}
},
}
@ -204,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);
@ -229,6 +262,18 @@ namespace osu.Game.Overlays.SkinEditor
{
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;
@ -316,15 +361,24 @@ namespace osu.Game.Overlays.SkinEditor
{
componentsSidebar.Add(new SkinComponentToolbox(skinComponentsContainer)
{
RequestPlacement = placeComponent
RequestPlacement = requestPlacement
});
}
// Remove the ruleset from the lookup to get base components.
componentsSidebar.Add(new SkinComponentToolbox(getTarget(new SkinComponentsContainerLookup(target.NewValue.Target)))
{
RequestPlacement = placeComponent
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);
}
}
private void skinChanged()
@ -349,20 +403,18 @@ namespace osu.Game.Overlays.SkinEditor
hasBegunMutating = true;
}
private void placeComponent(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)}.");
placeComponent(component);
}
private void placeComponent(ISerialisableDrawable component, bool applyDefaults = true)
/// <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)
{
var targetContainer = getTarget(selectedTarget.Value);
if (targetContainer == null)
return;
return false;
var drawableComponent = (Drawable)component;
@ -374,10 +426,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()
@ -410,6 +470,48 @@ namespace osu.Game.Overlays.SkinEditor
}
}
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);
@ -451,8 +553,12 @@ namespace osu.Game.Overlays.SkinEditor
public void DeleteItems(ISerialisableDrawable[] items)
{
changeHandler?.BeginChange();
foreach (var item in items)
availableTargets.FirstOrDefault(t => t.Components.Contains(item))?.Remove(item);
changeHandler?.EndChange();
}
#region Drag & drop import handling
@ -489,6 +595,7 @@ namespace osu.Game.Overlays.SkinEditor
Position = skinnableTarget.ToLocalSpace(GetContainingInputManager().CurrentState.Mouse.Position),
};
SelectedComponents.Clear();
placeComponent(sprite, false);
SkinSelectionHandler.ApplyClosestAnchor(sprite);

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!;