1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 17:32:54 +08:00

Merge branch 'master' into osu-markdown

This commit is contained in:
Gagah Pangeran Rosfatiputra 2021-05-06 16:23:20 +07:00
commit 67f51959db
No known key found for this signature in database
GPG Key ID: 25F6F17FD29031E2
30 changed files with 731 additions and 100 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.427.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.506.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,36 @@
// 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.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Skinning.Editor;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSkinEditor : PlayerTestScene
{
private SkinEditor skinEditor;
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("add editor overlay", () =>
{
skinEditor?.Expire();
LoadComponentAsync(skinEditor = new SkinEditor(Player), Add);
});
}
[Test]
public void TestToggleEditor()
{
AddToggleStep("toggle editor visibility", visible => skinEditor.ToggleVisibility());
}
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
}
}

View File

@ -0,0 +1,61 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Skinning.Editor;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSkinEditorMultipleSkins : SkinnableTestScene
{
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create editor overlay", () =>
{
SetContents(() =>
{
var ruleset = new OsuRuleset();
var working = CreateWorkingBeatmap(ruleset.RulesetInfo);
var beatmap = working.GetPlayableBeatmap(ruleset.RulesetInfo);
ScoreProcessor scoreProcessor = new ScoreProcessor();
var drawableRuleset = ruleset.CreateDrawableRulesetWith(beatmap);
var hudOverlay = new HUDOverlay(scoreProcessor, null, drawableRuleset, Array.Empty<Mod>())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
// Add any key just to display the key counter visually.
hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
hudOverlay.ComboCounter.Current.Value = 1;
return new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
drawableRuleset,
hudOverlay,
new SkinEditor(hudOverlay),
}
};
});
});
}
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
}
}

View File

@ -7,6 +7,7 @@ using JetBrains.Annotations;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
@ -112,8 +113,8 @@ namespace osu.Game.Tests.Visual.SongSelect
private void testInfoLabels(int expectedCount)
{
AddAssert("check info labels exists", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.BufferedWedgeInfo.InfoLabel>().Any());
AddAssert("check info labels count", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.BufferedWedgeInfo.InfoLabel>().Count() == expectedCount);
AddAssert("check info labels exists", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Any());
AddAssert("check info labels count", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Count() == expectedCount);
}
[Test]
@ -124,7 +125,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("check default title", () => infoWedge.Info.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title);
AddAssert("check default artist", () => infoWedge.Info.ArtistLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Artist);
AddAssert("check empty author", () => !infoWedge.Info.MapperContainer.Children.Any());
AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.BufferedWedgeInfo.InfoLabel>().Any());
AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Any());
}
[Test]
@ -135,15 +136,15 @@ namespace osu.Game.Tests.Visual.SongSelect
private void selectBeatmap([CanBeNull] IBeatmap b)
{
BeatmapInfoWedge.BufferedWedgeInfo infoBefore = null;
Container containerBefore = null;
AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () =>
{
infoBefore = infoWedge.Info;
containerBefore = infoWedge.DisplayedContent;
infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b);
});
AddUntilStep("wait for async load", () => infoWedge.Info != infoBefore);
AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
}
private IBeatmap createTestBeatmap(RulesetInfo ruleset)
@ -193,7 +194,9 @@ namespace osu.Game.Tests.Visual.SongSelect
private class TestBeatmapInfoWedge : BeatmapInfoWedge
{
public new BufferedWedgeInfo Info => base.Info;
public new Container DisplayedContent => base.DisplayedContent;
public new WedgeInfoText Info => base.Info;
}
private class TestHitObject : ConvertHitObject, IHasPosition

View File

@ -36,6 +36,24 @@ namespace osu.Game.Graphics.Containers
private BackgroundScreenStack backgroundStack;
private bool allowScaling = true;
/// <summary>
/// Whether user scaling preferences should be applied. Enabled by default.
/// </summary>
public bool AllowScaling
{
get => allowScaling;
set
{
if (value == allowScaling)
return;
allowScaling = value;
if (IsLoaded) updateSize();
}
}
/// <summary>
/// Create a new instance.
/// </summary>
@ -139,7 +157,7 @@ namespace osu.Game.Graphics.Containers
backgroundStack?.FadeOut(fade_time);
}
bool scaling = targetMode == null || scalingMode.Value == targetMode;
bool scaling = AllowScaling && (targetMode == null || scalingMode.Value == targetMode);
var targetSize = scaling ? new Vector2(sizeX.Value, sizeY.Value) : Vector2.One;
var targetPosition = scaling ? new Vector2(posX.Value, posY.Value) * (Vector2.One - targetSize) : Vector2.Zero;

