Files
Ez2Lazer/osu.Game/Overlays/SkinEditor/SkinEditorOverlay.cs
Dean Herbert 3e0abdace8 Apply new inspections from 2026.1EAP1
Nothing really egregious here so not bothering with PR review. One dodgy
bug which has been
[reported](https://youtrack.jetbrains.com/issue/RIDER-135036/Incorrect-recursive-on-all-execution-paths-inspection)
and temporarily ignored.
2026-01-26 17:26:50 +08:00

469 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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.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.Primitives;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Layout;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
using osu.Game.Screens.SelectV2;
using osu.Game.Users;
using osu.Game.Utils;
using osu.Game.LAsEzExtensions.Screens;
namespace osu.Game.Overlays.SkinEditor
{
/// <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 partial class SkinEditorOverlay : OverlayContainer, IKeyBindingHandler<GlobalAction>
{
private readonly ScalingContainer scalingContainer;
protected override bool BlockNonPositionalInput => true;
private SkinEditor? skinEditor;
private EzSkinEditorScreen? ezSkinEditorScreen;
[Resolved]
private IPerformFromScreenRunner? performer { get; set; }
[Resolved]
private IOverlayManager? overlayManager { get; set; }
[Cached]
public readonly EditorClipboard Clipboard = new EditorClipboard();
[Cached]
private readonly ExternalEditOverlay externalEditOverlay = new ExternalEditOverlay();
[Resolved]
private OsuGame game { get; set; } = null!;
[Resolved]
private MusicController music { get; set; } = null!;
[Resolved]
private Bindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
[Resolved]
private Bindable<RulesetInfo> ruleset { get; set; } = null!;
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
private OsuScreen? lastTargetScreen;
private InvokeOnDisposal? nestedInputManagerDisable;
private IDisposable? externalEditOverlayRegistration;
private readonly LayoutValue drawSizeLayout;
public SkinEditorOverlay(ScalingContainer scalingContainer)
{
this.scalingContainer = scalingContainer;
RelativeSizeAxes = Axes.Both;
AddLayout(drawSizeLayout = new LayoutValue(Invalidation.DrawSize));
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins);
config.BindWith(OsuSetting.HUDVisibilityMode, configVisibilityMode);
}
protected override void LoadComplete()
{
base.LoadComplete();
externalEditOverlayRegistration = overlayManager?.RegisterBlockingOverlay(externalEditOverlay);
// EzSkinEditorScreen 是皮肤编辑器内的 overlay不应 Push 到 ScreenStack。
// 它作为 SkinEditorOverlay 的子级存在,切换场景或退出皮肤编辑器时会自动隐藏/销毁,不与其他场景叠画。
AddInternal(ezSkinEditorScreen = new EzSkinEditorScreen
{
Depth = -10
});
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
switch (e.Action)
{
case GlobalAction.Back:
if (ezSkinEditorScreen?.State.Value == Visibility.Visible)
{
ToggleEzSkinEditor();
return true;
}
if (skinEditor?.State.Value != Visibility.Visible)
break;
Hide();
return true;
}
return false;
}
protected override void PopIn()
{
overrideSkinEditorRelevantSettings();
if (skinEditor != null)
{
disableNestedInputManagers();
skinEditor.Show();
if (lastTargetScreen is MainMenu)
PresentGameplay();
return;
}
var editor = new SkinEditor();
editor.State.BindValueChanged(_ => updateComponentVisibility());
skinEditor = editor;
LoadComponentAsync(editor, _ =>
{
if (editor != skinEditor)
return;
AddInternal(editor);
if (lastTargetScreen is MainMenu)
PresentGameplay();
Debug.Assert(lastTargetScreen != null);
SetTarget(lastTargetScreen);
});
}
protected override void PopOut()
{
skinEditor?.Save(false);
skinEditor?.Hide();
nestedInputManagerDisable?.Dispose();
nestedInputManagerDisable = null;
// 离开皮肤编辑器时确保关闭 Ez overlay。
ezSkinEditorScreen?.Hide();
restoreSkinEditorRelevantSettings();
}
/// <summary>
/// 在皮肤编辑器内切换 EzSkinEditorScreen 的可见性。
/// </summary>
public void ToggleEzSkinEditor()
{
if (ezSkinEditorScreen == null)
return;
if (ezSkinEditorScreen.State.Value == Visibility.Visible)
{
ezSkinEditorScreen.Hide();
}
else
{
ezSkinEditorScreen.Show();
// Ensure sizing is applied immediately when showing.
Scheduler.AddOnce(updateScreenSizing);
}
}
public void PresentGameplay() => presentGameplay(false);
private void presentGameplay(bool attemptedBeatmapSwitch)
{
performer?.PerformFromScreen(screen =>
{
if (State.Value != Visibility.Visible)
return;
if (beatmap.Value is DummyWorkingBeatmap)
{
// presume we don't have anything good to play and just bail.
return;
}
// If we're playing the intro, switch away to another beatmap.
if (beatmap.Value.BeatmapSetInfo.Protected)
{
if (!attemptedBeatmapSwitch)
{
music.NextTrack();
Schedule(() => presentGameplay(true));
}
return;
}
if (screen is Player)
return;
// the validity of the current game-wide beatmap + ruleset combination is enforced by song select.
// if we're anywhere else, the state is unknown and may not make sense, so forcibly set something that does.
if (screen is not SoloSongSelect)
ruleset.Value = beatmap.Value.BeatmapInfo.Ruleset;
var replayGeneratingMod = ruleset.Value.CreateInstance().GetAutoplayMod();
IReadOnlyList<Mod> usableMods = mods.Value;
if (replayGeneratingMod != null)
usableMods = usableMods.Append(replayGeneratingMod).ToArray();
if (!ModUtils.CheckCompatibleSet(usableMods, out var invalid))
mods.Value = mods.Value.Except(invalid).ToArray();
if (replayGeneratingMod != null)
screen.Push(new EndlessPlayer(replayGeneratingMod.CreateScoreFromReplayData));
}, new[] { typeof(Player), typeof(SoloSongSelect) });
}
protected override void Update()
{
base.Update();
if (!drawSizeLayout.IsValid)
{
updateScreenSizing();
drawSizeLayout.Validate();
}
}
private void updateScreenSizing()
{
if (skinEditor?.State.Value != Visibility.Visible) return;
const float padding = 10;
float relativeSidebarWidth = (EditorSidebar.WIDTH + padding) / DrawWidth;
float relativeToolbarHeight = (SkinEditorSceneLibrary.HEIGHT + SkinEditor.MENU_HEIGHT + padding) / DrawHeight;
var rect = new RectangleF(
relativeSidebarWidth,
relativeToolbarHeight,
1 - relativeSidebarWidth * 2,
1f - relativeToolbarHeight - padding / DrawHeight);
scalingContainer.SetCustomRect(rect, true);
// Keep Ez overlay constrained to the same central preview area.
if (ezSkinEditorScreen != null)
{
ezSkinEditorScreen.RelativePositionAxes = Axes.Both;
ezSkinEditorScreen.RelativeSizeAxes = Axes.Both;
ezSkinEditorScreen.Position = rect.Location;
ezSkinEditorScreen.Size = rect.Size;
}
}
private void updateComponentVisibility()
{
Debug.Assert(skinEditor != null);
if (skinEditor.State.Value == Visibility.Visible)
{
Scheduler.AddOnce(updateScreenSizing);
game.Toolbar.Hide();
if (externalEditOverlay.State.Value != Visibility.Visible)
game.CloseAllOverlays();
}
else
{
scalingContainer.SetCustomRect(null);
if (lastTargetScreen?.HideOverlaysOnEnter != true)
game.Toolbar.Show();
}
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
/// <summary>
/// Set a new target screen which will be used to find skinnable components.
/// </summary>
public void SetTarget(OsuScreen screen)
{
nestedInputManagerDisable?.Dispose();
nestedInputManagerDisable = null;
// 切换场景时Ez overlay 应当自动退出,避免与其他场景叠画。
ezSkinEditorScreen?.Hide();
lastTargetScreen = screen;
if (skinEditor == null) return;
// ensure the toolbar is re-hidden even if a new screen decides to try and show it.
updateComponentVisibility();
// AddOnce with parameter will ensure the newest target is loaded if there is any overlap.
Scheduler.AddOnce(setTarget, screen);
}
private void setTarget(OsuScreen? target)
{
if (target == null)
return;
Debug.Assert(skinEditor != null);
if (!target.IsLoaded || !skinEditor.IsLoaded)
{
Scheduler.AddOnce(setTarget, target);
return;
}
if (skinEditor.State.Value == Visibility.Visible)
{
if (externalEditOverlay.State.Value != Visibility.Visible)
skinEditor.Save(false);
skinEditor.UpdateTargetScreen(target);
disableNestedInputManagers();
}
else
{
skinEditor.Hide();
skinEditor.Expire();
skinEditor = null;
}
}
private void disableNestedInputManagers()
{
if (lastTargetScreen == null)
return;
var nestedInputManagers = lastTargetScreen.ChildrenOfType<PassThroughInputManager>().Where(manager => manager.UseParentInput).ToArray();
foreach (var inputManager in nestedInputManagers)
inputManager.UseParentInput = false;
nestedInputManagerDisable = new InvokeOnDisposal(() =>
{
foreach (var inputManager in nestedInputManagers)
inputManager.UseParentInput = true;
});
}
private readonly Bindable<bool> beatmapSkins = new Bindable<bool>();
private LeasedBindable<bool>? leasedBeatmapSkins;
private readonly Bindable<HUDVisibilityMode> configVisibilityMode = new Bindable<HUDVisibilityMode>();
private LeasedBindable<HUDVisibilityMode>? leasedVisibilityMode;
private void overrideSkinEditorRelevantSettings()
{
if (!beatmapSkins.Disabled)
{
// The skin editor doesn't work well if beatmap skins are being applied to the player screen.
// To keep things simple, disable the setting game-wide while using the skin editor.
//
// This causes a full reload of the skin, which is pretty ugly.
// TODO: Investigate if we can avoid this when a beatmap skin is not being applied by the current beatmap.
leasedBeatmapSkins = beatmapSkins.BeginLease(true);
leasedBeatmapSkins.Value = false;
}
leasedVisibilityMode = configVisibilityMode.BeginLease(true);
leasedVisibilityMode.Value = HUDVisibilityMode.Always;
}
private void restoreSkinEditorRelevantSettings()
{
leasedBeatmapSkins?.Return();
leasedBeatmapSkins = null;
leasedVisibilityMode?.Return();
leasedVisibilityMode = null;
}
public new void ToggleVisibility()
{
if (skinEditor?.ExternalEditInProgress == true)
return;
base.ToggleVisibility();
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
externalEditOverlayRegistration?.Dispose();
externalEditOverlayRegistration = null;
}
private partial class EndlessPlayer : ReplayPlayer
{
protected override UserActivity? InitialActivity => null;
public override bool DisallowExternalBeatmapRulesetChanges => true;
public override bool? AllowGlobalTrackControl => false;
public EndlessPlayer(Func<IBeatmap, IReadOnlyList<Mod>, Score> createScore)
: base(createScore, new PlayerConfiguration
{
ShowResults = false,
AutomaticallySkipIntro = true,
})
{
}
protected override void LoadComplete()
{
base.LoadComplete();
if (!LoadedBeatmapSuccessfully)
Scheduler.AddDelayed(this.Exit, 1000);
}
protected override void Update()
{
base.Update();
if (!LoadedBeatmapSuccessfully)
return;
if (GameplayState.HasPassed)
GameplayClockContainer.Seek(0);
}
}
}
}