diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 23c5494cf5..42bf5e4d21 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables positionBindable.BindValueChanged(_ => updatePosition()); pathBindable.BindValueChanged(_ => updatePosition(), true); + + // TODO: This has no drawable content. Support for skins should be added. } protected override void CheckForResult(bool userTriggered, double timeOffset) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 263eada07c..a7020b6534 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -16,6 +16,7 @@ using osu.Framework.Platform; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; @@ -34,6 +35,8 @@ namespace osu.Game.Tests.Visual.SongSelect private RulesetStore rulesets; + private MusicController music; + private WorkingBeatmap defaultBeatmap; public override IReadOnlyList RequiredTypes => new[] @@ -79,6 +82,11 @@ namespace osu.Game.Tests.Visual.SongSelect Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, defaultBeatmap = Beatmap.Default)); + Dependencies.Cache(music = new MusicController()); + + // required to get bindables attached + Add(music); + Beatmap.SetDefault(); Dependencies.Cache(config = new OsuConfigManager(LocalStorage)); @@ -93,6 +101,57 @@ namespace osu.Game.Tests.Visual.SongSelect manager?.Delete(manager.GetAllUsableBeatmapSets()); }); + [Test] + public void TestAudioResuming() + { + createSongSelect(); + + addRulesetImportStep(0); + addRulesetImportStep(0); + + checkMusicPlaying(true); + AddStep("select first", () => songSelect.Carousel.SelectBeatmap(songSelect.Carousel.BeatmapSets.First().Beatmaps.First())); + checkMusicPlaying(true); + + AddStep("manual pause", () => music.TogglePause()); + checkMusicPlaying(false); + AddStep("select next difficulty", () => songSelect.Carousel.SelectNext(skipDifficulties: false)); + checkMusicPlaying(false); + + AddStep("select next set", () => songSelect.Carousel.SelectNext()); + checkMusicPlaying(true); + } + + [TestCase(false)] + [TestCase(true)] + public void TestAudioRemainsCorrectOnRulesetChange(bool rulesetsInSameBeatmap) + { + createSongSelect(); + + // start with non-osu! to avoid convert confusion + changeRuleset(1); + + if (rulesetsInSameBeatmap) + AddStep("import multi-ruleset map", () => + { + var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray(); + manager.Import(createTestBeatmapSet(0, usableRulesets)).Wait(); + }); + else + { + addRulesetImportStep(1); + addRulesetImportStep(0); + } + + checkMusicPlaying(true); + + AddStep("manual pause", () => music.TogglePause()); + checkMusicPlaying(false); + + changeRuleset(0); + checkMusicPlaying(!rulesetsInSameBeatmap); + } + [Test] public void TestDummy() { @@ -128,12 +187,10 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - [Ignore("needs fixing")] public void TestImportUnderDifferentRuleset() { createSongSelect(); - changeRuleset(2); - addRulesetImportStep(0); + addRulesetImportStep(2); AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmap == null); } @@ -224,6 +281,9 @@ namespace osu.Game.Tests.Visual.SongSelect private static int importId; private int getImportId() => ++importId; + private void checkMusicPlaying(bool playing) => + AddUntilStep($"music {(playing ? "" : "not ")}playing", () => music.IsPlaying == playing); + private void changeMods(params Mod[] mods) => AddStep($"change mods to {string.Join(", ", mods.Select(m => m.Acronym))}", () => Mods.Value = mods); private void changeRuleset(int id) => AddStep($"change ruleset to {id}", () => Ruleset.Value = rulesets.AvailableRulesets.First(r => r.ID == id)); diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index bd7e8e44e5..2a99d81516 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Logging; -using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Skinning; @@ -23,8 +22,6 @@ namespace osu.Game.Screens.Edit.Compose private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); - private HitObjectComposer composer; - [BackgroundDependencyLoader(true)] private void load([CanBeNull] BindableBeatDivisor beatDivisor) { @@ -107,26 +104,32 @@ namespace osu.Game.Screens.Edit.Compose return; } - composer = ruleset.CreateHitObjectComposer(); + var composer = ruleset.CreateHitObjectComposer(); - if (composer == null) + Drawable content; + + if (composer != null) { - Logger.Log($"Ruleset {ruleset.Description} doesn't support hitobject composition."); - // ExitRequested?.Invoke(); - return; + var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin); + + // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation + // full access to all skin sources. + var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider)); + + // load the skinning hierarchy first. + // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. + content = beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(ruleset.CreateHitObjectComposer())); + } + else + { + content = new ScreenWhiteBox.UnderConstructionMessage($"{ruleset.Description}'s composer"); } - var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin); - - // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation - // full access to all skin sources. - var rulesetSkinProvider = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider)); - - // load the skinning hierarchy first. - // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. - composerContainer.Add( - beatmapSkinProvider.WithChild( - rulesetSkinProvider.WithChild(composer))); + LoadComponentAsync(content, _ => + { + composerContainer.Add(content); + content.FadeInFromZero(300, Easing.OutQuint); + }); } } } diff --git a/osu.Game/Screens/Edit/Design/DesignScreen.cs b/osu.Game/Screens/Edit/Design/DesignScreen.cs index 2a334e1b30..9f1fcf55b2 100644 --- a/osu.Game/Screens/Edit/Design/DesignScreen.cs +++ b/osu.Game/Screens/Edit/Design/DesignScreen.cs @@ -1,52 +1,13 @@ // Copyright (c) ppy Pty Ltd . 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.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics.Sprites; -using osuTK.Graphics; - namespace osu.Game.Screens.Edit.Design { public class DesignScreen : EditorScreen { public DesignScreen() { - Add(new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Alpha = 0.35f - }, - new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Alpha = 0.5f - }, - new Container - { - AutoSizeAxes = Axes.Both, - Padding = new MarginPadding(20), - Child = new OsuSpriteText { Text = "Design screen" } - } - } - } - } - }); + Child = new ScreenWhiteBox.UnderConstructionMessage("Design mode"); } } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 8cc227d9be..9ebe3bc26a 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -26,6 +26,8 @@ using System.Collections.Generic; using osu.Framework; using osu.Framework.Input.Bindings; using osu.Game.Input.Bindings; +using osu.Game.Screens.Edit.Setup; +using osu.Game.Screens.Edit.Timing; using osu.Game.Users; namespace osu.Game.Screens.Edit @@ -258,6 +260,10 @@ namespace osu.Game.Screens.Edit switch (e.NewValue) { + case EditorScreenMode.SongSetup: + currentScreen = new SetupScreen(); + break; + case EditorScreenMode.Compose: currentScreen = new ComposeScreen(); break; @@ -266,6 +272,10 @@ namespace osu.Game.Screens.Edit currentScreen = new DesignScreen(); break; + case EditorScreenMode.Timing: + currentScreen = new TimingScreen(); + break; + default: currentScreen = new EditorScreen(); break; diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs new file mode 100644 index 0000000000..758dbc6e16 --- /dev/null +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Screens.Edit.Setup +{ + public class SetupScreen : EditorScreen + { + public SetupScreen() + { + Child = new ScreenWhiteBox.UnderConstructionMessage("Setup mode"); + } + } +} diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs new file mode 100644 index 0000000000..9ded4207e5 --- /dev/null +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Screens.Edit.Timing +{ + public class TimingScreen : EditorScreen + { + public TimingScreen() + { + Child = new ScreenWhiteBox.UnderConstructionMessage("Timing mode"); + } + } +} diff --git a/osu.Game/Screens/ScreenWhiteBox.cs b/osu.Game/Screens/ScreenWhiteBox.cs index 6c5854d17e..e4971221c4 100644 --- a/osu.Game/Screens/ScreenWhiteBox.cs +++ b/osu.Game/Screens/ScreenWhiteBox.cs @@ -20,38 +20,17 @@ namespace osu.Game.Screens { public class ScreenWhiteBox : OsuScreen { + private readonly UnderConstructionMessage message; + private const double transition_time = 1000; protected virtual IEnumerable PossibleChildren => null; - private readonly FillFlowContainer textContainer; - private readonly Container boxContainer; - protected override BackgroundScreen CreateBackground() => new BackgroundScreenCustom(@"Backgrounds/bg2"); - public override void OnEntering(IScreen last) - { - base.OnEntering(last); - - Alpha = 0; - textContainer.Position = new Vector2(DrawSize.X / 16, 0); - - boxContainer.ScaleTo(0.2f); - boxContainer.RotateTo(-20); - - using (BeginDelayedSequence(300, true)) - { - boxContainer.ScaleTo(1, transition_time, Easing.OutElastic); - boxContainer.RotateTo(0, transition_time / 2, Easing.OutQuint); - - textContainer.MoveTo(Vector2.Zero, transition_time, Easing.OutExpo); - this.FadeIn(transition_time, Easing.OutExpo); - } - } - public override bool OnExiting(IScreen next) { - textContainer.MoveTo(new Vector2(DrawSize.X / 16, 0), transition_time, Easing.OutExpo); + message.TextContainer.MoveTo(new Vector2(DrawSize.X / 16, 0), transition_time, Easing.OutExpo); this.FadeOut(transition_time, Easing.OutExpo); return base.OnExiting(next); @@ -61,7 +40,7 @@ namespace osu.Game.Screens { base.OnSuspending(next); - textContainer.MoveTo(new Vector2(-(DrawSize.X / 16), 0), transition_time, Easing.OutExpo); + message.TextContainer.MoveTo(new Vector2(-(DrawSize.X / 16), 0), transition_time, Easing.OutExpo); this.FadeOut(transition_time, Easing.OutExpo); } @@ -69,7 +48,7 @@ namespace osu.Game.Screens { base.OnResuming(last); - textContainer.MoveTo(Vector2.Zero, transition_time, Easing.OutExpo); + message.TextContainer.MoveTo(Vector2.Zero, transition_time, Easing.OutExpo); this.FadeIn(transition_time, Easing.OutExpo); } @@ -79,65 +58,7 @@ namespace osu.Game.Screens InternalChildren = new Drawable[] { - boxContainer = new Container - { - Size = new Vector2(0.3f), - RelativeSizeAxes = Axes.Both, - CornerRadius = 20, - Masking = true, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - - Colour = getColourFor(GetType()), - Alpha = 0.2f, - Blending = BlendingParameters.Additive, - }, - textContainer = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new SpriteIcon - { - Icon = FontAwesome.Solid.UniversalAccess, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Size = new Vector2(50), - }, - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = GetType().Name, - Colour = getColourFor(GetType()).Lighten(0.8f), - Font = OsuFont.GetFont(size: 50), - }, - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = "is not yet ready for use!", - Font = OsuFont.GetFont(size: 20), - }, - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = "please check back a bit later.", - Font = OsuFont.GetFont(size: 14), - }, - } - }, - } - }, + message = new UnderConstructionMessage(GetType().Name), childModeButtons = new FillFlowContainer { Direction = FillDirection.Vertical, @@ -155,24 +76,24 @@ namespace osu.Game.Screens childModeButtons.Add(new ChildModeButton { Text = $@"{t.Name}", - BackgroundColour = getColourFor(t), - HoverColour = getColourFor(t).Lighten(0.2f), + BackgroundColour = getColourFor(t.Name), + HoverColour = getColourFor(t.Name).Lighten(0.2f), Action = delegate { this.Push(Activator.CreateInstance(t) as Screen); } }); } } } - private Color4 getColourFor(Type type) + private static Color4 getColourFor(object type) { - int hash = type.Name.GetHashCode(); + int hash = type.GetHashCode(); byte r = (byte)MathHelper.Clamp(((hash & 0xFF0000) >> 16) * 0.8f, 20, 255); byte g = (byte)MathHelper.Clamp(((hash & 0x00FF00) >> 8) * 0.8f, 20, 255); byte b = (byte)MathHelper.Clamp((hash & 0x0000FF) * 0.8f, 20, 255); return new Color4(r, g, b, 255); } - public class ChildModeButton : TwoLayerButton + private class ChildModeButton : TwoLayerButton { public ChildModeButton() { @@ -181,5 +102,104 @@ namespace osu.Game.Screens Origin = Anchor.BottomRight; } } + + public class UnderConstructionMessage : CompositeDrawable + { + public FillFlowContainer TextContainer { get; } + + private readonly Container boxContainer; + + public UnderConstructionMessage(string name) + { + RelativeSizeAxes = Axes.Both; + Size = new Vector2(0.3f); + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + var colour = getColourFor(name); + + InternalChildren = new Drawable[] + { + boxContainer = new Container + { + CornerRadius = 20, + Masking = true, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + + Colour = colour, + Alpha = 0.2f, + Blending = BlendingParameters.Additive, + }, + TextContainer = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new SpriteIcon + { + Icon = FontAwesome.Solid.UniversalAccess, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Size = new Vector2(50), + }, + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = name, + Colour = colour.Lighten(0.8f), + Font = OsuFont.GetFont(size: 36), + }, + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = "is not yet ready for use!", + Font = OsuFont.GetFont(size: 20), + }, + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = "please check back a bit later.", + Font = OsuFont.GetFont(size: 14), + }, + } + }, + } + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + TextContainer.Position = new Vector2(DrawSize.X / 16, 0); + + boxContainer.Hide(); + boxContainer.ScaleTo(0.2f); + boxContainer.RotateTo(-20); + + using (BeginDelayedSequence(300, true)) + { + boxContainer.ScaleTo(1, transition_time, Easing.OutElastic); + boxContainer.RotateTo(0, transition_time / 2, Easing.OutQuint); + + TextContainer.MoveTo(Vector2.Zero, transition_time, Easing.OutExpo); + boxContainer.FadeIn(transition_time, Easing.OutExpo); + } + } + } } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index d9ddfa2a94..6c5f64ed6c 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -412,9 +412,6 @@ namespace osu.Game.Screens.Select WorkingBeatmap previous = Beatmap.Value; Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, previous); - if (this.IsCurrentScreen() && Beatmap.Value?.Track != previous?.Track) - ensurePlayingSelected(true); - if (beatmap != null) { if (beatmap.BeatmapSetInfoID == beatmapNoDebounce?.BeatmapSetInfoID) @@ -424,6 +421,9 @@ namespace osu.Game.Screens.Select } } + if (this.IsCurrentScreen()) + ensurePlayingSelected(); + UpdateBeatmap(Beatmap.Value); } } @@ -581,19 +581,24 @@ namespace osu.Game.Screens.Select beatmap.Track.Looping = true; } - private void ensurePlayingSelected(bool restart = false) + private readonly WeakReference lastTrack = new WeakReference(null); + + /// + /// Ensures some music is playing for the current track. + /// Will resume playback from a manual user pause if the track has changed. + /// + private void ensurePlayingSelected() { Track track = Beatmap.Value.Track; - if (!track.IsRunning) - { - track.RestartPoint = Beatmap.Value.Metadata.PreviewTime; + bool isNewTrack = !lastTrack.TryGetTarget(out var last) || last != track; - if (restart) - track.Restart(); - else - track.Start(); - } + track.RestartPoint = Beatmap.Value.Metadata.PreviewTime; + + if (!track.IsRunning && (music?.IsUserPaused != true || isNewTrack)) + track.Restart(); + + lastTrack.SetTarget(track); } private void onBeatmapSetAdded(BeatmapSetInfo s) => Carousel.UpdateBeatmapSet(s);