View File

@ -48,6 +48,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings),
new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleBeatmapListing),
new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications),
new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.ToggleSkinEditor),
new KeyBinding(InputKey.Escape, GlobalAction.Back),
new KeyBinding(InputKey.ExtraMouseButton1, GlobalAction.Back),
@ -258,6 +259,9 @@ namespace osu.Game.Input.Bindings
EditorNudgeLeft,
[Description("Nudge selection right")]
EditorNudgeRight
EditorNudgeRight,
[Description("Toggle skin editor")]
ToggleSkinEditor,
}
}

View File

@ -51,6 +51,7 @@ using osu.Game.Utils;
using LogLevel = osu.Framework.Logging.LogLevel;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Skinning.Editor;
namespace osu.Game
{
@ -79,6 +80,8 @@ namespace osu.Game
private BeatmapSetOverlay beatmapSetOverlay;
private SkinEditorOverlay skinEditor;
[Cached]
private readonly DifficultyRecommender difficultyRecommender = new DifficultyRecommender();
@ -597,6 +600,8 @@ namespace osu.Game
screenContainer = new ScalingContainer(ScalingMode.ExcludeOverlays)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
receptor = new BackButton.Receptor(),
@ -685,6 +690,7 @@ namespace osu.Game
var changelogOverlay = loadComponentSingleFile(new ChangelogOverlay(), overlayContent.Add, true);
loadComponentSingleFile(userProfile = new UserProfileOverlay(), overlayContent.Add, true);
loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true);
loadComponentSingleFile(skinEditor = new SkinEditorOverlay(screenContainer), overlayContent.Add);
loadComponentSingleFile(new LoginOverlay
{
@ -968,6 +974,8 @@ namespace osu.Game
protected virtual void ScreenChanged(IScreen current, IScreen newScreen)
{
skinEditor.Reset();
switch (newScreen)
{
case IntroScreen intro:

View File

@ -57,11 +57,11 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
{
base.LoadComplete();
backgroundSourceDropdown.Current.BindValueChanged(source =>
user.BindValueChanged(u =>
{
const string not_supporter_note = "Changes to this setting will only apply with an active osu!supporter tag.";
backgroundSourceDropdown.WarningText = user.Value?.IsSupporter != true ? not_supporter_note : string.Empty;
backgroundSourceDropdown.WarningText = u.NewValue?.IsSupporter != true ? not_supporter_note : string.Empty;
}, true);
}
}

View File

@ -108,17 +108,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
}
/// <summary>
/// Given a selection target and a function of truth, retrieve the correct ternary state for display.
/// </summary>
protected TernaryState GetStateFromSelection<T>(IEnumerable<T> selection, Func<T, bool> func)
{
if (selection.Any(func))
return selection.All(func) ? TernaryState.True : TernaryState.Indeterminate;
return TernaryState.False;
}
#endregion
#region Ternary state changes

View File

@ -236,6 +236,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
DeleteSelected();
}
/// <summary>
/// Given a selection target and a function of truth, retrieve the correct ternary state for display.
/// </summary>
protected static TernaryState GetStateFromSelection<TObject>(IEnumerable<TObject> selection, Func<TObject, bool> func)
{
if (selection.Any(func))
return selection.All(func) ? TernaryState.True : TernaryState.Indeterminate;
return TernaryState.False;
}
/// <summary>
/// Called whenever the deletion of items has been requested.
/// </summary>
@ -274,8 +285,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
int count = SelectedItems.Count;
SelectionBox.Text = count > 0 ? count.ToString() : string.Empty;
SelectionBox.FadeTo(count > 0 ? 1 : 0);
OnSelectionChanged();
}

View File

@ -9,7 +9,7 @@ using osuTK;
namespace osu.Game.Screens.Play.HUD
{
public class DefaultAccuracyCounter : PercentageCounter, IAccuracyCounter
public class DefaultAccuracyCounter : PercentageCounter, IAccuracyCounter, ISkinnableComponent
{
private readonly Vector2 offset = new Vector2(-20, 5);

View File

@ -11,7 +11,7 @@ using osuTK;
namespace osu.Game.Screens.Play.HUD
{
public class DefaultComboCounter : RollingCounter<int>, IComboCounter
public class DefaultComboCounter : RollingCounter<int>, IComboCounter, ISkinnableComponent
{
private readonly Vector2 offset = new Vector2(20, 5);

View File

@ -16,7 +16,7 @@ using osu.Framework.Utils;
namespace osu.Game.Screens.Play.HUD
{
public class DefaultHealthDisplay : HealthDisplay, IHasAccentColour
public class DefaultHealthDisplay : HealthDisplay, IHasAccentColour, ISkinnableComponent
{
/// <summary>
/// The base opacity of the glow.

View File

@ -8,7 +8,7 @@ using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Play.HUD
{
public class DefaultScoreCounter : ScoreCounter
public class DefaultScoreCounter : ScoreCounter, ISkinnableComponent
{
public DefaultScoreCounter()
: base(6)

View File

@ -18,7 +18,7 @@ using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD.HitErrorMeters
{
public class BarHitErrorMeter : HitErrorMeter
public class BarHitErrorMeter : HitErrorMeter, ISkinnableComponent
{
private readonly Anchor alignment;

View File

@ -0,0 +1,14 @@
// 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;
namespace osu.Game.Screens.Play.HUD
{
/// <summary>
/// Denotes a drawable which, as a drawable, can be adjusted via skinning specifications.
/// </summary>
public interface ISkinnableComponent : IDrawable
{
}
}

View File

@ -14,7 +14,7 @@ namespace osu.Game.Screens.Play.HUD
/// <summary>
/// Uses the 'x' symbol and has a pop-out effect while rolling over.
/// </summary>
public class LegacyComboCounter : CompositeDrawable, IComboCounter
public class LegacyComboCounter : CompositeDrawable, IComboCounter, ISkinnableComponent
{
public Bindable<int> Current { get; } = new BindableInt { MinValue = 0, };

View File

@ -14,6 +14,7 @@ using osu.Framework.Timing;
using osu.Game.Configuration;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Screens.Play
{
@ -71,30 +72,38 @@ namespace osu.Game.Screens.Play
public SongProgress()
{
Masking = true;
Children = new Drawable[]
{
info = new SongProgressInfo
new SongProgressDisplay
{
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Height = info_height,
},
graph = new SongProgressGraph
{
RelativeSizeAxes = Axes.X,
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
Height = graph_height,
Margin = new MarginPadding { Bottom = bottom_bar_height },
},
bar = new SongProgressBar(bottom_bar_height, graph_height, handle_size)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
OnSeek = time => RequestSeek?.Invoke(time),
Masking = true,
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Children = new Drawable[]
{
info = new SongProgressInfo
{
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Height = info_height,
},
graph = new SongProgressGraph
{
RelativeSizeAxes = Axes.X,
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
Height = graph_height,
Margin = new MarginPadding { Bottom = bottom_bar_height },
},
bar = new SongProgressBar(bottom_bar_height, graph_height, handle_size)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
OnSeek = time => RequestSeek?.Invoke(time),
},
}
},
};
}
@ -175,5 +184,11 @@ namespace osu.Game.Screens.Play
float finalMargin = bottom_bar_height + (AllowSeeking.Value ? handle_size.Y : 0) + (ShowGraph.Value ? graph_height : 0);
info.TransformTo(nameof(info.Margin), new MarginPadding { Bottom = finalMargin }, transition_duration, Easing.In);
}
public class SongProgressDisplay : Container, ISkinnableComponent
{
// TODO: move actual implementation into this.
// exists for skin customisation purposes.
}
}
}

View File

@ -11,7 +11,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
@ -49,7 +48,9 @@ namespace osu.Game.Screens.Select
private IBindable<StarDifficulty?> beatmapDifficulty;
protected BufferedWedgeInfo Info;
protected Container DisplayedContent { get; private set; }
protected WedgeInfoText Info { get; private set; }
public BeatmapInfoWedge()
{
@ -110,9 +111,9 @@ namespace osu.Game.Screens.Select
}
}
public override bool IsPresent => base.IsPresent || Info == null; // Visibility is updated in the LoadComponentAsync callback
public override bool IsPresent => base.IsPresent || DisplayedContent == null; // Visibility is updated in the LoadComponentAsync callback
private BufferedWedgeInfo loadingInfo;
private Container loadingInfo;
private void updateDisplay()
{
@ -124,9 +125,9 @@ namespace osu.Game.Screens.Select
{
State.Value = beatmap == null ? Visibility.Hidden : Visibility.Visible;
Info?.FadeOut(250);
Info?.Expire();
Info = null;
DisplayedContent?.FadeOut(250);
DisplayedContent?.Expire();
DisplayedContent = null;
}
if (beatmap == null)
@ -135,17 +136,23 @@ namespace osu.Game.Screens.Select
return;
}
LoadComponentAsync(loadingInfo = new BufferedWedgeInfo(beatmap, ruleset.Value, mods.Value, beatmapDifficulty.Value ?? new StarDifficulty())
LoadComponentAsync(loadingInfo = new Container
{
RelativeSizeAxes = Axes.Both,
Shear = -Shear,
Depth = Info?.Depth + 1 ?? 0
Depth = DisplayedContent?.Depth + 1 ?? 0,
Children = new Drawable[]
{
new BeatmapInfoWedgeBackground(beatmap),
Info = new WedgeInfoText(beatmap, ruleset.Value, mods.Value, beatmapDifficulty.Value ?? new StarDifficulty()),
}
}, loaded =>
{
// ensure we are the most recent loaded wedge.
if (loaded != loadingInfo) return;
removeOldInfo();
Add(Info = loaded);
Add(DisplayedContent = loaded);
});
}
}
@ -156,7 +163,7 @@ namespace osu.Game.Screens.Select
cancellationSource?.Cancel();
}
public class BufferedWedgeInfo : BufferedContainer
public class WedgeInfoText : Container
{
public OsuSpriteText VersionLabel { get; private set; }
public OsuSpriteText TitleLabel { get; private set; }
@ -176,8 +183,7 @@ namespace osu.Game.Screens.Select
private ModSettingChangeTracker settingChangeTracker;
public BufferedWedgeInfo(WorkingBeatmap beatmap, RulesetInfo userRuleset, IReadOnlyList<Mod> mods, StarDifficulty difficulty)
: base(pixelSnapping: true)
public WedgeInfoText(WorkingBeatmap beatmap, RulesetInfo userRuleset, IReadOnlyList<Mod> mods, StarDifficulty difficulty)
{
this.beatmap = beatmap;
ruleset = userRuleset ?? beatmap.BeatmapInfo.Ruleset;
@ -191,7 +197,6 @@ namespace osu.Game.Screens.Select
var beatmapInfo = beatmap.BeatmapInfo;
var metadata = beatmapInfo.Metadata ?? beatmap.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata();
CacheDrawnFrameBuffer = true;
RelativeSizeAxes = Axes.Both;
titleBinding = localisation.GetLocalisedString(new RomanisableString(metadata.TitleUnicode, metadata.Title));
@ -199,32 +204,6 @@ namespace osu.Game.Screens.Select
Children = new Drawable[]
{
// We will create the white-to-black gradient by modulating transparency and having
// a black backdrop. This results in an sRGB-space gradient and not linear space,
// transitioning from white to black more perceptually uniformly.
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
},
// We use a container, such that we can set the colour gradient to go across the
// vertices of the masked container instead of the vertices of the (larger) sprite.
new Container
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(Color4.White, Color4.White.Opacity(0.3f)),
Children = new[]
{
// Zoomed-in and cropped beatmap background
new BeatmapBackgroundSprite(beatmap)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
FillMode = FillMode.Fill,
},
},
},
new DifficultyColourBar(starDifficulty)
{
RelativeSizeAxes = Axes.Y,
@ -340,7 +319,6 @@ namespace osu.Game.Screens.Select
{
ArtistLabel.Text = artistBinding.Value;
TitleLabel.Text = string.IsNullOrEmpty(source) ? titleBinding.Value : source + " — " + titleBinding.Value;
ForceRedraw();
}
private void addInfoLabels()
@ -426,8 +404,6 @@ namespace osu.Game.Screens.Select
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm),
Content = labelText
});
ForceRedraw();
}
private OsuSpriteText[] getMapper(BeatmapMetadata metadata)

View File

@ -0,0 +1,66 @@
// 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 osuTK.Graphics;
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.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Framework.Graphics.Shapes;
namespace osu.Game.Screens.Select
{
internal class BeatmapInfoWedgeBackground : CompositeDrawable
{
private readonly WorkingBeatmap beatmap;
public BeatmapInfoWedgeBackground(WorkingBeatmap beatmap)
{
this.beatmap = beatmap;
}
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.Both;
InternalChild = new BufferedContainer
{
CacheDrawnFrameBuffer = true,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
// We will create the white-to-black gradient by modulating transparency and having
// a black backdrop. This results in an sRGB-space gradient and not linear space,
// transitioning from white to black more perceptually uniformly.
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
},
// We use a container, such that we can set the colour gradient to go across the
// vertices of the masked container instead of the vertices of the (larger) sprite.
new Container
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(Color4.White, Color4.White.Opacity(0.3f)),
Children = new[]
{
// Zoomed-in and cropped beatmap background
new BeatmapBackgroundSprite(beatmap)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
FillMode = FillMode.Fill,
},
},
},
}
};
}
}
}

