diff --git a/osu.Desktop/app.manifest b/osu.Desktop/app.manifest new file mode 100644 index 0000000000..2e9127bf44 --- /dev/null +++ b/osu.Desktop/app.manifest @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + true + + + \ No newline at end of file diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 453cf6f94d..01e4ada2f1 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -8,6 +8,7 @@ osu!lazer osu!lazer lazer.ico + app.manifest 0.0.0 0.0.0 diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyCursor.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyCursor.cs index 470ba3acae..05b38ae195 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyCursor.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyCursor.cs @@ -11,6 +11,9 @@ namespace osu.Game.Rulesets.Osu.Skinning { public class LegacyCursor : CompositeDrawable { + private NonPlayfieldSprite cursor; + private bool spin; + public LegacyCursor() { Size = new Vector2(50); @@ -22,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin) { + spin = skin.GetConfig(OsuSkinConfiguration.CursorRotate)?.Value ?? true; + InternalChildren = new Drawable[] { new NonPlayfieldSprite @@ -30,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Skinning Anchor = Anchor.Centre, Origin = Anchor.Centre, }, - new NonPlayfieldSprite + cursor = new NonPlayfieldSprite { Texture = skin.GetTexture("cursor"), Anchor = Anchor.Centre, @@ -38,5 +43,11 @@ namespace osu.Game.Rulesets.Osu.Skinning } }; } + + protected override void LoadComplete() + { + if (spin) + cursor.Spin(10000, RotationDirection.Clockwise); + } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index 479c250eab..f5b7d9166f 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -49,7 +49,11 @@ namespace osu.Game.Rulesets.Osu.Skinning return this.GetAnimation(component.LookupName, true, false); case OsuSkinComponents.SliderFollowCircle: - return this.GetAnimation("sliderfollowcircle", true, true); + var followCircle = this.GetAnimation("sliderfollowcircle", true, true); + if (followCircle != null) + // follow circles are 2x the hitcircle resolution in legacy skins (since they are scaled down from >1x + followCircle.Scale *= 0.5f; + return followCircle; case OsuSkinComponents.SliderBall: var sliderBallContent = this.GetAnimation("sliderb", true, true, ""); diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs index 98219cafe8..5d99960f10 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs @@ -11,5 +11,6 @@ namespace osu.Game.Rulesets.Osu.Skinning SliderPathRadius, AllowSliderBallTint, CursorExpand, + CursorRotate } } diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs index 6e5b3b93e9..e618256c03 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs @@ -13,6 +13,8 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Objects; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK; using osuTK.Graphics; @@ -25,6 +27,7 @@ namespace osu.Game.Tests.Visual.Editor public override IReadOnlyList RequiredTypes => new[] { typeof(TimelineArea), + typeof(TimelineHitObjectDisplay), typeof(Timeline), typeof(TimelineButton), typeof(CentreMarker) @@ -35,6 +38,8 @@ namespace osu.Game.Tests.Visual.Editor { Beatmap.Value = new WaveformTestBeatmap(audio); + var editorBeatmap = new EditorBeatmap((Beatmap)Beatmap.Value.Beatmap); + Children = new Drawable[] { new FillFlowContainer @@ -50,6 +55,7 @@ namespace osu.Game.Tests.Visual.Editor }, new TimelineArea { + Child = new TimelineHitObjectDisplay(editorBeatmap), Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 74ae641bfe..dbea8d28a6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -19,6 +20,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens; @@ -55,6 +57,9 @@ namespace osu.Game.Tests.Visual.Gameplay beforeLoadAction?.Invoke(); Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + foreach (var mod in Mods.Value.OfType()) + mod.ApplyToClock(Beatmap.Value.Track); + InputManager.Child = container = new TestPlayerLoaderContainer( loader = new TestPlayerLoader(() => { @@ -63,6 +68,24 @@ namespace osu.Game.Tests.Visual.Gameplay })); } + /// + /// When exits early, it has to wait for the player load task + /// to complete before running disposal on player. This previously caused an issue where mod + /// speed adjustments were undone too late, causing cross-screen pollution. + /// + [Test] + public void TestEarlyExit() + { + AddStep("load dummy beatmap", () => ResetPlayer(false, () => Mods.Value = new[] { new OsuModNightcore() })); + AddUntilStep("wait for current", () => loader.IsCurrentScreen()); + AddAssert("mod rate applied", () => Beatmap.Value.Track.Rate != 1); + AddStep("exit loader", () => loader.Exit()); + AddUntilStep("wait for not current", () => !loader.IsCurrentScreen()); + AddAssert("player did not load", () => !player.IsLoaded); + AddUntilStep("player disposed", () => loader.DisposalTask?.IsCompleted == true); + AddAssert("mod rate still applied", () => Beatmap.Value.Track.Rate != 1); + } + [Test] public void TestBlockLoadViaMouseMovement() { @@ -196,6 +219,8 @@ namespace osu.Game.Tests.Visual.Gameplay { public new VisualSettings VisualSettings => base.VisualSettings; + public new Task DisposalTask => base.DisposalTask; + public TestPlayerLoader(Func createPlayer) : base(createPlayer) { diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs index 3d39bb7003..7207506ccd 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs @@ -1,9 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Dialog; namespace osu.Game.Tests.Visual.UserInterface @@ -11,13 +14,22 @@ namespace osu.Game.Tests.Visual.UserInterface [TestFixture] public class TestScenePopupDialog : OsuTestScene { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(PopupDialogOkButton), + typeof(PopupDialogCancelButton), + typeof(PopupDialogButton), + typeof(DialogButton), + }; + public TestScenePopupDialog() { - Add(new TestPopupDialog - { - RelativeSizeAxes = Axes.Both, - State = { Value = Framework.Graphics.Containers.Visibility.Visible }, - }); + AddStep("new popup", () => + Add(new TestPopupDialog + { + RelativeSizeAxes = Axes.Both, + State = { Value = Framework.Graphics.Containers.Visibility.Visible }, + })); } private class TestPopupDialog : PopupDialog diff --git a/osu.Game/Graphics/UserInterface/DialogButton.cs b/osu.Game/Graphics/UserInterface/DialogButton.cs index 927ad13829..aed07e56ee 100644 --- a/osu.Game/Graphics/UserInterface/DialogButton.cs +++ b/osu.Game/Graphics/UserInterface/DialogButton.cs @@ -20,9 +20,10 @@ namespace osu.Game.Graphics.UserInterface { public class DialogButton : OsuClickableContainer { + private const float idle_width = 0.8f; private const float hover_width = 0.9f; + private const float hover_duration = 500; - private const float glow_fade_duration = 250; private const float click_duration = 200; public readonly BindableBool Selected = new BindableBool(); @@ -99,7 +100,7 @@ namespace osu.Game.Graphics.UserInterface RelativeSizeAxes = Axes.Both, Origin = Anchor.Centre, Anchor = Anchor.Centre, - Width = 0.8f, + Width = idle_width, Masking = true, MaskingSmoothness = 2, EdgeEffect = new EdgeEffectParameters @@ -199,26 +200,50 @@ namespace osu.Game.Graphics.UserInterface public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => backgroundContainer.ReceivePositionalInputAt(screenSpacePos); + private bool clickAnimating; + protected override bool OnClick(ClickEvent e) { - colourContainer.ResizeTo(new Vector2(1.5f, 1f), click_duration, Easing.In); - flash(); - - this.Delay(click_duration).Schedule(delegate + var flash = new Box { - colourContainer.ResizeTo(new Vector2(0.8f, 1f)); - spriteText.Spacing = Vector2.Zero; - glowContainer.FadeOut(); - }); + RelativeSizeAxes = Axes.Both, + Colour = ButtonColour, + Blending = BlendingParameters.Additive, + Alpha = 0.05f + }; + + colourContainer.Add(flash); + flash.FadeOutFromOne(100).Expire(); + + clickAnimating = true; + colourContainer.ResizeWidthTo(colourContainer.Width * 1.05f, 100, Easing.OutQuint) + .OnComplete(_ => + { + clickAnimating = false; + Selected.TriggerChange(); + }); return base.OnClick(e); } + protected override bool OnMouseDown(MouseDownEvent e) + { + colourContainer.ResizeWidthTo(hover_width * 0.98f, click_duration * 4, Easing.OutQuad); + return base.OnMouseDown(e); + } + + protected override bool OnMouseUp(MouseUpEvent e) + { + if (Selected.Value) + colourContainer.ResizeWidthTo(hover_width, click_duration, Easing.In); + return base.OnMouseUp(e); + } + protected override bool OnHover(HoverEvent e) { base.OnHover(e); - Selected.Value = true; + return true; } @@ -230,36 +255,23 @@ namespace osu.Game.Graphics.UserInterface private void selectionChanged(ValueChangedEvent args) { + if (clickAnimating) + return; + if (args.NewValue) { spriteText.TransformSpacingTo(hoverSpacing, hover_duration, Easing.OutElastic); - colourContainer.ResizeTo(new Vector2(hover_width, 1f), hover_duration, Easing.OutElastic); - glowContainer.FadeIn(glow_fade_duration, Easing.Out); + colourContainer.ResizeWidthTo(hover_width, hover_duration, Easing.OutElastic); + glowContainer.FadeIn(hover_duration, Easing.OutQuint); } else { - colourContainer.ResizeTo(new Vector2(0.8f, 1f), hover_duration, Easing.OutElastic); + colourContainer.ResizeWidthTo(idle_width, hover_duration, Easing.OutElastic); spriteText.TransformSpacingTo(Vector2.Zero, hover_duration, Easing.OutElastic); - glowContainer.FadeOut(glow_fade_duration, Easing.Out); + glowContainer.FadeOut(hover_duration, Easing.OutQuint); } } - private void flash() - { - var flash = new Box - { - RelativeSizeAxes = Axes.Both - }; - - colourContainer.Add(flash); - - flash.Colour = ButtonColour; - flash.Blending = BlendingParameters.Additive; - flash.Alpha = 0.3f; - flash.FadeOutFromOne(click_duration); - flash.Expire(); - } - private void updateGlow() { leftGlow.Colour = ColourInfo.GradientHorizontal(new Color4(ButtonColour.R, ButtonColour.G, ButtonColour.B, 0f), ButtonColour); diff --git a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs index 79ce04ed66..b941cd8973 100644 --- a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs @@ -34,7 +34,7 @@ namespace osu.Game.Online.API.Requests.Responses PP = PP, Beatmap = Beatmap, RulesetID = OnlineRulesetID, - Hash = "online", // todo: temporary? + Hash = Replay ? "online" : string.Empty, // todo: temporary? Rank = Rank, Ruleset = ruleset, Mods = mods, diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 58f5f02956..f6723839b2 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -63,7 +63,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores return; for (int i = 0; i < value.Count; i++) - backgroundFlow.Add(new ScoreTableRowBackground(i)); + backgroundFlow.Add(new ScoreTableRowBackground(i, value[i])); Columns = createHeaders(value[0]); Content = value.Select((s, i) => createContent(i, s)).ToArray().ToRectangular(); diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTableRowBackground.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTableRowBackground.cs index d820f4d89d..724a7f8b55 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTableRowBackground.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTableRowBackground.cs @@ -7,6 +7,8 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Graphics; +using osu.Game.Online.API; +using osu.Game.Scoring; namespace osu.Game.Overlays.BeatmapSet.Scores { @@ -17,8 +19,14 @@ namespace osu.Game.Overlays.BeatmapSet.Scores private readonly Box hoveredBackground; private readonly Box background; - public ScoreTableRowBackground(int index) + private readonly int index; + private readonly ScoreInfo score; + + public ScoreTableRowBackground(int index, ScoreInfo score) { + this.index = index; + this.score = score; + RelativeSizeAxes = Axes.X; Height = 25; @@ -37,16 +45,21 @@ namespace osu.Game.Overlays.BeatmapSet.Scores Alpha = 0, }, }; - - if (index % 2 != 0) - background.Alpha = 0; } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, IAPIProvider api) { - hoveredBackground.Colour = colours.Gray4; - background.Colour = colours.Gray3; + var isOwnScore = api.LocalUser.Value.Id == score.UserID; + + if (isOwnScore) + background.Colour = colours.GreenDarker; + else if (index % 2 == 0) + background.Colour = colours.Gray3; + else + background.Alpha = 0; + + hoveredBackground.Colour = isOwnScore ? colours.GreenDark : colours.Gray4; } protected override bool OnHover(HoverEvent e) diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs index c6b4787ff1..69a4a4181a 100644 --- a/osu.Game/Overlays/Mods/ModButton.cs +++ b/osu.Game/Overlays/Mods/ModButton.cs @@ -167,10 +167,6 @@ namespace osu.Game.Overlays.Mods { switch (e.Button) { - case MouseButton.Left: - SelectNext(1); - break; - case MouseButton.Right: SelectNext(-1); break; @@ -180,6 +176,13 @@ namespace osu.Game.Overlays.Mods return true; } + protected override bool OnClick(ClickEvent e) + { + SelectNext(1); + + return true; + } + /// /// Select the next available mod in a specified direction. /// diff --git a/osu.Game/Overlays/SettingsSubPanel.cs b/osu.Game/Overlays/SettingsSubPanel.cs index 5000156e97..1fa233d9d4 100644 --- a/osu.Game/Overlays/SettingsSubPanel.cs +++ b/osu.Game/Overlays/SettingsSubPanel.cs @@ -3,16 +3,14 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Bindings; -using osu.Framework.Input.Events; using osu.Game.Graphics; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osu.Game.Input.Bindings; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; -using osu.Game.Screens.Ranking; using osuTK; +using osuTK.Graphics; namespace osu.Game.Overlays { @@ -36,21 +34,21 @@ namespace osu.Game.Overlays protected override bool DimMainContent => false; // dimming is handled by main overlay - private class BackButton : OsuClickableContainer, IKeyBindingHandler + private class BackButton : OsuButton { - private AspectContainer aspect; - [BackgroundDependencyLoader] private void load() { Size = new Vector2(Sidebar.DEFAULT_WIDTH); - Children = new Drawable[] + + BackgroundColour = Color4.Black; + + AddRange(new Drawable[] { - aspect = new AspectContainer + new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, Children = new Drawable[] { new SpriteIcon @@ -71,34 +69,8 @@ namespace osu.Game.Overlays }, } } - }; + }); } - - protected override bool OnMouseDown(MouseDownEvent e) - { - aspect.ScaleTo(0.75f, 2000, Easing.OutQuint); - return base.OnMouseDown(e); - } - - protected override bool OnMouseUp(MouseUpEvent e) - { - aspect.ScaleTo(1, 1000, Easing.OutElastic); - return base.OnMouseUp(e); - } - - public bool OnPressed(GlobalAction action) - { - switch (action) - { - case GlobalAction.Back: - Click(); - return true; - } - - return false; - } - - public bool OnReleased(GlobalAction action) => false; } } } diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 805fc2b46f..9ac967ef74 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -34,7 +34,9 @@ namespace osu.Game.Rulesets.Edit where TObject : HitObject { protected IRulesetConfigManager Config { get; private set; } - protected EditorBeatmap EditorBeatmap { get; private set; } + + protected new EditorBeatmap EditorBeatmap { get; private set; } + protected readonly Ruleset Ruleset; [Resolved] @@ -148,7 +150,7 @@ namespace osu.Game.Rulesets.Edit beatmapProcessor = Ruleset.CreateBeatmapProcessor(playableBeatmap); - EditorBeatmap = new EditorBeatmap(playableBeatmap); + base.EditorBeatmap = EditorBeatmap = new EditorBeatmap(playableBeatmap); EditorBeatmap.HitObjectAdded += addHitObject; EditorBeatmap.HitObjectRemoved += removeHitObject; EditorBeatmap.StartTimeChanged += UpdateHitObject; @@ -333,6 +335,11 @@ namespace osu.Game.Rulesets.Edit /// public abstract IEnumerable HitObjects { get; } + /// + /// An editor-specific beatmap, exposing mutation events. + /// + public IEditorBeatmap EditorBeatmap { get; protected set; } + /// /// Whether the user's cursor is currently in an area of the that is valid for placement. /// diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs index 26d9614631..7706e33179 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs @@ -14,12 +14,14 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts /// /// Represents a part of the summary timeline.. /// - public abstract class TimelinePart : CompositeDrawable + public abstract class TimelinePart : Container { protected readonly IBindable Beatmap = new Bindable(); private readonly Container timeline; + protected override Container Content => timeline; + protected TimelinePart() { AddInternal(timeline = new Container { RelativeSizeAxes = Axes.Both }); @@ -50,8 +52,6 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts timeline.RelativeChildSize = new Vector2((float)Math.Max(1, Beatmap.Value.Track.Length), 1); } - protected void Add(Drawable visualisation) => timeline.Add(visualisation); - protected virtual void LoadBeatmap(WorkingBeatmap beatmap) { timeline.Clear(); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 748c9e2ba3..b4f3b1f610 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -36,7 +36,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { this.adjustableClock = adjustableClock; - Child = waveform = new WaveformGraph + Add(waveform = new WaveformGraph { RelativeSizeAxes = Axes.Both, Colour = colours.Blue.Opacity(0.2f), @@ -44,7 +44,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline MidColour = colours.BlueDark, HighColour = colours.BlueDarker, Depth = float.MaxValue - }; + }); // We don't want the centre marker to scroll AddInternal(new CentreMarker()); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs index 863a120fc3..02e5db306d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs @@ -1,6 +1,7 @@ // 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.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -11,17 +12,18 @@ using osuTK; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { - public class TimelineArea : CompositeDrawable + public class TimelineArea : Container { - private readonly Timeline timeline; + private readonly Timeline timeline = new Timeline { RelativeSizeAxes = Axes.Both }; - public TimelineArea() + protected override Container Content => timeline; + + [BackgroundDependencyLoader] + private void load() { Masking = true; CornerRadius = 5; - OsuCheckbox hitObjectsCheckbox; - OsuCheckbox hitSoundsCheckbox; OsuCheckbox waveformCheckbox; InternalChildren = new Drawable[] @@ -60,8 +62,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Spacing = new Vector2(0, 4), Children = new[] { - hitObjectsCheckbox = new OsuCheckbox { LabelText = "Hit objects" }, - hitSoundsCheckbox = new OsuCheckbox { LabelText = "Hit sounds" }, waveformCheckbox = new OsuCheckbox { LabelText = "Waveform" } } } @@ -107,7 +107,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } }, - timeline = new Timeline { RelativeSizeAxes = Axes.Both } + timeline }, }, ColumnDimensions = new[] @@ -119,8 +119,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } }; - hitObjectsCheckbox.Current.Value = true; - hitSoundsCheckbox.Current.Value = true; waveformCheckbox.Current.Value = true; timeline.WaveformVisible.BindTo(waveformCheckbox.Current); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectDisplay.cs new file mode 100644 index 0000000000..db4aca75e5 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectDisplay.cs @@ -0,0 +1,108 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Edit.Compose.Components.Timeline +{ + internal class TimelineHitObjectDisplay : TimelinePart + { + private IEditorBeatmap beatmap { get; } + + public TimelineHitObjectDisplay(IEditorBeatmap beatmap) + { + RelativeSizeAxes = Axes.Both; + + this.beatmap = beatmap; + } + + [BackgroundDependencyLoader] + private void load() + { + foreach (var h in beatmap.HitObjects) + add(h); + + beatmap.HitObjectAdded += add; + beatmap.HitObjectRemoved += remove; + beatmap.StartTimeChanged += h => + { + remove(h); + add(h); + }; + } + + private void remove(HitObject h) + { + foreach (var d in Children.OfType().Where(c => c.HitObject == h)) + d.Expire(); + } + + private void add(HitObject h) + { + var yOffset = Children.Count(d => d.X == h.StartTime); + + Add(new TimelineHitObjectRepresentation(h) { Y = -yOffset * TimelineHitObjectRepresentation.THICKNESS }); + } + + private class TimelineHitObjectRepresentation : CompositeDrawable + { + public const float THICKNESS = 3; + + public readonly HitObject HitObject; + + public TimelineHitObjectRepresentation(HitObject hitObject) + { + HitObject = hitObject; + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; + + Width = (float)(hitObject.GetEndTime() - hitObject.StartTime); + + X = (float)hitObject.StartTime; + + RelativePositionAxes = Axes.X; + RelativeSizeAxes = Axes.X; + + if (hitObject is IHasEndTime) + { + AddInternal(new Container + { + CornerRadius = 2, + Masking = true, + Size = new Vector2(1, THICKNESS), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativePositionAxes = Axes.X, + RelativeSizeAxes = Axes.X, + Colour = Color4.Black, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + } + }); + } + + AddInternal(new Circle + { + Size = new Vector2(16), + Anchor = Anchor.CentreLeft, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.X, + AlwaysPresent = true, + Colour = Color4.White, + BorderColour = Color4.Black, + BorderThickness = THICKNESS, + }); + } + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 2e9094ebe6..6984716a2c 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -3,32 +3,35 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Edit; +using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Skinning; namespace osu.Game.Screens.Edit.Compose { public class ComposeScreen : EditorScreenWithTimeline { + private HitObjectComposer composer; + protected override Drawable CreateMainContent() { var ruleset = Beatmap.Value.BeatmapInfo.Ruleset?.CreateInstance(); + composer = ruleset?.CreateHitObjectComposer(); - var composer = ruleset?.CreateHitObjectComposer(); + if (ruleset == null || composer == null) + return new ScreenWhiteBox.UnderConstructionMessage(ruleset == null ? "This beatmap" : $"{ruleset.Description}'s composer"); - if (composer != null) - { - var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin); + 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)); + // 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. - return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(ruleset.CreateHitObjectComposer())); - } - - return new ScreenWhiteBox.UnderConstructionMessage(ruleset == null ? "This beatmap" : $"{ruleset.Description}'s composer"); + // 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. + return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(composer)); } + + protected override Drawable CreateTimelineContent() => new TimelineHitObjectDisplay(composer.EditorBeatmap); } } diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index 752356e8c4..aa8d99b517 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -20,6 +20,8 @@ namespace osu.Game.Screens.Edit private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); + private TimelineArea timelineArea; + [BackgroundDependencyLoader(true)] private void load([CanBeNull] BindableBeatDivisor beatDivisor) { @@ -64,7 +66,7 @@ namespace osu.Game.Screens.Edit { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Right = 5 }, - Child = CreateTimeline() + Child = timelineArea = CreateTimelineArea() }, new BeatDivisorControl(beatDivisor) { RelativeSizeAxes = Axes.Both } }, @@ -97,11 +99,15 @@ namespace osu.Game.Screens.Edit { mainContent.Add(content); content.FadeInFromZero(300, Easing.OutQuint); + + LoadComponentAsync(CreateTimelineContent(), timelineArea.Add); }); } protected abstract Drawable CreateMainContent(); - protected virtual Drawable CreateTimeline() => new TimelineArea { RelativeSizeAxes = Axes.Both }; + protected virtual Drawable CreateTimelineContent() => new Container(); + + protected TimelineArea CreateTimelineArea() => new TimelineArea { RelativeSizeAxes = Axes.Both }; } } diff --git a/osu.Game/Screens/Edit/IEditorBeatmap.cs b/osu.Game/Screens/Edit/IEditorBeatmap.cs index 2f250ba446..3e3418ef79 100644 --- a/osu.Game/Screens/Edit/IEditorBeatmap.cs +++ b/osu.Game/Screens/Edit/IEditorBeatmap.cs @@ -23,6 +23,11 @@ namespace osu.Game.Screens.Edit /// Invoked when a is removed from this . /// event Action HitObjectRemoved; + + /// + /// Invoked when the start time of a in this was changed. + /// + event Action StartTimeChanged; } /// diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index ff78d85bf0..58c9a6a784 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -214,10 +214,13 @@ namespace osu.Game.Screens.Play base.Update(); } + private bool speedAdjustmentsApplied; + private void updateRate() { if (sourceClock == null) return; + speedAdjustmentsApplied = true; sourceClock.ResetSpeedAdjustments(); if (sourceClock is IHasTempoAdjust tempo) @@ -239,7 +242,12 @@ namespace osu.Game.Screens.Play private void removeSourceClockAdjustments() { - sourceClock.ResetSpeedAdjustments(); + if (speedAdjustmentsApplied) + { + sourceClock.ResetSpeedAdjustments(); + speedAdjustmentsApplied = false; + } + (sourceClock as IAdjustableAudioComponent)?.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); } } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 87d902b547..57021dfc68 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -55,7 +55,9 @@ namespace osu.Game.Screens.Play protected override bool PlayResumeSound => false; - private Task loadTask; + protected Task LoadTask { get; private set; } + + protected Task DisposalTask { get; private set; } private InputManager inputManager; private IdleTracker idleTracker; @@ -159,7 +161,7 @@ namespace osu.Game.Screens.Play player.RestartCount = restartCount; player.RestartRequested = restartRequested; - loadTask = LoadComponentAsync(player, _ => info.Loading = false); + LoadTask = LoadComponentAsync(player, _ => info.Loading = false); } private void contentIn() @@ -250,7 +252,7 @@ namespace osu.Game.Screens.Play { if (!this.IsCurrentScreen()) return; - loadTask = null; + LoadTask = null; //By default, we want to load the player and never be returned to. //Note that this may change if the player we load requested a re-run. @@ -301,7 +303,7 @@ namespace osu.Game.Screens.Play if (isDisposing) { // if the player never got pushed, we should explicitly dispose it. - loadTask?.ContinueWith(_ => player.Dispose()); + DisposalTask = LoadTask?.ContinueWith(_ => player.Dispose()); } } diff --git a/osu.Game/Skinning/SkinnableDrawable.cs b/osu.Game/Skinning/SkinnableDrawable.cs index 9ca5d60cb0..fda031e6cb 100644 --- a/osu.Game/Skinning/SkinnableDrawable.cs +++ b/osu.Game/Skinning/SkinnableDrawable.cs @@ -29,13 +29,13 @@ namespace osu.Game.Skinning /// A function to create the default skin implementation of this element. /// A conditional to decide whether to allow fallback to the default implementation if a skinned element is not present. /// How (if at all) the should be resize to fit within our own bounds. - public SkinnableDrawable(ISkinComponent component, Func defaultImplementation, Func allowFallback = null, ConfineMode confineMode = ConfineMode.ScaleDownToFit) + public SkinnableDrawable(ISkinComponent component, Func defaultImplementation, Func allowFallback = null, ConfineMode confineMode = ConfineMode.NoScaling) : this(component, allowFallback, confineMode) { createDefault = defaultImplementation; } - protected SkinnableDrawable(ISkinComponent component, Func allowFallback = null, ConfineMode confineMode = ConfineMode.ScaleDownToFit) + protected SkinnableDrawable(ISkinComponent component, Func allowFallback = null, ConfineMode confineMode = ConfineMode.NoScaling) : base(allowFallback) { this.component = component; diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs index e225bfc490..5352928ec6 100644 --- a/osu.Game/Skinning/SkinnableSprite.cs +++ b/osu.Game/Skinning/SkinnableSprite.cs @@ -19,7 +19,7 @@ namespace osu.Game.Skinning [Resolved] private TextureStore textures { get; set; } - public SkinnableSprite(string textureName, Func allowFallback = null, ConfineMode confineMode = ConfineMode.ScaleDownToFit) + public SkinnableSprite(string textureName, Func allowFallback = null, ConfineMode confineMode = ConfineMode.NoScaling) : base(new SpriteComponent(textureName), allowFallback, confineMode) { } diff --git a/osu.Game/Skinning/SkinnableSpriteText.cs b/osu.Game/Skinning/SkinnableSpriteText.cs index e72f9c9811..567dd348e1 100644 --- a/osu.Game/Skinning/SkinnableSpriteText.cs +++ b/osu.Game/Skinning/SkinnableSpriteText.cs @@ -8,7 +8,7 @@ namespace osu.Game.Skinning { public class SkinnableSpriteText : SkinnableDrawable, IHasText { - public SkinnableSpriteText(ISkinComponent component, Func defaultImplementation, Func allowFallback = null, ConfineMode confineMode = ConfineMode.ScaleDownToFit) + public SkinnableSpriteText(ISkinComponent component, Func defaultImplementation, Func allowFallback = null, ConfineMode confineMode = ConfineMode.NoScaling) : base(component, defaultImplementation, allowFallback, confineMode) { }