View File

@ -0,0 +1,78 @@
// 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.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Play.HUD;
using osuTK;
namespace osu.Game.Skinning.Editor
{
public class SkinBlueprint : SelectionBlueprint<ISkinnableComponent>
{
private Container box;
private Drawable drawable => (Drawable)Item;
/// <summary>
/// Whether the blueprint should be shown even when the <see cref="SelectionBlueprint{T}.Item"/> is not alive.
/// </summary>
protected virtual bool AlwaysShowWhenSelected => false;
protected override bool ShouldBeAlive => (drawable.IsAlive && Item.IsPresent) || (AlwaysShowWhenSelected && State == SelectionState.Selected);
public SkinBlueprint(ISkinnableComponent component)
: base(component)
{
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
InternalChildren = new Drawable[]
{
box = new Container
{
Colour = colours.Yellow,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.2f,
AlwaysPresent = true,
},
}
},
};
}
private Quad drawableQuad;
public override Quad ScreenSpaceDrawQuad => drawableQuad;
protected override void Update()
{
base.Update();
drawableQuad = drawable.ScreenSpaceDrawQuad;
var quad = ToLocalSpace(drawable.ScreenSpaceDrawQuad);
box.Position = quad.TopLeft;
box.Size = quad.Size;
box.Rotation = drawable.Rotation;
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => drawable.ReceivePositionalInputAt(screenSpacePos);
public override Vector2 ScreenSpaceSelectionPoint => drawable.ScreenSpaceDrawQuad.Centre;
public override Quad SelectionQuad => drawable.ScreenSpaceDrawQuad;
}
}

View File

@ -0,0 +1,43 @@
// 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 osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Skinning.Editor
{
public class SkinBlueprintContainer : BlueprintContainer<ISkinnableComponent>
{
private readonly Drawable target;
public SkinBlueprintContainer(Drawable target)
{
this.target = target;
}
protected override void LoadComplete()
{
base.LoadComplete();
checkForComponents();
}
private void checkForComponents()
{
foreach (var c in target.ChildrenOfType<ISkinnableComponent>().ToArray()) AddBlueprintFor(c);
// We'd hope to eventually be running this in a more sensible way, but this handles situations where new drawables become present (ie. during ongoing gameplay)
// or when drawables in the target are loaded asynchronously and may not be immediately available when this BlueprintContainer is loaded.
Scheduler.AddDelayed(checkForComponents, 1000);
}
protected override SelectionHandler<ISkinnableComponent> CreateSelectionHandler() => new SkinSelectionHandler();
protected override SelectionBlueprint<ISkinnableComponent> CreateBlueprintFor(ISkinnableComponent component)
=> new SkinBlueprint(component);
}
}

View File

@ -0,0 +1,79 @@
// 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.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
namespace osu.Game.Skinning.Editor
{
public class SkinEditor : FocusedOverlayContainer
{
public const double TRANSITION_DURATION = 500;
private readonly Drawable target;
private OsuTextFlowContainer headerText;
protected override bool StartHidden => true;
public SkinEditor(Drawable target)
{
this.target = target;
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
InternalChild = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
headerText = new OsuTextFlowContainer
{
TextAnchor = Anchor.TopCentre,
Padding = new MarginPadding(20),
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X
},
new SkinBlueprintContainer(target),
}
};
headerText.AddParagraph("Skin editor (preview)", cp => cp.Font = OsuFont.Default.With(size: 24));
headerText.AddParagraph("This is a preview of what is to come. Changes are lost on changing screens.", cp =>
{
cp.Font = OsuFont.Default.With(size: 12);
cp.Colour = colours.Yellow;
});
}
protected override void LoadComplete()
{
base.LoadComplete();
Show();
}
protected override bool OnHover(HoverEvent e) => true;
protected override bool OnMouseDown(MouseDownEvent e) => true;
protected override void PopIn()
{
this.FadeIn(TRANSITION_DURATION, Easing.OutQuint);
}
protected override void PopOut()
{
this.FadeOut(TRANSITION_DURATION, Easing.OutQuint);
}
}
}

View File

@ -0,0 +1,97 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings;
namespace osu.Game.Skinning.Editor
{
/// <summary>
/// A container which handles loading a skin editor on user request for a specified target.
/// This also handles the scaling / positioning adjustment of the target.
/// </summary>
public class SkinEditorOverlay : CompositeDrawable, IKeyBindingHandler<GlobalAction>
{
private readonly ScalingContainer target;
private SkinEditor skinEditor;
private const float visible_target_scale = 0.8f;
[Resolved]
private OsuColour colours { get; set; }
public SkinEditorOverlay(ScalingContainer target)
{
this.target = target;
RelativeSizeAxes = Axes.Both;
}
public bool OnPressed(GlobalAction action)
{
switch (action)
{
case GlobalAction.Back:
if (skinEditor?.State.Value == Visibility.Visible)
{
skinEditor.ToggleVisibility();
return true;
}
break;
case GlobalAction.ToggleSkinEditor:
if (skinEditor == null)
{
LoadComponentAsync(skinEditor = new SkinEditor(target), AddInternal);
skinEditor.State.BindValueChanged(editorVisibilityChanged);
}
else
skinEditor.ToggleVisibility();
return true;
}
return false;
}
private void editorVisibilityChanged(ValueChangedEvent<Visibility> visibility)
{
if (visibility.NewValue == Visibility.Visible)
{
target.ScaleTo(visible_target_scale, SkinEditor.TRANSITION_DURATION, Easing.OutQuint);
target.Masking = true;
target.BorderThickness = 5;
target.BorderColour = colours.Yellow;
target.AllowScaling = false;
}
else
{
target.BorderThickness = 0;
target.AllowScaling = true;
target.ScaleTo(1, SkinEditor.TRANSITION_DURATION, Easing.OutQuint).OnComplete(_ => target.Masking = false);
}
}
public void OnReleased(GlobalAction action)
{
}
/// <summary>
/// Exit any existing skin editor due to the game state changing.
/// </summary>
public void Reset()
{
skinEditor?.Hide();
skinEditor?.Expire();
skinEditor = null;
}
}
}

View File

@ -0,0 +1,132 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Extensions;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Screens.Play.HUD;
using osuTK;
namespace osu.Game.Skinning.Editor
{
public class SkinSelectionHandler : SelectionHandler<ISkinnableComponent>
{
public override bool HandleRotation(float angle)
{
// TODO: this doesn't correctly account for origin/anchor specs being different in a multi-selection.
foreach (var c in SelectedBlueprints)
((Drawable)c.Item).Rotation += angle;
return base.HandleRotation(angle);
}
public override bool HandleScale(Vector2 scale, Anchor anchor)
{
adjustScaleFromAnchor(ref scale, anchor);
foreach (var c in SelectedBlueprints)
// TODO: this is temporary and will be fixed with a separate refactor of selection transform logic.
((Drawable)c.Item).Scale += scale * 0.02f;
return true;
}
public override bool HandleMovement(MoveSelectionEvent<ISkinnableComponent> moveEvent)
{
foreach (var c in SelectedBlueprints)
{
Drawable drawable = (Drawable)c.Item;
drawable.Position += drawable.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta);
}
return true;
}
protected override void OnSelectionChanged()
{
base.OnSelectionChanged();
SelectionBox.CanRotate = true;
SelectionBox.CanScaleX = true;
SelectionBox.CanScaleY = true;
SelectionBox.CanReverse = false;
}
protected override void DeleteItems(IEnumerable<ISkinnableComponent> items)
{
foreach (var i in items)
{
((Drawable)i).Expire();
SelectedItems.Remove(i);
}
}
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<ISkinnableComponent>> selection)
{
yield return new OsuMenuItem("Anchor")
{
Items = createAnchorItems().ToArray()
};
foreach (var item in base.GetContextMenuItemsForSelection(selection))
yield return item;
IEnumerable<AnchorMenuItem> createAnchorItems()
{
var displayableAnchors = new[]
{
Anchor.TopLeft,
Anchor.TopCentre,
Anchor.TopRight,
Anchor.CentreLeft,
Anchor.Centre,
Anchor.CentreRight,
Anchor.BottomLeft,
Anchor.BottomCentre,
Anchor.BottomRight,
};
return displayableAnchors.Select(a =>
{
return new AnchorMenuItem(a, selection, _ => applyAnchor(a))
{
State = { Value = GetStateFromSelection(selection, c => ((Drawable)c.Item).Anchor == a) }
};
});
}
}
private void applyAnchor(Anchor anchor)
{
foreach (var item in SelectedItems)
((Drawable)item).Anchor = anchor;
}
private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference)
{
// cancel out scale in axes we don't care about (based on which drag handle was used).
if ((reference & Anchor.x1) > 0) scale.X = 0;
if ((reference & Anchor.y1) > 0) scale.Y = 0;
// reverse the scale direction if dragging from top or left.
if ((reference & Anchor.x0) > 0) scale.X = -scale.X;
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
}
public class AnchorMenuItem : TernaryStateMenuItem
{
public AnchorMenuItem(Anchor anchor, IEnumerable<SelectionBlueprint<ISkinnableComponent>> selection, Action<TernaryState> action)
: base(anchor.ToString(), getNextState, MenuItemType.Standard, action)
{
}
private static TernaryState getNextState(TernaryState state) => TernaryState.True;
}
}
}

View File

@ -11,7 +11,7 @@ using osuTK;
namespace osu.Game.Skinning
{
public class LegacyAccuracyCounter : PercentageCounter, IAccuracyCounter
public class LegacyAccuracyCounter : PercentageCounter, IAccuracyCounter, ISkinnableComponent
{
private readonly ISkin skin;

View File

@ -16,7 +16,7 @@ using osuTK.Graphics;
namespace osu.Game.Skinning
{
public class LegacyHealthDisplay : CompositeDrawable, IHealthDisplay
public class LegacyHealthDisplay : CompositeDrawable, IHealthDisplay, ISkinnableComponent
{
private const double epic_cutoff = 0.5;

View File

@ -5,11 +5,12 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Play.HUD;
using osuTK;
namespace osu.Game.Skinning
{
public class LegacyScoreCounter : ScoreCounter
public class LegacyScoreCounter : ScoreCounter, ISkinnableComponent
{
private readonly ISkin skin;

View File

@ -29,7 +29,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2021.427.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.506.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" />
<PackageReference Include="Sentry" Version="3.3.4" />
<PackageReference Include="SharpCompress" Version="0.28.2" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.427.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.506.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" />
</ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@ -93,7 +93,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2021.427.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.506.0" />
<PackageReference Include="SharpCompress" Version="0.28.2" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="SharpRaven" Version="2.4.0" />